├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
└── index.html
├── rollup.config.js
├── src
├── App.svelte
├── components
│ ├── BoardLabel.svelte
│ ├── ConfirmModal.svelte
│ ├── CreateBoard.svelte
│ ├── EditableTitle.svelte
│ ├── SearchPalette.svelte
│ ├── base
│ │ ├── Editor.svelte
│ │ ├── Modal.svelte
│ │ └── Popover.svelte
│ ├── card
│ │ ├── CardDue.svelte
│ │ ├── CardLabel.svelte
│ │ └── CardPriority.svelte
│ ├── column
│ │ ├── InlineEditor.svelte
│ │ ├── Layout.svelte
│ │ ├── List.svelte
│ │ ├── ListCard.svelte
│ │ └── ListMenu.svelte
│ └── zen
│ │ └── Multiselect.svelte
├── datastore
│ ├── db.ts
│ ├── models
│ │ ├── Board.ts
│ │ ├── Card.ts
│ │ ├── List.ts
│ │ ├── Settings.ts
│ │ └── index.ts
│ └── stores.ts
├── main.ts
├── nimbo.ts
├── routes
│ ├── Board.svelte
│ ├── Card.svelte
│ ├── Home.svelte
│ ├── Zen.svelte
│ └── index.ts
├── style
│ └── TailwindCSS.svelte
├── types.ts
└── util
│ └── index.ts
├── tailwind.config.js
└── tsconfig.json
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | tags:
6 | - v**
7 |
8 | jobs:
9 | build:
10 | name: Create release
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Check out repo
15 | uses: actions/checkout@v2
16 | - name: Install NodeJS
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: '12.x'
20 | - name: Build release
21 | run: |
22 | npm install
23 | npm run build-release
24 | - name: Get version
25 | id: nimbo
26 | run: |
27 | echo ::set-output name=VERSION::$(git describe --tags)
28 | - name: Create release
29 | id: create_release
30 | uses: actions/create-release@v1
31 | env:
32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 | with:
34 | tag_name: ${{ steps.nimbo.outputs.VERSION }}
35 | release_name: nimbo ${{ steps.nimbo.outputs.VERSION }}
36 | draft: false
37 | prerelease: false
38 | - name: Upload release
39 | id: upload-release-asset
40 | uses: actions/upload-release-asset@v1
41 | env:
42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43 | with:
44 | upload_url: ${{ steps.create_release.outputs.upload_url }}
45 | asset_path: ./public/nimbo.zip
46 | asset_name: nimbo.zip
47 | asset_content_type: application/zip
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | public/assets/
3 |
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 sereneblue
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nimbo
2 |
3 | 
4 | 
5 |
6 | The nimble Kanban board.
7 |
8 | ## Installation
9 |
10 | You can try nimbo [here](https://sereneblue.github.io/nimbo) or grab a local copy from the [releases](https://github.com/sereneblue/nimbo/releases) page.
11 |
12 | 
13 |
14 | ## Features
15 |
16 | - Works offline, data stored in IndexedDB
17 | - Search for board and cards instantly
18 | - Keyboard shortcuts
19 | - Light/dark theme
20 | - Import/export data between browsers
21 | - Import boards from Trello
22 | - Archive and star boards
23 | - Card checklist, description, due date, labels, and priority
24 | - Markdown support for card descriptions
25 | - Changes are synced between tabs
26 | - Zen mode with time tracking
27 |
28 | See more [here](https://sereneblue.github.io/nimbo/about).
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nimbo",
3 | "version": "1.0.6",
4 | "scripts": {
5 | "build": "rollup -c --environment NODE_ENV:production",
6 | "build-release": "npm run build && cd public && rm assets/*.map && zip -r nimbo.zip .",
7 | "dev": "rollup -c -w",
8 | "start": "sirv public --single",
9 | "validate": "svelte-check"
10 | },
11 | "devDependencies": {
12 | "@rollup/plugin-commonjs": "^14.0.0",
13 | "@rollup/plugin-node-resolve": "^8.0.0",
14 | "@rollup/plugin-replace": "^2.3.4",
15 | "@rollup/plugin-typescript": "^4.0.0",
16 | "@tailwindcss/custom-forms": "^0.2.1",
17 | "@tsconfig/svelte": "^1.0.0",
18 | "autoprefixer": "^9.8.6",
19 | "cssnano": "^4.1.10",
20 | "postcss": "^7.0.35",
21 | "postcss-load-config": "^2.1.2",
22 | "rollup": "^2.35.1",
23 | "rollup-plugin-css-only": "^2.1.0",
24 | "rollup-plugin-livereload": "^2.0.0",
25 | "rollup-plugin-svelte": "^6.1.1",
26 | "rollup-plugin-terser": "^7.0.0",
27 | "svelte": "^3.31.0",
28 | "svelte-check": "^1.1.23",
29 | "svelte-preprocess": "^4.6.1",
30 | "tailwindcss": "^1.9.6",
31 | "tailwindcss-truncate-multiline": "^1.0.3",
32 | "tslib": "^2.0.3",
33 | "typescript": "^3.9.3"
34 | },
35 | "dependencies": {
36 | "broadcast-channel": "^3.3.0",
37 | "dexie": "^3.0.3",
38 | "dexie-export-import": "^1.0.0",
39 | "dompurify": "^2.2.6",
40 | "file-saver": "^2.0.5",
41 | "flatpickr": "^4.6.8",
42 | "fuzzysort": "^1.1.4",
43 | "marked": "^1.2.7",
44 | "nanoid": "^3.1.20",
45 | "parse-duration": "^0.4.4",
46 | "sirv-cli": "^1.0.10",
47 | "sortablejs": "^1.12.0",
48 | "svelte-spa-router": "^3.1.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const prod = !process.env.ROLLUP_WATCH;
2 |
3 | module.exports = {
4 | plugins: [
5 | require('tailwindcss'),
6 | ...(prod
7 | ? [
8 | require('autoprefixer'),
9 | require('cssnano')({
10 | preset: ['default', { discardComments: { removeAll: true } }],
11 | }),
12 | ]
13 | : []),
14 | ],
15 | };
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | nimbo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import replace from '@rollup/plugin-replace';
3 | import resolve from '@rollup/plugin-node-resolve';
4 | import commonjs from '@rollup/plugin-commonjs';
5 | import css from "rollup-plugin-css-only";
6 | import livereload from 'rollup-plugin-livereload';
7 | import { terser } from 'rollup-plugin-terser';
8 | import sveltePreprocess from 'svelte-preprocess';
9 | import typescript from '@rollup/plugin-typescript';
10 |
11 | const production = !process.env.ROLLUP_WATCH;
12 |
13 | function serve() {
14 | let server;
15 |
16 | function toExit() {
17 | if (server) server.kill(0);
18 | }
19 |
20 | return {
21 | writeBundle() {
22 | if (server) return;
23 | server = require('child_process').spawn(
24 | 'npm',
25 | ['run', 'start', '--', '--dev'],
26 | {
27 | stdio: ['ignore', 'inherit', 'inherit'],
28 | shell: true,
29 | }
30 | );
31 |
32 | process.on('SIGTERM', toExit);
33 | process.on('exit', toExit);
34 | },
35 | };
36 | }
37 |
38 | export default {
39 | input: 'src/main.ts',
40 | output: {
41 | sourcemap: true,
42 | format: 'iife',
43 | name: 'app',
44 | file: 'public/assets/bundle.js',
45 | },
46 | plugins: [
47 | replace({
48 | 'process.env.NODE_ENV': production ? JSON.stringify( 'production' ) : JSON.stringify( 'development' )
49 | }),
50 | css({ output: "public/assets/extra.css" }),
51 | svelte({
52 | // enable run-time checks when not in production
53 | dev: !production,
54 | // we'll extract any component CSS out into
55 | // a separate file - better for performance
56 | css: (css) => {
57 | css.write('bundle.css');
58 | },
59 | preprocess: sveltePreprocess({ postcss: true }),
60 | }),
61 |
62 | // If you have external dependencies installed from
63 | // npm, you'll most likely need these plugins. In
64 | // some cases you'll need additional configuration -
65 | // consult the documentation for details:
66 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
67 | resolve({
68 | browser: true,
69 | dedupe: ['svelte'],
70 | }),
71 | commonjs(),
72 | typescript({ sourceMap: !production }),
73 |
74 | // In dev mode, call `npm run start` once
75 | // the bundle has been generated
76 | !production && serve(),
77 |
78 | // Watch the `public` directory and refresh the
79 | // browser on changes when not in production
80 | !production && livereload('public'),
81 |
82 | // If we're building for production (npm run build
83 | // instead of npm run dev), minify
84 | production && terser(),
85 | ],
86 | watch: {
87 | clearScreen: false,
88 | },
89 | };
--------------------------------------------------------------------------------
/src/App.svelte:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
39 | {#await loading then done}
40 |
41 |
42 |
43 | {/await}
44 |
--------------------------------------------------------------------------------
/src/components/BoardLabel.svelte:
--------------------------------------------------------------------------------
1 |
51 |
52 |
53 |
54 |
55 |
56 |
85 |
86 |
--------------------------------------------------------------------------------
/src/components/ConfirmModal.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
26 | {modalMessage}
27 |
28 |
29 |
32 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/components/CreateBoard.svelte:
--------------------------------------------------------------------------------
1 |
61 |
62 |
63 |
89 |
--------------------------------------------------------------------------------
/src/components/EditableTitle.svelte:
--------------------------------------------------------------------------------
1 |
56 |
57 |
62 |
63 |
64 |
65 | {#if isTitle}
66 |
67 |
68 |
69 | {:else}
70 |
71 |
72 |
73 | {/if}
74 |
75 |
--------------------------------------------------------------------------------
/src/components/SearchPalette.svelte:
--------------------------------------------------------------------------------
1 |
132 |
133 |
142 |
143 | {#if $nimboStore.showCommandPalette }
144 |
145 |
146 |
150 |
151 |
152 | {#each searchResults as s}
153 | - handleResultsEvent(e, s) } on:click={e => execAction(s) } tabindex="0">
154 | {#if s.obj.type == RESULT_TYPE.BOARD}
155 | Board
156 | {:else if s.obj.type == RESULT_TYPE.CARD}
157 | Card
158 | {/if}
159 | {#if s.target}
160 | {@html fuzzysort.highlight(s, '', '')}
161 | {:else}
162 | {s.obj.text}
163 | {/if}
164 |
165 | {/each}
166 |
167 |
168 |
169 | {/if}
--------------------------------------------------------------------------------
/src/components/base/Editor.svelte:
--------------------------------------------------------------------------------
1 |
277 |
278 |
279 |
280 | {#if !inline}
281 |
292 | {/if}
293 |
294 | {#if inline}
295 |
303 | {/if}
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 | Description
314 |
315 |
316 | {#if editDescription || cardDetails.card.description }
317 |
318 |
319 |
320 | {/if}
321 |
322 |
323 | {#if cardDetails.card.description && !editDescription}
324 |
325 | {@html DOMPurify.sanitize(marked(cardDetails.card.description))}
326 |
327 | {:else}
328 | {#if editDescription}
329 |
330 | {:else}
331 |
Add a description
332 | {/if}
333 | {/if}
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 | Due Date
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 | Priority
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 | Label
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 | {#if isZen}
375 |
376 |
377 |
378 |
379 | List
380 |
381 |
382 |
383 |
384 |
389 |
390 |
391 | {/if}
392 |
393 |
394 |
395 |
396 | {#if !inline}
397 |
400 | {/if}
401 |
402 |
403 |
404 | Checklist
405 | {checklistProgress}
406 |
407 | {#if cardDetails.card.checklist.length}
408 |
409 |
416 |
417 |
418 | {/if}
419 |
420 |
421 |
422 |
423 | {#if cardDetails.card.checklist.length}
424 |
427 | {/if}
428 |
443 |
444 |
449 |
450 |
451 |
452 |
453 |
454 |
455 | {#if !inline}
456 |
459 | {/if}
460 |
461 |
462 |
463 | Time Tracking
464 | {formatTime(totalTime)}
465 |
466 |
467 |
468 |
469 |
470 |
471 | {#each cardDetails.card.log as l, index (l)}
472 | -
473 |
474 |
{formatTime(l.duration)}
475 |
{formatDate(l.date)}
476 |
477 |
480 |
481 | {/each}
482 |
483 |
484 |
489 |
490 |
491 |
492 |
493 |
494 |
497 |
498 |
--------------------------------------------------------------------------------
/src/components/base/Modal.svelte:
--------------------------------------------------------------------------------
1 |
32 |
33 | {#if isOpen }
34 |
39 | {/if}
--------------------------------------------------------------------------------
/src/components/base/Popover.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 | {#if isOpen }
20 |
21 |
22 |
23 | {/if}
--------------------------------------------------------------------------------
/src/components/card/CardDue.svelte:
--------------------------------------------------------------------------------
1 |
63 |
64 |
217 |
218 |
--------------------------------------------------------------------------------
/src/components/card/CardLabel.svelte:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
39 |
40 |
65 |
66 |
67 |
68 |
69 | {#each labels as l (l)}
70 | - handleSelectLabel(l.color)} data-label={l.color} role="option" class="hover:bg-light-200 dark:hover:bg-dark-100 dark:hover:bg-dark-100 cursor-pointer select-none relative py-2 pl-3 pr-9">
71 |
72 |
81 |
82 | {l.text}
83 |
84 |
85 |
86 | {#if selected === l.color}
87 |
88 |
91 |
92 | {/if}
93 |
94 | {/each}
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/src/components/card/CardPriority.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
47 |
48 |
49 |
52 |
55 |
58 |
61 |
--------------------------------------------------------------------------------
/src/components/column/InlineEditor.svelte:
--------------------------------------------------------------------------------
1 |
47 |
48 |
49 |
55 | {#if id}
56 |
60 | {/if}
61 |
62 |
--------------------------------------------------------------------------------
/src/components/column/Layout.svelte:
--------------------------------------------------------------------------------
1 |
100 |
101 |
102 |
103 |
108 |
109 |
110 |
111 |
112 | {#each board.lists as l (l.id)}
113 |
114 |
120 |
121 | {/each}
122 |
123 |
129 |
130 |
--------------------------------------------------------------------------------
/src/components/column/List.svelte:
--------------------------------------------------------------------------------
1 |
103 |
104 |
115 |
116 |
117 |
122 |
123 |
124 |
125 |
128 |
129 |
130 |
141 |
142 |
143 |
144 | {#each list.cards as c (c.id)}
145 |
handleCardClick(c.id, list.index)}>
146 |
147 |
148 | {/each}
149 |
150 |
151 |
152 |
157 |
158 |
--------------------------------------------------------------------------------
/src/components/column/ListCard.svelte:
--------------------------------------------------------------------------------
1 |
44 |
45 |
48 | {#if card.label != null}
49 |
60 | {/if}
61 |
62 | {card.title}
63 |
64 | {#if hasMoreDetails}
65 |
66 |
67 | {#if card.description}
68 |
71 | {/if}
72 | {#if card.checklist.length}
73 |
74 |
75 |
{checklistCompleted} / {card.checklist.length}
76 |
77 | {/if}
78 | {#if card.log.length}
79 |
80 |
81 |
{formatTime(totalTrackedTime)}
82 |
83 | {/if}
84 | {#if card.due}
85 |
92 |
93 |
{formatDate(card.due)}
94 |
95 | {/if}
96 | {#if card.priority in PRIORITY}
97 |
98 | {#if card.priority == PRIORITY.P1}
99 |
100 | P1
101 |
102 | {:else if card.priority == PRIORITY.P2}
103 |
104 | P2
105 |
106 | {:else if card.priority == PRIORITY.P3}
107 |
108 | P3
109 |
110 | {:else if card.priority == PRIORITY.P4}
111 |
112 | P4
113 |
114 | {/if}
115 |
116 | {/if}
117 |
118 |
119 | {/if}
120 | {#if isZen}
121 |
122 |
133 |
134 | {/if}
135 |
--------------------------------------------------------------------------------
/src/components/column/ListMenu.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 | {#if isOpen}
25 |
29 |
30 |
35 | {#if index > 0}
36 |
50 | {/if}
51 | {#if hasMore}
52 |
66 | {/if}
67 |
82 |
99 |
100 |
101 |
102 | {/if}
--------------------------------------------------------------------------------
/src/components/zen/Multiselect.svelte:
--------------------------------------------------------------------------------
1 |
93 |
94 |
100 |
101 |
102 |
104 | {displayText}
105 |
106 | {#if open}
107 |
108 |
109 | {#each dropdownOptions as o (o.value)}
110 |
114 | {/each}
115 |
116 |
117 | {/if}
118 |
--------------------------------------------------------------------------------
/src/datastore/db.ts:
--------------------------------------------------------------------------------
1 | import Dexie from 'dexie';
2 | import { Board, Card, List, Settings } from './models';
3 |
4 | export class nimboDB extends Dexie {
5 | boards: Dexie.Table;
6 | cards: Dexie.Table;
7 | lists: Dexie.Table;
8 | settings: Dexie.Table;
9 |
10 | constructor() {
11 | super("nimboDB");
12 |
13 | let db = this;
14 |
15 | db.version(1).stores({
16 | boards: "&id, title",
17 | cards: "&id, title, listId, index",
18 | lists: "&id, boardId, index",
19 | settings: "&id"
20 | });
21 |
22 | db.boards.mapToClass(Board);
23 | db.cards.mapToClass(Card);
24 | db.lists.mapToClass(List);
25 | db.settings.mapToClass(Settings);
26 | }
27 | }
--------------------------------------------------------------------------------
/src/datastore/models/Board.ts:
--------------------------------------------------------------------------------
1 | import { nanoid } from '../../util';
2 | import { BOARD_COLORS, BoardColor, LABEL_COLOR } from '../../types';
3 | import type { BoardLabel, SwapObject } from '../../types';
4 | import type List from './List';
5 | import type Card from './Card';
6 |
7 | const BOARD_LABELS: BoardLabel[] = [
8 | { color: LABEL_COLOR.RED, text: "Red" },
9 | { color: LABEL_COLOR.ORANGE, text: "Orange" },
10 | { color: LABEL_COLOR.YELLOW, text: "Yellow" },
11 | { color: LABEL_COLOR.GREEN, text: "Green" },
12 | { color: LABEL_COLOR.TEAL, text: "Teal" },
13 | { color: LABEL_COLOR.BLUE, text: "Blue" },
14 | { color: LABEL_COLOR.PURPLE, text: "Purple" }
15 | ];
16 |
17 | export default class Board {
18 | id: string;
19 | title: string;
20 | color: number;
21 | isArchived: boolean;
22 | isStarred: boolean;
23 | labels: BoardLabel[];
24 | lastViewTime: number;
25 | lists: List[];
26 |
27 | constructor(title: string) {
28 | this.id = nanoid();
29 | this.title = title;
30 | this.isArchived = false;
31 | this.isStarred = false;
32 | this.lastViewTime = new Date().getTime();
33 | this.color = this.getRandomColor();
34 | this.lists = new Array();
35 | this.labels = BOARD_LABELS;
36 |
37 | Object.defineProperties(this, {
38 | lists: { value: [], enumerable: false, writable: true }
39 | });
40 | }
41 |
42 | private getRandomColor(): number {
43 | return Math.floor(Math.random() * BOARD_COLORS.length);
44 | }
45 |
46 | setLabels(labels: BoardLabel[]): void {
47 | this.labels = labels;
48 | }
49 |
50 | setLastViewTime(timestamp: number): void {
51 | this.lastViewTime = timestamp;
52 | }
53 |
54 | setTitle(title: string): void {
55 | this.title = title;
56 | }
57 |
58 | swapLists(l: SwapObject): void {
59 | this.lists[l.from].index = l.to;
60 | this.lists[l.to].index = l.from;
61 |
62 | let tmp: List = this.lists[l.from];
63 | this.lists[l.from] = this.lists[l.to];
64 | this.lists[l.to] = tmp;
65 | }
66 |
67 | toggleBoardArchive(): void {
68 | this.isArchived = !this.isArchived;
69 | }
70 |
71 | toggleBoardStar(): void {
72 | this.isStarred = !this.isStarred;
73 | }
74 | }
--------------------------------------------------------------------------------
/src/datastore/models/Card.ts:
--------------------------------------------------------------------------------
1 | import { nanoid } from '../../util';
2 | import type List from './List';
3 | import type { ChecklistItem, PRIORITY, LABEL_COLOR, TimeEntry } from '../../types';
4 |
5 | export default class Card {
6 | created: number;
7 | checklist: ChecklistItem[];
8 | description: string;
9 | due: number;
10 | id: string;
11 | isComplete: boolean;
12 | index: number;
13 | label: LABEL_COLOR;
14 | listId: string;
15 | log: TimeEntry[];
16 | priority: PRIORITY;
17 | title: string;
18 |
19 | constructor(list: List, title: string) {
20 | this.id = nanoid();
21 | this.checklist = new Array();
22 | this.created = new Date().getTime();
23 | this.description = "";
24 | this.index = list.cards.length;
25 | this.label = null;
26 | this.listId = list.id;
27 | this.log = new Array();
28 | this.title = title;
29 | }
30 |
31 | addToChecklist(text: string) {
32 | this.checklist.push({
33 | checked: false,
34 | text
35 | });
36 | }
37 |
38 | deleteFromChecklist(index: number) {
39 | this.checklist.splice(index, 1);
40 | }
41 |
42 | deleteTimeEntry(index: number) {
43 | this.log.splice(index, 1);
44 | }
45 |
46 | setComplete(complete: boolean) {
47 | this.isComplete = complete;
48 | }
49 |
50 | setDueDate(timestamp: number) {
51 | this.due = timestamp;
52 | }
53 |
54 | setDescription(text: string) {
55 | this.description = text;
56 | }
57 |
58 | setLabel(label: LABEL_COLOR) {
59 | this.label = label;
60 | }
61 |
62 | setPriority(priority: PRIORITY) {
63 | this.priority = priority;
64 | }
65 |
66 | setTitle(title: string) {
67 | this.title = title;
68 | }
69 |
70 | trackTime(duration: number) {
71 | this.log.push({
72 | duration,
73 | date: new Date().getTime()
74 | });
75 | }
76 | }
--------------------------------------------------------------------------------
/src/datastore/models/List.ts:
--------------------------------------------------------------------------------
1 | import { nanoid } from '../../util';
2 | import type Card from './Card';
3 |
4 | export default class List {
5 | id: string;
6 | boardId: string;
7 | index: number;
8 | title: string;
9 | cards: Card[];
10 |
11 | constructor(boardId: string, index: number) {
12 | this.id = nanoid();
13 | this.title = "New List";
14 | this.index = index;
15 | this.boardId = boardId;
16 | this.cards = new Array();
17 |
18 | Object.defineProperties(this, {
19 | cards: { value: [], enumerable: false, writable: true }
20 | });
21 | }
22 |
23 | deleteCard(cardId: string): void {
24 | let cardIndex: number = this.cards.findIndex(c => c.id === cardId);
25 |
26 | if (cardIndex > -1) {
27 | this.cards.splice(cardIndex, 1);
28 | }
29 | }
30 |
31 | setTitle(title: string): void {
32 | this.title = title;
33 | }
34 | }
--------------------------------------------------------------------------------
/src/datastore/models/Settings.ts:
--------------------------------------------------------------------------------
1 | import type { DueFilterOptions, Theme } from '../../types';
2 |
3 | export default class Settings {
4 | id: number;
5 | dueFilter: DueFilterOptions;
6 | listFilter: string;
7 | maxCards: number;
8 | theme: Theme;
9 | selectedBoards: string[];
10 |
11 | constructor() {
12 | this.id = 1;
13 | this.theme = "dark";
14 | this.maxCards = 5;
15 | this.dueFilter = "all";
16 | this.listFilter = "";
17 | this.selectedBoards = ["all"];
18 | }
19 | }
--------------------------------------------------------------------------------
/src/datastore/models/index.ts:
--------------------------------------------------------------------------------
1 | import Board from "./Board";
2 | import Card from "./Card";
3 | import List from "./List";
4 | import Settings from "./Settings";
5 |
6 | export {
7 | Board,
8 | Card,
9 | List,
10 | Settings
11 | }
--------------------------------------------------------------------------------
/src/datastore/stores.ts:
--------------------------------------------------------------------------------
1 | import { writable, Writable } from 'svelte/store';
2 | import nimbo from '../nimbo';
3 |
4 | export const nimboStore: Writable = writable(new nimbo())
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import App from './App.svelte';
2 |
3 | const app = new App({
4 | target: document.body
5 | });
6 |
7 | export default app;
--------------------------------------------------------------------------------
/src/nimbo.ts:
--------------------------------------------------------------------------------
1 | import { exportDB, peakImportFile} from "dexie-export-import";
2 | import { nimboDB } from './datastore/db';
3 | import type { BroadcastChannel } from 'broadcast-channel';
4 | import { Board, Card, List, Settings } from './datastore/models';
5 | import { BoardLabel, CardDetails, PRIORITY, RESULT_TYPE, SearchObject, SortObject, SwapObject, Theme } from './types';
6 | import { getTimestamps } from './util';
7 |
8 | export default class nimbo {
9 | db: nimboDB;
10 | boards: Board[];
11 | channel: BroadcastChannel;
12 | selectedCardId: string;
13 | settings: Settings;
14 | showCommandPalette: boolean;
15 |
16 | constructor() {
17 | this.db = new nimboDB();
18 | this.boards = new Array();
19 | this.selectedCardId = null;
20 | this.settings = new Settings();
21 | this.showCommandPalette = false;
22 | }
23 |
24 | public async init(): Promise {
25 | this.boards = await this.db.boards.toArray();
26 |
27 | let lists: List[][] = await Promise.all(this.boards.map(b => this.db.lists.where({ boardId: b.id }).toArray()));
28 |
29 | for (let i: number = 0; i < lists.length; i++) {
30 | this.boards[i].lists = lists[i].sort((a: List, b: List): number => {
31 | return a.index - b.index
32 | });
33 |
34 | let cards: Card[][] = await Promise.all(this.boards[i].lists.map(l => this.db.cards.where({ listId: l.id }).toArray()));
35 |
36 | for (let j: number = 0; j < this.boards[i].lists.length; j++) {
37 | this.boards[i].lists[j].cards = cards[j].sort((a: Card, b: Card): number => {
38 | return a.index - b.index
39 | });
40 | }
41 | }
42 |
43 | let settings: Settings[] = await this.db.settings.toArray();
44 | if (settings.length === 0) {
45 | await this.updateSettings(this.settings);
46 | } else {
47 | this.settings = settings[0];
48 | }
49 |
50 | return true;
51 | }
52 |
53 | public openPalette() {
54 | this.showCommandPalette = true;
55 | }
56 |
57 | public closePalette() {
58 | this.showCommandPalette = false;
59 | }
60 |
61 | public async addNewCard(list: List, title: string): Promise {
62 | let boardIndex: number = this.boards.findIndex(b => b.id === list.boardId);
63 |
64 | if (boardIndex > -1) {
65 | let listIndex: number = this.boards[boardIndex].lists.findIndex(l => l.id === list.id);
66 |
67 | if (listIndex > -1) {
68 | let c: Card = new Card(list, title);
69 |
70 | this.boards[boardIndex].lists[listIndex].cards = [...this.boards[boardIndex].lists[listIndex].cards, c];
71 |
72 | await this.db.cards.add(c);
73 | }
74 | }
75 |
76 | this.update();
77 | }
78 |
79 | public async addNewList(boardId: string, index: number = null): Promise {
80 | let boardIndex: number = this.boards.findIndex(b => b.id === boardId);
81 |
82 | if (boardIndex > -1) {
83 | let l: List = new List(boardId, this.boards[boardIndex].lists.length);
84 |
85 | if (index != null) {
86 | let listIndexes: object = {};
87 |
88 | this.boards[boardIndex].lists.splice(index + 1, 0, l);
89 | this.boards[boardIndex].lists = this.boards[boardIndex].lists;
90 |
91 | for (let i: number = 0; i < this.boards[boardIndex].lists.length; i++) {
92 | this.boards[boardIndex].lists[i].index = i;
93 |
94 | listIndexes[this.boards[boardIndex].lists[i].id] = i;
95 | };
96 |
97 | await this.updateListIndexes(listIndexes);
98 | } else {
99 | this.boards[boardIndex].lists = [...this.boards[boardIndex].lists, l];
100 | }
101 |
102 | await this.db.lists.add(l);
103 | }
104 |
105 | this.update();
106 | }
107 |
108 | public async createBoard(title: string): Promise {
109 | let b: Board = new Board(title);
110 | this.boards = [...this.boards, b];
111 |
112 | await this.db.boards.add(b);
113 |
114 | this.update();
115 |
116 | return b.id;
117 | }
118 |
119 | public async deleteBoard(id: string): Promise {
120 | let boardIndex: number = this.boards.findIndex(b => b.id === id);
121 |
122 | if (boardIndex > -1) {
123 | this.boards.splice(boardIndex, 1);
124 | }
125 |
126 | await this.db.boards.delete(id);
127 |
128 | this.update();
129 | }
130 |
131 | public async deleteCard(list: List, cardId: string): Promise {
132 | for (let i: number = 0; i < this.boards.length; i++) {
133 | for (let j: number = 0; j < this.boards[i].lists.length; j++) {
134 | if (this.boards[i].lists[j].id === list.id) {
135 | list.deleteCard(cardId);
136 |
137 | this.boards[i].lists[j] = list;
138 | }
139 | }
140 | }
141 |
142 | await this.db.cards.delete(cardId);
143 |
144 | this.update();
145 | }
146 |
147 | public async deleteList(boardId: string, listId: string): Promise {
148 | let boardIndex: number = this.boards.findIndex(b => b.id === boardId);
149 |
150 | if (boardIndex > -1) {
151 | let listIndex: number = this.boards[boardIndex].lists.findIndex(l => l.id === listId);
152 |
153 | if (listIndex > -1) {
154 | let listIndexes: object = {};
155 |
156 | this.boards[boardIndex].lists.splice(listIndex, 1);
157 |
158 | for (let i: number = 0; i < this.boards[boardIndex].lists.length; i++) {
159 | this.boards[boardIndex].lists[i].index = i;
160 |
161 | listIndexes[this.boards[boardIndex].lists[i].id] = i;
162 | };
163 |
164 | await this.db.transaction('rw', this.db.lists, this.db.cards, async (): Promise =>{
165 | await Promise.all([
166 | this.db.lists.delete(listId),
167 | this.db.cards.where({ listId }).delete()
168 | ]);
169 | });
170 |
171 | await this.updateListIndexes(listIndexes);
172 | }
173 | }
174 |
175 | this.update();
176 | }
177 |
178 | public async export(): Promise {
179 | return await exportDB(this.db);
180 | }
181 |
182 | public everything(): SearchObject[] {
183 | return (this.boards.map((b) => {
184 | if (b.isArchived) return [];
185 |
186 | return [{
187 | type: RESULT_TYPE.BOARD,
188 | text: b.title,
189 | path: `/b/${b.id}`
190 | }, b.lists.map(l => {
191 | return l.cards.map(c => {
192 | return {
193 | type: RESULT_TYPE.CARD,
194 | text: c.title,
195 | path: `/c/${c.id}`
196 | };
197 | });
198 | })
199 | ];
200 | }) as any).flat(4);
201 | }
202 |
203 | public getBoard(boardId: string): Board | null {
204 | let boardIndex: number = this.boards.findIndex(b => b.id === boardId);
205 |
206 | if (boardIndex > -1) {
207 | return this.boards[boardIndex];
208 | }
209 |
210 | return null;
211 | }
212 |
213 | public getCardDetails(cardId: string): CardDetails | null {
214 | for (let i: number = 0; i < this.boards.length; i++) {
215 | for (let j: number = 0; j < this.boards[i].lists.length; j++) {
216 | for (let k: number = 0; k < this.boards[i].lists[j].cards.length; k++) {
217 | if (this.boards[i].lists[j].cards[k].id === cardId) {
218 | return {
219 | board: this.boards[i],
220 | list: this.boards[i].lists[j],
221 | card: this.boards[i].lists[j].cards[k]
222 | }
223 | }
224 | }
225 | }
226 | }
227 |
228 | return null;
229 | }
230 |
231 | public async import(db: File): Promise {
232 | try {
233 | let e = await peakImportFile(db);
234 |
235 | if (e.formatName == "dexie" && e.formatVersion == 1) {
236 | await this.db.import(db, {
237 | clearTablesBeforeImport: true
238 | });
239 |
240 | this.update();
241 | return true;
242 | }
243 | } catch (err) {};
244 |
245 | return;
246 | }
247 |
248 | public async importTrelloBoard(b: any): Promise {
249 | let board: Board = new Board(b.name);
250 |
251 | board.lastViewTime = new Date(b.dateLastView).getTime();
252 | board.isStarred = b.starred;
253 | board.isArchived = b.closed;
254 |
255 | let cards: Card[] = [];
256 | let lists: List[] = [];
257 |
258 | for (let i: number = 0; i < b.lists.length; i++) {
259 | let l: List = new List(board.id, i);
260 | l.setTitle(b.lists[i].name);
261 |
262 | for (let j: number = 0; j < b.cards.length; j++) {
263 | if (b.lists[i].id === b.cards[j].idList) {
264 | let c: Card = new Card(l, b.cards[j].name);
265 |
266 | c.description = b.cards[j].desc;
267 | c.due = b.cards[j].due ? new Date(b.cards[j].due).getTime() : null;
268 | c.isComplete = b.cards[j].dueComplete;
269 |
270 | l.cards.push(c);
271 | }
272 | }
273 |
274 | lists.push(l);
275 | }
276 |
277 | for (let i: number = 0; i < lists.length; i++) {
278 | cards = cards.concat(lists[i].cards);
279 | }
280 |
281 | await this.db.transaction('rw', this.db.boards, this.db.lists, this.db.cards, async (): Promise =>{
282 | await Promise.all([
283 | this.db.boards.add(board),
284 | this.db.lists.bulkAdd(lists),
285 | this.db.cards.bulkAdd(cards)
286 | ]);
287 | });
288 |
289 | this.update();
290 |
291 | return board.id;
292 | }
293 |
294 | public async moveCard(c: SortObject): Promise {
295 | let boardIndex: number = this.boards.findIndex(b => b.id === c.boardId);
296 |
297 | if (boardIndex > -1) {
298 | let listFromIndex: number = this.boards[boardIndex].lists.findIndex(l => l.id === c.from.list);
299 | let listToIndex: number = this.boards[boardIndex].lists.findIndex(l => l.id === c.to.list);
300 |
301 | if (listFromIndex > -1 && listToIndex > -1) {
302 | let cardIndexes: object = {};
303 | let tmp: Card;
304 |
305 | if (c.from.list != c.to.list) {
306 | let fromListId: string;
307 |
308 | tmp = this.boards[boardIndex].lists[listFromIndex].cards.splice(c.from.index, 1)[0];
309 | fromListId = tmp.listId;
310 |
311 | tmp.listId = this.boards[boardIndex].lists[listToIndex].id;
312 | tmp.index = c.to.index;
313 |
314 | for (let i: number = 0; i < this.boards[boardIndex].lists[listFromIndex].cards.length; i++) {
315 | this.boards[boardIndex].lists[listFromIndex].cards[i].index = i;
316 |
317 | cardIndexes[this.boards[boardIndex].lists[listFromIndex].cards[i].id] = i;
318 | }
319 |
320 | this.boards[boardIndex].lists[listToIndex].cards.splice(c.to.index, 0, tmp);
321 |
322 | for (let i: number = 0; i < this.boards[boardIndex].lists[listToIndex].cards.length; i++) {
323 | this.boards[boardIndex].lists[listToIndex].cards[i].index = i;
324 |
325 | cardIndexes[this.boards[boardIndex].lists[listToIndex].cards[i].id] = i;
326 | }
327 | } else {
328 | if (c.from.index != c.to.index) {
329 | tmp = this.boards[boardIndex].lists[listFromIndex].cards.splice(c.from.index, 1)[0];
330 | tmp.index = c.to.index;
331 |
332 | this.boards[boardIndex].lists[listFromIndex].cards.splice(c.to.index, 0, tmp);
333 |
334 | for (let i: number = 0; i < this.boards[boardIndex].lists[listFromIndex].cards.length; i++) {
335 | this.boards[boardIndex].lists[listFromIndex].cards[i].index = i;
336 |
337 | cardIndexes[this.boards[boardIndex].lists[listFromIndex].cards[i].id] = i;
338 | }
339 | }
340 | }
341 |
342 | await this.updateCardIndexes(tmp, cardIndexes);
343 | }
344 | }
345 | }
346 |
347 | public async refresh(): Promise {
348 | await this.init();
349 | }
350 |
351 | public setChannel(channel: BroadcastChannel): void {
352 | this.channel = channel;
353 | }
354 |
355 | public setSelectedCard(cardId: string | null): void {
356 | this.selectedCardId = cardId;
357 | }
358 |
359 | public async swapLists(boardId: string, l: SwapObject): Promise {
360 | let boardIndex: number = this.boards.findIndex(b => b.id === boardId);
361 |
362 | if (boardIndex > -1) {
363 | this.boards[boardIndex].swapLists(l);
364 |
365 | await this.db.transaction('rw', this.db.lists, async (): Promise =>{
366 | let [from, to]: List[] = await Promise.all([
367 | this.db.lists.get({ boardId, index: l.from }),
368 | this.db.lists.get({ boardId, index: l.to })
369 | ]);
370 |
371 | from.index = l.to;
372 | to.index = l.from;
373 |
374 | await this.db.lists.bulkPut([from, to]);
375 | });
376 | }
377 |
378 | this.update();
379 | }
380 |
381 | public async toggleBoardArchive(boardId: string): Promise {
382 | let boardIndex: number = this.boards.findIndex(b => b.id === boardId);
383 |
384 | if (boardIndex > -1) {
385 | this.boards[boardIndex].toggleBoardArchive();
386 |
387 | await this.db.boards.update(boardId, {
388 | isArchived: this.boards[boardIndex].isArchived
389 | });
390 | }
391 |
392 | this.update();
393 | }
394 |
395 | public async toggleBoardStar(boardId: string): Promise {
396 | let boardIndex: number = this.boards.findIndex(b => b.id === boardId);
397 |
398 | if (boardIndex > -1) {
399 | this.boards[boardIndex].toggleBoardStar();
400 |
401 | await this.db.boards.update(boardId, {
402 | isStarred: this.boards[boardIndex].isStarred
403 | });
404 | }
405 |
406 | this.update();
407 | }
408 |
409 | public async toggleTheme(): Promise {
410 | let theme: Theme = this.settings.theme === 'dark' ? 'light' : 'dark';
411 |
412 | this.settings.theme = theme;
413 |
414 | await this.updateSettings(this.settings);
415 | this.update();
416 | }
417 |
418 | public update(): void {
419 | this.channel.postMessage("update");
420 | }
421 |
422 | public async updateBoardTitle(boardId: string, title: string): Promise {
423 | let boardIndex: number = this.boards.findIndex(b => b.id === boardId);
424 |
425 | if (boardIndex > -1) {
426 | this.boards[boardIndex].setTitle(title);
427 |
428 | await this.db.boards.update(boardId, {
429 | title
430 | });
431 | }
432 |
433 | this.update();
434 | }
435 |
436 | public async updateCard(c: Card, property: string): Promise {
437 | for (let i: number = 0; i < this.boards.length; i++) {
438 | for (let j: number = 0; j < this.boards[i].lists.length; j++) {
439 | for (let k: number = 0; k < this.boards[i].lists[j].cards.length; k++) {
440 | if (this.boards[i].lists[j].cards[k].id === c.id) {
441 | this.boards[i].lists[j].cards[k] = c;
442 | }
443 | }
444 | }
445 | }
446 |
447 | await this.db.cards.update(c.id, {
448 | [property]: c[property]
449 | });
450 |
451 | this.update();
452 | }
453 |
454 | private async updateCardIndexes(c: Card, cardIndexes: object): Promise {
455 | await this.db.transaction('rw', this.db.cards, async (): Promise =>{
456 | await this.db.cards.update(c.id, {
457 | listId: c.listId
458 | });
459 |
460 | await this.db.cards.where("id").anyOf(Object.keys(cardIndexes)).modify(c => {
461 | c.index = cardIndexes[c.id];
462 | })
463 | });
464 |
465 | this.update();
466 | }
467 |
468 | public async updateLabels(boardId: string, labels: BoardLabel[]): Promise {
469 | let boardIndex: number = this.boards.findIndex(b => b.id === boardId);
470 |
471 | if (boardIndex > -1) {
472 | this.boards[boardIndex].setLabels(labels);
473 |
474 | await this.db.boards.update(boardId, {
475 | labels
476 | });
477 | }
478 |
479 | this.update();
480 | }
481 |
482 | private async updateListIndexes(listIndexes: object): Promise {
483 | await this.db.lists.where("id").anyOf(Object.keys(listIndexes)).modify(l => {
484 | l.index = listIndexes[l.id];
485 | });
486 | }
487 |
488 | public async updateListTitle(boardId: string, listId: string, title: string): Promise {
489 | let boardIndex: number = this.boards.findIndex(b => b.id === boardId);
490 |
491 | if (boardIndex > -1) {
492 | let listIndex: number = this.boards[boardIndex].lists.findIndex(l => l.id === listId);
493 |
494 | if (listIndex > -1) {
495 | this.boards[boardIndex].lists[listIndex].setTitle(title);
496 |
497 | await this.db.lists.update(listId, {
498 | title
499 | });
500 | }
501 | }
502 |
503 | this.update();
504 | }
505 |
506 | public async updateSettings(s: Settings): Promise {
507 | await this.db.settings.put(s);
508 |
509 | this.settings = s;
510 |
511 | this.update();
512 | }
513 |
514 | public async updateViewTime(boardId: string): Promise {
515 | let boardIndex: number = this.boards.findIndex(b => b.id === boardId);
516 |
517 | if (boardIndex > -1) {
518 | let lastViewTime: number = new Date().getTime();
519 | this.boards[boardIndex].setLastViewTime(lastViewTime);
520 |
521 | await this.db.boards.update(boardId, {
522 | lastViewTime
523 | });
524 | }
525 |
526 | this.update();
527 | }
528 |
529 | public async zenCards(): Promise {
530 | let d: Date = new Date();
531 | d.setHours(23, 59, 59, 999);
532 |
533 | let t: any = getTimestamps(d);
534 | let allBoardsSelected: boolean = this.settings.selectedBoards.includes('all');
535 | let listFilters: string[] = this.settings.listFilter.split(',').map(f => f.toLowerCase());
536 |
537 | let lists: string[] = this.boards
538 | .filter(b => allBoardsSelected || this.settings.selectedBoards.includes(b.id))
539 | .map(b => {
540 | return b.lists
541 | .filter(l => {
542 | return listFilters.length === 0 || listFilters.some(filter => l.title.toLowerCase().includes(filter))
543 | })
544 | .map(l => l.id)
545 | })
546 | .reduce((acc, val) => acc.concat(val), []);
547 |
548 | let cards: Card[] = await this.db.cards
549 | .where('listId')
550 | .anyOf(lists)
551 | .filter((c: Card) => {
552 | if (this.settings.dueFilter === "all") {
553 | return true;
554 | } else if (this.settings.dueFilter === "overdue") {
555 | return c.isComplete && c.due && d.getTime() > c.due;
556 | } else if (this.settings.dueFilter === "tomorrow") {
557 | return !c.isComplete && c.due && c.due < t.tomorrow;
558 | } else if (this.settings.dueFilter === "week") {
559 | return !c.isComplete && c.due && c.due < t.week;
560 | } else if (this.settings.dueFilter === "month") {
561 | return !c.isComplete && c.due && c.due < t.month;
562 | }
563 | })
564 | .toArray();
565 |
566 | // sort by due date
567 | let cardsSortByDueDate = cards.filter(c => c.due && !c.isComplete);
568 | cardsSortByDueDate.sort((a: Card, b: Card) => {
569 | return a.due - b.due
570 | });
571 |
572 | let cardsWithOutDueDate = cards.filter(c => c.due === null || c.due === undefined);
573 |
574 | // sort by priority
575 | let cardsSortByPriority = cardsWithOutDueDate.filter(c => c.priority in PRIORITY);
576 | cardsSortByPriority.sort((a: Card, b: Card) => {
577 | return a.priority - b.priority
578 | });
579 |
580 | // sort by creation date
581 | let cardsSortByCreationDate = cardsWithOutDueDate.filter(c => !(c.priority in PRIORITY));
582 | cardsSortByCreationDate.sort((a: Card, b: Card) => {
583 | return a.created - b.created
584 | });
585 |
586 | cards = cardsSortByDueDate.concat(cardsSortByPriority, cardsSortByCreationDate);
587 |
588 | return cards.slice(0, this.settings.maxCards);
589 | }
590 | }
--------------------------------------------------------------------------------
/src/routes/Board.svelte:
--------------------------------------------------------------------------------
1 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | {title}
63 |
64 |
65 |
66 | {#if boardIndex > -1}
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | {:else}
81 |
88 | {/if}
89 |
--------------------------------------------------------------------------------
/src/routes/Card.svelte:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 | {title}
61 |
62 |
63 |
64 |
65 |
70 |
71 |
72 |
73 | {#if cardDetails}
74 |
75 | {:else}
76 |
77 |
78 | ¯\_(ツ)_/¯
79 |
80 |
Card not found
81 |
Go to home
82 |
83 | {/if}
84 |
85 |
--------------------------------------------------------------------------------
/src/routes/Home.svelte:
--------------------------------------------------------------------------------
1 |
106 |
107 |
108 |
109 |
110 | My Boards | nimbo
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
My Boards
120 |
127 |
128 |
129 |
130 |
143 |
144 |
150 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | Starred
164 |
165 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | Recently Viewed
180 |
181 |
189 |
190 |
191 |
229 |
230 |
--------------------------------------------------------------------------------
/src/routes/Zen.svelte:
--------------------------------------------------------------------------------
1 |
96 |
97 |
98 |
99 |
100 | Zen Mode | nimbo
101 |
102 |
103 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | {#if cards.length}
116 | {#each cards as c}
117 |
handleCardClick(c.id)}>
118 |
119 |
120 | {/each}
121 | {:else}
122 |
123 | No cards to display.
124 |
125 | {/if}
126 |
127 |
128 |
129 |
135 |
136 | {#if showFilters}
137 |
138 |
139 |
140 | Boards
141 |
142 |
143 |
144 | Lists
145 |
151 |
152 |
162 |
171 |
172 |
173 | {/if}
174 |
175 | {#if $nimboStore.selectedCardId}
176 |
177 |
178 |
179 | {/if}
180 |
181 |
182 |
--------------------------------------------------------------------------------
/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import Board from "./Board.svelte";
2 | import Card from "./Card.svelte";
3 | import Home from "./Home.svelte";
4 | import Zen from "./Zen.svelte";
5 |
6 | export {
7 | Board,
8 | Card,
9 | Home,
10 | Zen
11 | }
--------------------------------------------------------------------------------
/src/style/TailwindCSS.svelte:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { Board, Card, List } from "./datastore/models";
2 |
3 | export const BOARD_COLORS: string[] = [
4 | "#DC7684",
5 | "#A0C1B8",
6 | "#2D7F9D",
7 | "#A1B6B4",
8 | "#717D84",
9 | "#B5A25D",
10 | "#6B436E",
11 | "#9A83A3",
12 | "#737088",
13 | "#A63D11",
14 | "#E97140",
15 | "#818773"
16 | ];
17 |
18 | export type BoardColor = typeof BOARD_COLORS[number];
19 |
20 | export type BoardLabel = {
21 | color: LABEL_COLOR;
22 | text: string;
23 | }
24 |
25 | export type CardDetails = {
26 | board: Board;
27 | list: List;
28 | card: Card;
29 | }
30 |
31 | export type ChecklistItem = {
32 | text: string;
33 | checked: boolean;
34 | }
35 |
36 | export type DueFilterOptions = "all" | "overdue" | "month" | "tomorrow" | "week";
37 |
38 | export enum PRIORITY {
39 | P1,
40 | P2,
41 | P3,
42 | P4
43 | }
44 |
45 | export enum LABEL_COLOR {
46 | RED,
47 | ORANGE,
48 | YELLOW,
49 | GREEN,
50 | TEAL,
51 | BLUE,
52 | PURPLE
53 | }
54 |
55 | export enum RESULT_TYPE {
56 | BOARD,
57 | CARD
58 | }
59 |
60 | export type SearchObject = {
61 | type: RESULT_TYPE;
62 | text: string;
63 | path: string;
64 | }
65 |
66 | export type SortObject = {
67 | boardId: string,
68 | from: {
69 | list: string,
70 | index: number
71 | },
72 | to: {
73 | list: string,
74 | index: number
75 | }
76 | }
77 |
78 | export type SwapObject = {
79 | from: number;
80 | to: number;
81 | }
82 |
83 | export type TimeEntry = {
84 | date: number;
85 | duration: number;
86 | }
87 |
88 | export type Theme = "dark" | "light";
--------------------------------------------------------------------------------
/src/util/index.ts:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from 'nanoid';
2 |
3 | const d: Date = new Date();
4 | const day: number = d.getDate();
5 | const month: number = d.getMonth();
6 | const year: number = d.getFullYear();
7 |
8 | const beforeDate = (timestamp: number, when: string): boolean => {
9 | let tmpDate = new Date();
10 |
11 | if (when === 'today') {
12 | return timestamp < tmpDate.getTime();
13 | } else if (when === 'tomorrow') {
14 | tmpDate.setDate(tmpDate.getDate() + 1);
15 | return timestamp < tmpDate.getTime();
16 | }
17 |
18 | return false;
19 | }
20 |
21 | const clickOutside = (node: Node, closeAction: Function): object => {
22 | const handleClick = (event: Event) => {
23 | let path = event.composedPath();
24 |
25 | if (!path.includes(node)) {
26 | closeAction();
27 | }
28 | };
29 |
30 | setTimeout(() => {
31 | document.addEventListener("click", handleClick);
32 | }, 10);
33 |
34 | return {
35 | destroy() {
36 | document.removeEventListener("click", handleClick);
37 | },
38 | };
39 | };
40 |
41 | const getTimestamps = (d: Date): object => {
42 | let tmp: Date = new Date(d);
43 | let timestamps: any = {};
44 |
45 | tmp.setDate(d.getDate() + 1);
46 | timestamps.tomorrow = tmp.getTime();
47 |
48 | tmp.setDate(d.getDate() + 7);
49 | timestamps.week = tmp.getTime();
50 |
51 | tmp.setDate(d.getDate() + 30);
52 | timestamps.month = tmp.getTime();
53 |
54 | return timestamps;
55 | }
56 |
57 | const formatDate = (timestamp: number): string => {
58 | d.setTime(timestamp);
59 |
60 | if (
61 | d.getDate() === day &&
62 | d.getMonth() === month &&
63 | d.getFullYear() === year
64 | ) {
65 | return d.toLocaleString('default', { hour: 'numeric', minute: 'numeric' });
66 | } else if (d.getFullYear() == year) {
67 | return d.toLocaleString('default', { month: 'short', day: 'numeric' });
68 | }
69 |
70 | return d.toLocaleString('default', { year: 'numeric' });
71 | }
72 |
73 | const formatForStopwatch = (seconds: number): string => {
74 | let hours = Math.floor(seconds / 3600)
75 | let minutes = Math.floor(seconds / 60) % 60
76 | seconds = seconds % 60
77 |
78 | return [hours,minutes,seconds]
79 | .map(v => v < 10 ? "0" + v : v)
80 | .filter((v,i) => v !== "00" || i > 0)
81 | .join(":")
82 | }
83 |
84 | const formatTime = (seconds: number): string => {
85 | let t: string = "";
86 |
87 | let hrs: number = Math.floor(seconds / 3600);
88 | let min: number = Math.floor((seconds % 3600) / 60);
89 | let sec: number = seconds % 60;
90 |
91 | if (hrs) {
92 | t += `${hrs}h `;
93 | }
94 |
95 | if (min) {
96 | t += `${min}m `;
97 | }
98 |
99 | if (sec) {
100 | t += `${sec}s`;
101 | }
102 |
103 | return t;
104 | }
105 |
106 | const nanoid = customAlphabet('2346789ABCDEFGHJKLMNPQRTUVWXYZabcdefghijkmnpqrtwxyz', 10);
107 |
108 | export {
109 | beforeDate,
110 | clickOutside,
111 | getTimestamps,
112 | formatDate,
113 | formatForStopwatch,
114 | formatTime,
115 | nanoid
116 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: {
3 | mode: 'all',
4 | content: [
5 | "./src/**/*.svelte"
6 | ],
7 | options: {
8 | defaultExtractor: (content) => {
9 | const regExp = new RegExp(/[A-Za-z0-9-_:\./]+/g)
10 | const matchedTokens = []
11 | let match = regExp.exec(content)
12 |
13 | while (match) {
14 | if (match[0].startsWith('class:')) {
15 | matchedTokens.push(match[0].substring(6))
16 | } else {
17 | matchedTokens.push(match[0])
18 | }
19 | match = regExp.exec(content)
20 | }
21 | return matchedTokens
22 | }
23 | }
24 | },
25 | theme: {
26 | customForms: theme => ({
27 | default: {
28 | checkbox: {
29 | '&:focus': {
30 | boxShadow: '0 0 0 3px rgba(102, 126, 234, 0.5)',
31 | borderColor: theme('colors.indigo.400')
32 | }
33 | },
34 | select: {
35 | '&:focus': {
36 | boxShadow: '0 0 0 3px rgba(102, 126, 234, 0.5)',
37 | borderColor: theme('colors.indigo.400')
38 | }
39 | }
40 | }
41 | }),
42 | extend: {
43 | colors: {
44 | "dark-100": "#4a4c4d",
45 | "dark-100-50": "rgba(74, 76, 77, 0.5)",
46 | "dark-200": "#343537",
47 | "dark-300": "#1d1f21",
48 | "dark-300-75": "rgba(29, 31, 33, 0.75)",
49 | "dark": "#a5a8a7",
50 | "light-100": "#e7e7e5",
51 | "light-100-50": "rgba(231, 231, 229, 0.5)",
52 | "light-200": "#dbdcdd",
53 | "light-300": "#fafbfd",
54 | "light": "#707070"
55 | },
56 | spacing: {
57 | '116': '29rem',
58 | '160': '40rem'
59 | }
60 | },
61 | truncate: {
62 | lines: {
63 | 2: '2',
64 | 3: '3'
65 | }
66 | }
67 | },
68 | variants: {
69 | display: ({ after }) => after(['group-hover']),
70 | },
71 | plugins: [
72 | require('@tailwindcss/custom-forms'),
73 | require('tailwindcss-truncate-multiline')(),
74 | ],
75 | future: {
76 | removeDeprecatedGapUtilities: true,
77 | },
78 | dark: 'class',
79 | experimental: {
80 | extendedSpacingScale: true,
81 | applyComplexClasses: true,
82 | darkModeVariant: true,
83 | purgeLayersByDefault: true
84 | },
85 | }
86 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 |
4 | "include": ["src/**/*"],
5 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"],
6 | }
--------------------------------------------------------------------------------