├── .gitignore
├── src
├── main.js
├── utils
│ ├── string.js
│ ├── date.js
│ ├── download.js
│ └── data.js
├── stores
│ ├── modalStore.js
│ ├── pageStore.js
│ ├── projectStore.js
│ └── issueStore.js
├── App.svelte
├── components
│ ├── Footer.svelte
│ ├── Projects.svelte
│ ├── Issue.svelte
│ ├── CreateProject.svelte
│ ├── EditProject.svelte
│ ├── Project.svelte
│ ├── Navbar.svelte
│ ├── MobileNav.svelte
│ ├── Issues.svelte
│ ├── EditIssue.svelte
│ └── CreateIssue.svelte
└── shared
│ ├── Button.svelte
│ ├── Card.svelte
│ ├── Input.svelte
│ ├── Modal.svelte
│ └── Icon.svelte
├── README.md
├── package.json
├── public
├── index.html
├── global.css
└── vendors
│ └── bootstrap5
│ └── css
│ └── bootstrap-grid.min.css
├── rollup.config.js
├── scripts
└── setupTypeScript.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /public/build/
3 |
4 | .DS_Store
5 | TODO
6 |
7 | dummy/
8 | test.js
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte';
2 |
3 | const app = new App({
4 | target: document.body
5 | });
6 |
7 | export default app;
--------------------------------------------------------------------------------
/src/utils/string.js:
--------------------------------------------------------------------------------
1 | const capitalizeFirstLetter = (string = "") => {
2 | return string[0].toUpperCase() + string.substring(1);
3 | }
4 |
5 | export { capitalizeFirstLetter };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bugspy
2 | 📖 Bugspy is an web application that helps you to save plans, bugs and issues in your project for you to remember later.
3 |
4 | https://bugspy.netlify.app/
--------------------------------------------------------------------------------
/src/stores/modalStore.js:
--------------------------------------------------------------------------------
1 | import { writable } from "svelte/store";
2 |
3 | const modalCreateStore = writable(false);
4 | const modalEditStore = writable(false);
5 |
6 | export { modalCreateStore, modalEditStore };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-app",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "rollup -c",
7 | "dev": "rollup -c -w",
8 | "start": "sirv public --no-clear"
9 | },
10 | "devDependencies": {
11 | "@rollup/plugin-commonjs": "^17.0.0",
12 | "@rollup/plugin-node-resolve": "^11.0.0",
13 | "rollup": "^2.3.4",
14 | "rollup-plugin-css-only": "^3.1.0",
15 | "rollup-plugin-livereload": "^2.0.0",
16 | "rollup-plugin-svelte": "^7.0.0",
17 | "rollup-plugin-terser": "^7.0.0",
18 | "svelte": "^3.0.0"
19 | },
20 | "dependencies": {
21 | "sirv-cli": "^1.0.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/stores/pageStore.js:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store';
2 |
3 | const PROJECTS_PAGE = "projects";
4 | const ISSUES_PAGE = "issues";
5 | let ISSUE_PROJECT_ID = null;
6 | let ISSUE_ID_EDIT = null;
7 | let PROJECT_ID_EDIT = null;
8 |
9 | const changeIssueProjectId = (id) => {
10 | ISSUE_PROJECT_ID = id;
11 | }
12 |
13 | const changeProjectIdEdit = (id) => {
14 | PROJECT_ID_EDIT = id;
15 | }
16 |
17 | const changeIssueIdEdit = (id) => {
18 | ISSUE_ID_EDIT = id;
19 | }
20 |
21 | const pageStore = writable(PROJECTS_PAGE);
22 |
23 | export default pageStore;
24 | export { PROJECTS_PAGE, ISSUES_PAGE, changeIssueProjectId, ISSUE_PROJECT_ID, ISSUE_ID_EDIT, changeIssueIdEdit, changeProjectIdEdit, PROJECT_ID_EDIT };
--------------------------------------------------------------------------------
/src/App.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | {#if $pageStore == PROJECTS_PAGE}
16 |
17 |
18 |
19 | {:else if $pageStore == ISSUES_PAGE}
20 |
21 |
22 |
23 | {/if}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Bugspy
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/utils/date.js:
--------------------------------------------------------------------------------
1 | const humanizeDate = (date) => {
2 | const delta = Math.round((+new Date - date) / 1000);
3 | const minute = 60;
4 | const hour = minute * 60;
5 | const day = hour * 24;
6 | const week = day * 7;
7 |
8 | switch (true) {
9 | case delta < 30:
10 | return 'Just then'
11 | case delta < minute:
12 | return delta + ' seconds ago';
13 | case delta < 2 * minute:
14 | return 'A minute ago'
15 | case delta < hour:
16 | return Math.floor(delta / minute) + ' minutes ago';
17 | case delta < Math.floor(delta / hour) == 1:
18 | return '1 hour ago'
19 | case delta < day:
20 | return Math.floor(delta / hour) + ' hours ago'
21 | case delta < day * 2:
22 | return 'Yasterday';
23 | default:
24 | return new Date(date).toLocaleString();
25 | }
26 | }
27 |
28 | export { humanizeDate };
--------------------------------------------------------------------------------
/src/utils/download.js:
--------------------------------------------------------------------------------
1 | import { getCurrentIssues } from "../stores/issueStore";
2 | import { ISSUE_PROJECT_ID } from "../stores/pageStore";
3 |
4 | const downloadAsTodoFile = () => {
5 | if (confirm("Are you sure to download the TODO file of this project?")) {
6 | let content = "";
7 | let no = 1;
8 | getCurrentIssues().map((issue) => {
9 | if (issue.projectId == ISSUE_PROJECT_ID) {
10 | content += `${no}. ${issue.solved ? "✅" : "❌"} ${issue.title
11 | } - ${issue.description || "..."}\n`;
12 | no++;
13 | }
14 | });
15 | const a = document.createElement("a");
16 | const file = new Blob([content], { type: "plain/text" });
17 |
18 | a.href = URL.createObjectURL(file);
19 | a.download = "TODO";
20 | a.click();
21 | URL.revokeObjectURL(a.href);
22 | }
23 | };
24 |
25 | export {
26 | downloadAsTodoFile
27 | }
--------------------------------------------------------------------------------
/src/utils/data.js:
--------------------------------------------------------------------------------
1 | const sort = (data = []) => {
2 | return data.sort((a, b) => b.dateCreated - a.dateCreated);
3 | }
4 |
5 | const getCurrentData = (DBKey = "", keyword = "") => {
6 | try {
7 | let data = localStorage.getItem(DBKey);
8 | // db's key is not exists on localStorage
9 | if (data == null) {
10 | localStorage.setItem(DBKey, "[]");
11 | return [];
12 | }
13 | data = JSON.parse(data);
14 | // search
15 | if (keyword) {
16 | data = data.filter(bug => {
17 | return bug.title.toLowerCase().includes(keyword.toLowerCase());
18 | });
19 | }
20 | return sort(data);
21 | } catch (err) {
22 | localStorage.setItem(DBKey, "[]");
23 | return [];
24 | }
25 | }
26 |
27 | const saveData = (DBKey = "", data = []) => {
28 | localStorage.setItem(DBKey, JSON.stringify(data));
29 | return sort(data);
30 | }
31 |
32 |
33 |
34 | export {
35 | getCurrentData,
36 | saveData
37 | }
--------------------------------------------------------------------------------
/src/components/Footer.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
15 |
16 |
38 |
--------------------------------------------------------------------------------
/src/shared/Button.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
14 |
15 |
46 |
--------------------------------------------------------------------------------
/src/stores/projectStore.js:
--------------------------------------------------------------------------------
1 | import { writable } from "svelte/store";
2 | import { getCurrentData, saveData } from '../utils/data';
3 |
4 | const DBProject = '_projects';
5 |
6 | const getCurrentProjects = (keyword = "") => {
7 | return getCurrentData(DBProject, keyword);
8 | }
9 |
10 | const createProject = (project) => {
11 | let projects = getCurrentProjects();
12 | projects.push(project);
13 | return saveData(DBProject, projects);
14 | }
15 |
16 | const editProject = (updatedProject) => {
17 | let projects = getCurrentProjects()
18 | projects = projects.map((project) => {
19 | if (project.id == updatedProject.id) {
20 | project.title = updatedProject.title;
21 | project.priority = updatedProject.priority;
22 | }
23 | return project;
24 | });
25 | return saveData(DBProject, projects);
26 | }
27 |
28 | const deleteProject = (id) => {
29 | let projects = getCurrentProjects();
30 | projects = projects.filter(project => project.id !== id);
31 | return saveData(DBProject, projects);
32 | }
33 |
34 | const projectStore = writable(getCurrentProjects());
35 |
36 | export default projectStore;
37 |
38 | export {
39 | getCurrentProjects,
40 | createProject,
41 | deleteProject,
42 | DBProject,
43 | editProject
44 | }
--------------------------------------------------------------------------------
/src/shared/Card.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 | {titleCard}
17 |
18 |
19 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
46 |
--------------------------------------------------------------------------------
/src/components/Projects.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
Projects
18 |
19 |
20 |
25 |
26 |
27 |
28 |
29 | {#each $projectStore as project (project.id)}
30 |
38 | {:else}
39 |
40 | Project not created yet.
41 |
42 | {/each}
43 |
44 |
45 |
46 |
47 |
52 |
--------------------------------------------------------------------------------
/src/components/Issue.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 | handleDeleteIssue(id)}
33 | onShowModalEdit={() => {
34 | // issue's id that want to edit
35 | changeIssueIdEdit(id);
36 | // show modal edit
37 | modalEditStore.set(true);
38 | }}
39 | onDblClickCard={() => {
40 | $issueStore = changeSolveIssue(id);
41 | // re-render data projects in project page
42 | $projectStore = getCurrentProjects();
43 | }}
44 | >
45 | {description}
46 | {#if category}
47 |
50 | {/if}
51 | {humanizeDate(dateCreated)}
52 |
53 |
--------------------------------------------------------------------------------
/src/components/CreateProject.svelte:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
61 |
62 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import resolve from '@rollup/plugin-node-resolve';
4 | import livereload from 'rollup-plugin-livereload';
5 | import { terser } from 'rollup-plugin-terser';
6 | import css from 'rollup-plugin-css-only';
7 |
8 | const production = !process.env.ROLLUP_WATCH;
9 |
10 | function serve() {
11 | let server;
12 |
13 | function toExit() {
14 | if (server) server.kill(0);
15 | }
16 |
17 | return {
18 | writeBundle() {
19 | if (server) return;
20 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
21 | stdio: ['ignore', 'inherit', 'inherit'],
22 | shell: true
23 | });
24 |
25 | process.on('SIGTERM', toExit);
26 | process.on('exit', toExit);
27 | }
28 | };
29 | }
30 |
31 | export default {
32 | input: 'src/main.js',
33 | output: {
34 | sourcemap: true,
35 | format: 'iife',
36 | name: 'app',
37 | file: 'public/build/bundle.js'
38 | },
39 | plugins: [
40 | svelte({
41 | compilerOptions: {
42 | // enable run-time checks when not in production
43 | dev: !production
44 | }
45 | }),
46 | // we'll extract any component CSS out into
47 | // a separate file - better for performance
48 | css({ output: 'bundle.css' }),
49 |
50 | // If you have external dependencies installed from
51 | // npm, you'll most likely need these plugins. In
52 | // some cases you'll need additional configuration -
53 | // consult the documentation for details:
54 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
55 | resolve({
56 | browser: true,
57 | dedupe: ['svelte']
58 | }),
59 | commonjs(),
60 |
61 | // In dev mode, call `npm run start` once
62 | // the bundle has been generated
63 | !production && serve(),
64 |
65 | // Watch the `public` directory and refresh the
66 | // browser on changes when not in production
67 | !production && livereload('public'),
68 |
69 | // If we're building for production (npm run build
70 | // instead of npm run dev), minify
71 | production && terser()
72 | ],
73 | watch: {
74 | clearScreen: false
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/src/shared/Input.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 | {#if withLabel}
11 |
15 | {/if}
16 |
17 | {#if type == "text"}
18 |
26 | {/if}
27 |
28 | {#if type == "textarea"}
29 |
38 | {/if}
39 |
40 | {#if type == "select"}
41 |
52 | {/if}
53 |
54 |
82 |
--------------------------------------------------------------------------------
/src/components/EditProject.svelte:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
70 |
71 |
--------------------------------------------------------------------------------
/src/components/Project.svelte:
--------------------------------------------------------------------------------
1 |
42 |
43 | handleDeleteProject(id)}
47 | onShowModalEdit={() => {
48 | changeProjectIdEdit(id);
49 | modalEditStore.set(true);
50 | }}
51 | onClickCard={() => {
52 | changeIssueProjectId(id);
53 | $pageStore = ISSUES_PAGE;
54 | }}
55 | >
56 | {issues} {issues == 1 ? "Issue" : "Issues"}
57 |
58 |
59 | {$tweenedCompletedIssues.toFixed(0)}%
61 |
62 |
63 | {issues != 0 && issues == completedIssues ? "Solved" : ""}
64 |
65 |
66 |
72 |
73 | {humanizeDate(dateCreated)}
74 |
75 |
--------------------------------------------------------------------------------
/src/shared/Modal.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 | {
12 | modalCreateStore.set(false);
13 | modalEditStore.set(false);
14 | }}
15 | >
16 |
33 |
34 |
35 |
100 |
--------------------------------------------------------------------------------
/src/components/Navbar.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
56 |
57 |
74 |
--------------------------------------------------------------------------------
/src/components/MobileNav.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {#if $pageStore == PROJECTS_PAGE}
11 |
20 | {:else if $pageStore == ISSUES_PAGE}
21 |
22 |
23 |
30 |
31 |
32 |
41 |
42 |
43 |
54 |
55 |
56 | {/if}
57 |
58 |
59 |
79 |
--------------------------------------------------------------------------------
/src/stores/issueStore.js:
--------------------------------------------------------------------------------
1 | import { writable } from "svelte/store";
2 | import { getCurrentData, saveData } from '../utils/data';
3 | import { DBProject, getCurrentProjects } from './projectStore';
4 |
5 | const DBIssue = '_issues';
6 |
7 | const getCurrentIssues = (keyword = "") => {
8 | return getCurrentData(DBIssue, keyword);
9 | }
10 |
11 | const createIssue = (issue) => {
12 | let issues = getCurrentIssues();
13 | let projects = getCurrentData(DBProject);
14 | issues.push(issue);
15 | projects = projects.map(project => {
16 | if (issue.projectId == project.id) {
17 | project.issues += 1;
18 | }
19 | return project;
20 | });
21 | saveData(DBProject, projects);
22 | return saveData(DBIssue, issues);
23 | }
24 |
25 | const deleteIssue = (id) => {
26 | let issues = getCurrentIssues();
27 | let issuesWithoutDeleted = issues.filter(issue => issue.id !== id);
28 | let deletedIssue = issues.find(issue => issue.id === id);
29 | let projects = getCurrentProjects();
30 | projects = projects.map(project => {
31 | if (project.id == deletedIssue.projectId) {
32 | if (deletedIssue.solved) {
33 | project.completedIssues -= 1;
34 | }
35 | project.issues -= 1;
36 | }
37 | return project;
38 | })
39 | saveData(DBProject, projects);
40 | return saveData(DBIssue, issuesWithoutDeleted);
41 | }
42 |
43 | const editIssue = (updatedIssue) => {
44 | let issues = getCurrentIssues()
45 | issues = issues.map((issue) => {
46 | if (issue.id == updatedIssue.id) {
47 | issue.title = updatedIssue.title;
48 | issue.priority = updatedIssue.priority;
49 | issue.description = updatedIssue.description;
50 | issue.category = updatedIssue.category;
51 | }
52 | return issue;
53 | });
54 | return saveData(DBIssue, issues);
55 | }
56 |
57 | const changeSolveIssue = (id) => {
58 | let issues = getCurrentIssues();
59 | issues = issues.map(issue => {
60 | if (issue.id === id) {
61 | issue.solved = !issue.solved;
62 | let projects = getCurrentData(DBProject)
63 | projects = projects.map(project => {
64 | if (issue.projectId == project.id) {
65 | if (issue.solved) {
66 | project.completedIssues += 1;
67 | } else {
68 | project.completedIssues -= 1;
69 | }
70 | }
71 | return project;
72 | });
73 | saveData(DBProject, projects);
74 | }
75 | return issue;
76 | });
77 | return saveData(DBIssue, issues);
78 | }
79 |
80 | const issueStore = writable(getCurrentIssues());
81 |
82 | export default issueStore;
83 |
84 | export {
85 | getCurrentIssues,
86 | createIssue,
87 | deleteIssue,
88 | changeSolveIssue,
89 | editIssue
90 | }
--------------------------------------------------------------------------------
/src/shared/Icon.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {#if name == "trash"}
7 |
16 | {:else if name == "edit"}
17 |
26 | {:else if name == "projects"}
27 |
36 | {:else if name == "download"}
37 |
46 | {:else if name == "plus"}
47 |
54 | {:else if name == "github"}
55 |
66 | {/if}
67 |
68 |
--------------------------------------------------------------------------------
/src/components/Issues.svelte:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
35 |
36 |
37 |
Issues
38 |
39 |
40 |
45 |
46 |
47 |
48 |
49 |
50 |
Active Issues ({activeIssues.length})
53 | {#each activeIssues as issue (issue.id)}
54 |
60 |
61 |
62 | {:else}
63 |
64 | Issues not created yet.
65 |
66 | {/each}
67 |
68 |
69 |
70 |
Solved Issues ({solvedIssues.length})
73 | {#each solvedIssues as issue (issue.id)}
74 |
80 |
81 |
82 | {:else}
83 |
84 | No solved issues yet.
85 |
86 | {/each}
87 |
88 |
89 |
90 |
91 |
92 |
97 |
--------------------------------------------------------------------------------
/src/components/EditIssue.svelte:
--------------------------------------------------------------------------------
1 |
60 |
61 |
62 |
102 |
103 |
--------------------------------------------------------------------------------
/src/components/CreateIssue.svelte:
--------------------------------------------------------------------------------
1 |
57 |
58 |
59 |
99 |
100 |
--------------------------------------------------------------------------------
/scripts/setupTypeScript.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /** This script modifies the project to support TS code in .svelte files like:
4 |
5 |
8 |
9 | As well as validating the code for CI.
10 | */
11 |
12 | /** To work on this script:
13 | rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
14 | */
15 |
16 | const fs = require("fs")
17 | const path = require("path")
18 | const { argv } = require("process")
19 |
20 | const projectRoot = argv[2] || path.join(__dirname, "..")
21 |
22 | // Add deps to pkg.json
23 | const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
24 | packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
25 | "svelte-check": "^2.0.0",
26 | "svelte-preprocess": "^4.0.0",
27 | "@rollup/plugin-typescript": "^8.0.0",
28 | "typescript": "^4.0.0",
29 | "tslib": "^2.0.0",
30 | "@tsconfig/svelte": "^2.0.0"
31 | })
32 |
33 | // Add script for checking
34 | packageJSON.scripts = Object.assign(packageJSON.scripts, {
35 | "check": "svelte-check --tsconfig ./tsconfig.json"
36 | })
37 |
38 | // Write the package JSON
39 | fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
40 |
41 | // mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
42 | const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
43 | const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
44 | fs.renameSync(beforeMainJSPath, afterMainTSPath)
45 |
46 | // Switch the app.svelte file to use TS
47 | const appSveltePath = path.join(projectRoot, "src", "App.svelte")
48 | let appFile = fs.readFileSync(appSveltePath, "utf8")
49 | appFile = appFile.replace("