first commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/node_modules/
|
||||
/public/build/
|
||||
/build.env
|
||||
/.idea
|
||||
/.vscode
|
||||
|
||||
.DS_Store
|
||||
Generated
+4368
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@gisle/stufflog2-svelte-ui",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"start": "sirv public",
|
||||
"validate": "svelte-check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||
"@rollup/plugin-alias": "^3.1.1",
|
||||
"@rollup/plugin-commonjs": "^16.0.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||
"@rollup/plugin-replace": "^2.3.4",
|
||||
"@rollup/plugin-typescript": "^6.0.0",
|
||||
"@tsconfig/svelte": "^1.0.0",
|
||||
"@types/node": "^14.14.17",
|
||||
"amazon-cognito-identity-js": "^4.5.6",
|
||||
"aws-amplify": "^3.3.13",
|
||||
"fa-svelte": "^3.1.0",
|
||||
"lodash-es": "^4.17.20",
|
||||
"rollup": "^2.3.4",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-dev": "^1.1.3",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^7.0.0",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"svelte": "^3.0.0",
|
||||
"svelte-calendar": "^2.0.4",
|
||||
"svelte-check": "^1.0.0",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"svelte-routing": "^1.4.2",
|
||||
"svelte-time-picker": "^1.0.6",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"sirv-cli": "^1.0.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,62 @@
|
||||
html, body {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111;
|
||||
color: #CCC;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #FC1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input, button, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
-webkit-padding: 0.4em 0;
|
||||
padding: 0.4em;
|
||||
margin: 0 0 0.5em 0;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
button {
|
||||
color: #333;
|
||||
background-color: #f4f4f4;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
button:not(:disabled):active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
border-color: #666;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
|
||||
<title>Svelte app</title>
|
||||
|
||||
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||
<link rel='stylesheet' href='/global.css'>
|
||||
<link rel='stylesheet' href='/build/bundle.css'>
|
||||
|
||||
<script defer src='/build/bundle.js'></script>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -0,0 +1,99 @@
|
||||
import fs from "fs";
|
||||
|
||||
import svelte from "rollup-plugin-svelte";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import resolve from "@rollup/plugin-node-resolve";
|
||||
import livereload from "rollup-plugin-livereload";
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
import sveltePreprocess from "svelte-preprocess";
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
import css from "rollup-plugin-css-only";
|
||||
import dev from "rollup-plugin-dev";
|
||||
import replace from "@rollup/plugin-replace";
|
||||
import json from "@rollup/plugin-json";
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
const envVariables = fs.readFileSync("build.env", "utf-8")
|
||||
.split("\n")
|
||||
.filter(l => l.length > 0)
|
||||
.map(l => l.trim().split("="))
|
||||
.reduce((p, [key, value]) => ({...p, [key]: value}), {});
|
||||
|
||||
export default {
|
||||
input: "src/main.ts",
|
||||
output: {
|
||||
sourcemap: true,
|
||||
format: "iife",
|
||||
name: "app",
|
||||
file: "public/build/bundle.js"
|
||||
},
|
||||
plugins: [
|
||||
svelte({
|
||||
preprocess: sveltePreprocess(),
|
||||
compilerOptions: {
|
||||
// enable run-time checks when not in production
|
||||
dev: !production
|
||||
}
|
||||
}),
|
||||
// we"ll extract any component CSS out into
|
||||
// a separate file - better for performance
|
||||
css({ output: "bundle.css" }),
|
||||
|
||||
// If you have external dependencies installed from
|
||||
// npm, you'll most likely need these plugins. In
|
||||
// some cases you'll need additional configuration -
|
||||
// consult the documentation for details:
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
preferBuiltins: false,
|
||||
dedupe: ["svelte"],
|
||||
}),
|
||||
json(),
|
||||
commonjs({
|
||||
include: 'node_modules/**',
|
||||
}),
|
||||
typescript({
|
||||
sourceMap: !production,
|
||||
inlineSources: !production
|
||||
}),
|
||||
|
||||
replace({
|
||||
// 2 level deep object should be stringify
|
||||
"process.env": JSON.stringify({
|
||||
NODE_ENV: production ? "production" : "development",
|
||||
...envVariables,
|
||||
}),
|
||||
}),
|
||||
|
||||
// Watch the `public` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload("public"),
|
||||
|
||||
// Add dev server in development.
|
||||
!production && dev({
|
||||
dirs: ["public"],
|
||||
spa: "public/index.html",
|
||||
port: 5000,
|
||||
proxy: {
|
||||
"/api/*": "localhost:8000",
|
||||
},
|
||||
}),
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
production && terser({
|
||||
output: { comments: false },
|
||||
})
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false
|
||||
},
|
||||
onwarn: function(warning) {
|
||||
// Skip certain warnings
|
||||
if ( warning.code === "THIS_IS_UNDEFINED" ) { return; }
|
||||
|
||||
// console.warn everything else
|
||||
console.warn( warning.message );
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { Router, Link, Route } from "svelte-routing";
|
||||
import { onMount } from "svelte";
|
||||
import Menu from "./components/Menu.svelte";
|
||||
import FrontPage from "./pages/FrontPage.svelte";
|
||||
import ProjectPage from "./pages/ProjectPage.svelte";
|
||||
import ModalRoute from "./components/ModalRoute.svelte";
|
||||
import LogAddForm from "./forms/LogAddForm.svelte";
|
||||
import LogsPage from "./pages/LogsPage.svelte";
|
||||
import LogEditForm from "./forms/LogEditForm.svelte";
|
||||
import LogDeleteForm from "./forms/LogDeleteForm.svelte";
|
||||
import TaskAddForm from "./forms/TaskAddForm.svelte";
|
||||
import TaskEditForm from "./forms/TaskEditForm.svelte";
|
||||
import TaskDeleteForm from "./forms/TaskDeleteForm.svelte";
|
||||
import ProjectAddForm from "./forms/ProjectAddForm.svelte";
|
||||
import ProjectEditForm from "./forms/ProjectEditForm.svelte";
|
||||
import ProjectDeleteForm from "./forms/ProjectDeleteForm.svelte";
|
||||
import GroupPage from "./pages/GroupPage.svelte";
|
||||
import ItemAddForm from "./forms/ItemAddForm.svelte";
|
||||
import ItemEditForm from "./forms/ItemEditForm.svelte";
|
||||
import ItemDeleteForm from "./forms/ItemDeleteForm.svelte";
|
||||
import GroupForm from "./forms/GroupForm.svelte";
|
||||
import GoalPage from "./pages/GoalPage.svelte";
|
||||
import GoalForm from "./forms/GoalForm.svelte";
|
||||
import LoginForm from "./forms/LoginForm.svelte";
|
||||
import authStore from "./stores/auth";
|
||||
|
||||
onMount(() => {
|
||||
authStore.check()
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $authStore.checked}
|
||||
{#if $authStore.loggedIn}
|
||||
<Router>
|
||||
<Menu />
|
||||
<main>
|
||||
<Route path="/" component={FrontPage} />
|
||||
<Route path="/goals/" component={GoalPage} />
|
||||
<Route path="/projects/" component={ProjectPage} />
|
||||
<Route path="/logs/" component={LogsPage} />
|
||||
<Route path="/items/" component={GroupPage} />
|
||||
</main>
|
||||
</Router>
|
||||
<ModalRoute name="log.add"> <LogAddForm/> </ModalRoute>
|
||||
<ModalRoute name="log.edit"> <LogEditForm/> </ModalRoute>
|
||||
<ModalRoute name="log.delete"> <LogDeleteForm/> </ModalRoute>
|
||||
<ModalRoute name="task.add"> <TaskAddForm/> </ModalRoute>
|
||||
<ModalRoute name="task.edit"> <TaskEditForm/> </ModalRoute>
|
||||
<ModalRoute name="task.delete"> <TaskDeleteForm/> </ModalRoute>
|
||||
<ModalRoute name="project.add"> <ProjectAddForm/> </ModalRoute>
|
||||
<ModalRoute name="project.edit"> <ProjectEditForm/> </ModalRoute>
|
||||
<ModalRoute name="project.delete"> <ProjectDeleteForm/> </ModalRoute>
|
||||
<ModalRoute name="item.add"> <ItemAddForm/> </ModalRoute>
|
||||
<ModalRoute name="item.edit"> <ItemEditForm/> </ModalRoute>
|
||||
<ModalRoute name="item.delete"> <ItemDeleteForm/> </ModalRoute>
|
||||
<ModalRoute name="group.add"> <GroupForm creation/> </ModalRoute>
|
||||
<ModalRoute name="group.edit"> <GroupForm/> </ModalRoute>
|
||||
<ModalRoute name="group.delete"> <GroupForm deletion/> </ModalRoute>
|
||||
<ModalRoute name="goal.add"> <GoalForm creation/> </ModalRoute>
|
||||
<ModalRoute name="goal.edit"> <GoalForm/> </ModalRoute>
|
||||
<ModalRoute name="goal.delete"> <GoalForm deletion/> </ModalRoute>
|
||||
{:else}
|
||||
<LoginForm />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
main {
|
||||
text-align: left;
|
||||
max-width: 99.5%;
|
||||
width: 920px;
|
||||
margin: 1em auto;
|
||||
padding-bottom: 4em;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
import Amplify from "@aws-amplify/core";
|
||||
import Auth from "@aws-amplify/auth";
|
||||
import type {CognitoAccessToken, CognitoUser} from "amazon-cognito-identity-js";
|
||||
|
||||
Amplify.configure({
|
||||
Auth: {
|
||||
region: process.env.AWS_AMPLIFY_REGION,
|
||||
userPoolId: process.env.AWS_AMPLIFY_USER_POOL_ID,
|
||||
userPoolWebClientId: process.env.AWS_AMPLIFY_USER_POOL_WEB_CLIENT_ID,
|
||||
},
|
||||
});
|
||||
|
||||
export async function signIn(username: string, password: string): Promise<CognitoUser | null> {
|
||||
const u = await Auth.signIn(username, password);
|
||||
return u || null;
|
||||
}
|
||||
|
||||
export async function signOut(): Promise<void> {
|
||||
await Auth.signOut();
|
||||
}
|
||||
|
||||
async function getAccessToken(): Promise<CognitoAccessToken | null> {
|
||||
try {
|
||||
const u = await Auth.currentSession();
|
||||
if (!u || !u.isValid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return u.getAccessToken();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getJwt(): Promise<string> {
|
||||
const token = await getAccessToken();
|
||||
if (!token) {
|
||||
throw new Error("unauthorized");
|
||||
}
|
||||
|
||||
return token.getJwtToken();
|
||||
}
|
||||
|
||||
export async function checkSession(): Promise<boolean> {
|
||||
return !!(await getAccessToken());
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import { getJwt } from "./amplify";
|
||||
import type { GoalFilter, GoalInput, GoalResult, GoalUpdate } from "../models/goal";
|
||||
import type { ProjectFilter, ProjectInput, ProjectResult, ProjectUpdate } from "../models/project";
|
||||
import type { TaskInput, TaskResult, TaskUpdate } from "../models/task";
|
||||
import type { LogFilter, LogInput, LogResult, LogUpdate } from "../models/log";
|
||||
import type { GroupInput, GroupResult, GroupUpdate } from "../models/group";
|
||||
import type { ItemInput, ItemResult, ItemUpdate } from "../models/item";
|
||||
|
||||
export class StufflogClient {
|
||||
private root: string;
|
||||
|
||||
constructor(root: string) {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async findGoal(id: string): Promise<GoalResult> {
|
||||
const data = await this.fetch("GET", `/api/goal/${id}`);
|
||||
return data.goal;
|
||||
}
|
||||
|
||||
async listGoals({minTime, maxTime, includesTime}: GoalFilter): Promise<GoalResult[]> {
|
||||
let queries = [];
|
||||
if (minTime != null) {
|
||||
queries.push(`minTime=${minTime.toISOString()}`);
|
||||
}
|
||||
if (maxTime != null) {
|
||||
queries.push(`maxTime=${maxTime.toISOString()}`);
|
||||
}
|
||||
if (includesTime != null) {
|
||||
queries.push(`includesTime=${includesTime.toISOString()}`);
|
||||
}
|
||||
|
||||
const query = queries.length > 0 ? `?${queries.join("&")}` : "";
|
||||
|
||||
const data = await this.fetch("GET", `/api/goal/${query}`);
|
||||
return data.goals;
|
||||
}
|
||||
|
||||
async createGoal(input: GoalInput): Promise<GoalResult> {
|
||||
const data = await this.fetch("POST", "/api/goal/", input);
|
||||
return data.goal;
|
||||
}
|
||||
|
||||
async updateGoal(id: string, update: GoalUpdate): Promise<GoalResult> {
|
||||
const data = await this.fetch("PUT", `/api/goal/${id}`, update);
|
||||
return data.goal;
|
||||
}
|
||||
|
||||
async deleteGoal(id: string): Promise<GoalResult> {
|
||||
const data = await this.fetch("DELETE", `/api/goal/${id}`);
|
||||
return data.goal;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async findProject(id: string): Promise<ProjectResult> {
|
||||
const data = await this.fetch("GET", `/api/project/${id}`);
|
||||
return data.project;
|
||||
}
|
||||
|
||||
async listProjects({active, expiring}: ProjectFilter): Promise<ProjectResult[]> {
|
||||
let queries = [];
|
||||
if (active != null) {
|
||||
queries.push(`active=${active}`);
|
||||
}
|
||||
if (expiring != null) {
|
||||
queries.push(`expiring=${expiring}`);
|
||||
}
|
||||
|
||||
const query = queries.length > 0 ? `?${queries.join("&")}` : "";
|
||||
|
||||
const data = await this.fetch("GET", `/api/project/${query}`);
|
||||
return data.projects;
|
||||
}
|
||||
|
||||
async createProject(input: ProjectInput): Promise<ProjectResult> {
|
||||
const data = await this.fetch("POST", "/api/project/", input);
|
||||
return data.project;
|
||||
}
|
||||
|
||||
async updateProject(id: string, update: ProjectUpdate): Promise<ProjectResult> {
|
||||
const data = await this.fetch("PUT", `/api/project/${id}`, update);
|
||||
return data.project;
|
||||
}
|
||||
|
||||
async deleteProject(id: string): Promise<ProjectResult> {
|
||||
const data = await this.fetch("DELETE", `/api/project/${id}`);
|
||||
return data.project;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async findLog(id: string): Promise<LogResult> {
|
||||
const data = await this.fetch("GET", `/api/log/${id}`);
|
||||
return data.log;
|
||||
}
|
||||
|
||||
async listLogs({minTime, maxTime}: LogFilter): Promise<LogResult[]> {
|
||||
let queries = [];
|
||||
if (minTime != null) {
|
||||
queries.push(`minTime=${minTime.toISOString()}`);
|
||||
}
|
||||
if (maxTime != null) {
|
||||
queries.push(`maxTime=${maxTime.toISOString()}`);
|
||||
}
|
||||
|
||||
const query = queries.length > 0 ? `?${queries.join("&")}` : "";
|
||||
|
||||
const data = await this.fetch("GET", `/api/log/${query}`);
|
||||
return data.logs;
|
||||
}
|
||||
|
||||
async createLog(input: LogInput): Promise<LogResult> {
|
||||
const data = await this.fetch("POST", "/api/log/", input);
|
||||
return data.log;
|
||||
}
|
||||
|
||||
async updateLog(id: string, update: LogUpdate): Promise<LogResult> {
|
||||
const data = await this.fetch("PUT", `/api/log/${id}`, update);
|
||||
return data.log;
|
||||
}
|
||||
|
||||
async deleteLog(id: string): Promise<LogResult> {
|
||||
const data = await this.fetch("DELETE", `/api/log/${id}`);
|
||||
return data.log;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async findTask(id: string): Promise<TaskResult> {
|
||||
const data = await this.fetch("GET", `/api/task/${id}`);
|
||||
return data.task;
|
||||
}
|
||||
|
||||
async listTasks(active?: boolean): Promise<TaskResult[]> {
|
||||
let query = (active != null) ? `?active=${active}` : "";
|
||||
|
||||
const data = await this.fetch("GET", `/api/task/${query}`);
|
||||
return data.tasks;
|
||||
}
|
||||
|
||||
async createTask(input: TaskInput): Promise<TaskResult> {
|
||||
const data = await this.fetch("POST", "/api/task/", input);
|
||||
return data.task;
|
||||
}
|
||||
|
||||
async updateTask(id: string, update: TaskUpdate): Promise<TaskResult> {
|
||||
const data = await this.fetch("PUT", `/api/task/${id}`, update);
|
||||
return data.task;
|
||||
}
|
||||
|
||||
async deleteTask(id: string): Promise<TaskResult> {
|
||||
const data = await this.fetch("DELETE", `/api/task/${id}`);
|
||||
return data.task;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async findGroup(id: string): Promise<GroupResult> {
|
||||
const data = await this.fetch("GET", `/api/group/${id}`);
|
||||
return data.group;
|
||||
}
|
||||
|
||||
async listGroups(): Promise<GroupResult[]> {
|
||||
const data = await this.fetch("GET", "/api/group/");
|
||||
return data.groups;
|
||||
}
|
||||
|
||||
async createGroup(input: GroupInput): Promise<GroupResult> {
|
||||
const data = await this.fetch("POST", "/api/group/", input);
|
||||
return data.group;
|
||||
}
|
||||
|
||||
async updateGroup(id: string, update: GroupUpdate): Promise<GroupResult> {
|
||||
const data = await this.fetch("PUT", `/api/group/${id}`, update);
|
||||
return data.group;
|
||||
}
|
||||
|
||||
async deleteGroup(id: string): Promise<GroupResult> {
|
||||
const data = await this.fetch("DELETE", `/api/group/${id}`);
|
||||
return data.group;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async findItem(id: string): Promise<ItemResult> {
|
||||
const data = await this.fetch("GET", `/api/item/${id}`);
|
||||
return data.item;
|
||||
}
|
||||
|
||||
async listItems(): Promise<ItemResult[]> {
|
||||
const data = await this.fetch("GET", "/api/item/");
|
||||
return data.projects;
|
||||
}
|
||||
|
||||
async createItem(input: ItemInput): Promise<ItemResult> {
|
||||
const data = await this.fetch("POST", "/api/item/", input);
|
||||
return data.item;
|
||||
}
|
||||
|
||||
async updateItem(id: string, update: ItemUpdate): Promise<ItemResult> {
|
||||
const data = await this.fetch("PUT", `/api/item/${id}`, update);
|
||||
return data.item;
|
||||
}
|
||||
|
||||
async deleteItem(id: string): Promise<ItemResult> {
|
||||
const data = await this.fetch("DELETE", `/api/item/${id}`);
|
||||
return data.item;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async fetch(method: string, path: string, body?: object) {
|
||||
const fullPath = this.root + path;
|
||||
const req: RequestInit = {method, headers: {}}
|
||||
|
||||
if (body != null) {
|
||||
const data = new Blob([JSON.stringify(body)])
|
||||
|
||||
req.headers["Content-Type"] = req;
|
||||
req.headers["Content-Length"] = data.size;
|
||||
req.body = data;
|
||||
}
|
||||
|
||||
console.warn("AUTH SKIPPED, remember to change back in prod!")
|
||||
req.headers["Authorization"] = `Bearer ${await getJwt()}`;
|
||||
|
||||
const res = await fetch(fullPath, req);
|
||||
if (!res.ok) {
|
||||
if ((res.headers.get("Content-Type") || "").includes("application/json")) {
|
||||
const data = await res.json();
|
||||
throw new StuffLogError(data.errorCode, data.errorMessage)
|
||||
} else {
|
||||
const text = await res.text();
|
||||
throw new StuffLogError(res.status, text)
|
||||
}
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
}
|
||||
|
||||
export class StuffLogError {
|
||||
public code: number
|
||||
public message: string
|
||||
|
||||
constructor(code: number, message: string) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Error ${this.code}: ${this.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
const stuffLogClient = new StufflogClient("");
|
||||
export default stuffLogClient
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import modalStore from "../stores/modal";
|
||||
|
||||
export let open: ModalData = {name: "none"};
|
||||
export let disabled: boolean = false;
|
||||
export let compact: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleClick() {
|
||||
dispatch("click", {open});
|
||||
|
||||
if (open.name !== "none") {
|
||||
modalStore.set(open);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="boi" class:disabled class:compact on:click={handleClick}><slot></slot></div>
|
||||
|
||||
<style>
|
||||
div.boi {
|
||||
border: 6px dashed;
|
||||
padding: 0.5em;
|
||||
margin: 1em 0.5ch;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
border-color: #333;
|
||||
cursor: pointer;
|
||||
border-bottom-right-radius: 0.25em;
|
||||
font-size: 2em;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
div.boi:hover {
|
||||
color: #AAA;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
div.boi.disabled {
|
||||
color: #333;
|
||||
border-color: #222;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
div.boi.compact {
|
||||
margin: 0;
|
||||
border-width: 4px;
|
||||
padding: 0.25em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
export let time: Date | string = new Date();
|
||||
|
||||
let timeStr = "";
|
||||
|
||||
function formatTime(time: Date): string {
|
||||
const pad = (n:number) => n < 10 ? '0'+n : n.toString();
|
||||
|
||||
return `${time.getFullYear()}-${pad(time.getMonth()+1)}-${pad(time.getDate())} ${pad(time.getHours())}:${pad(time.getMinutes())}`
|
||||
}
|
||||
|
||||
$: timeStr = formatTime(new Date(time));
|
||||
</script>
|
||||
|
||||
<span>{timeStr}</span>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
export let startTime: Date | string = new Date();
|
||||
export let endTime: Date | string = new Date();
|
||||
|
||||
let started = false;
|
||||
let overdue = false;
|
||||
let danger = false;
|
||||
let amount = 0;
|
||||
let amountStr = "0";
|
||||
let unit = "days";
|
||||
let titleTimeStr = "";
|
||||
|
||||
function formatTime(time: Date): string {
|
||||
const pad = (n:number) => n < 9 ? '0'+n : n.toString();
|
||||
|
||||
return `${time.getFullYear()}-${pad(time.getMonth()+1)}-${pad(time.getDate())}`
|
||||
}
|
||||
|
||||
$: {
|
||||
const now = new Date();
|
||||
|
||||
overdue = false;
|
||||
unit = "days";
|
||||
|
||||
const st = (startTime instanceof Date) ? startTime : new Date(startTime);
|
||||
const et = (endTime instanceof Date) ? endTime : new Date(endTime);
|
||||
|
||||
if (now < st) {
|
||||
started = false;
|
||||
amount = (st.getTime() - now.getTime()) / 86400000
|
||||
} else {
|
||||
started = true;
|
||||
amount = (et.getTime() - now.getTime()) / 86400000
|
||||
}
|
||||
|
||||
if (amount < 0) {
|
||||
overdue = true;
|
||||
amount = -amount;
|
||||
}
|
||||
|
||||
danger = (!overdue && started && amount <= 3);
|
||||
|
||||
if (amount < 2) {
|
||||
amount *= 24;
|
||||
unit = "hours"
|
||||
}
|
||||
if (amount < 2) {
|
||||
amount *= 60;
|
||||
unit = "minutes";
|
||||
}
|
||||
|
||||
amount = Math.floor(amount);
|
||||
if (amount <= 1) {
|
||||
unit = unit.slice(0, -1);
|
||||
}
|
||||
|
||||
if (amount < 1) {
|
||||
amountStr = "< 1"
|
||||
} else {
|
||||
amountStr = amount.toString()
|
||||
}
|
||||
|
||||
titleTimeStr = `${formatTime(new Date(startTime))} – ${formatTime(new Date(endTime))}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<span title={titleTimeStr}>
|
||||
{#if (overdue)}
|
||||
<span class="overdue">{amountStr} {unit} ago</span>
|
||||
{:else if (started)}
|
||||
<span class:danger class="started">{amountStr} {unit} left</span>
|
||||
{:else}
|
||||
<span class="pending">In {amountStr} {unit}</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
span.pending {
|
||||
color: #2797e2;
|
||||
}
|
||||
span.danger {
|
||||
color: #e28127;
|
||||
}
|
||||
span.overdue {
|
||||
color: #666666;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import type { IconName } from "../external/icons";
|
||||
import type { GoalResult } from "../models/goal";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import DaysLeft from "./DaysLeft.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import Option from "./Option.svelte";
|
||||
import OptionRow from "./OptionRow.svelte";
|
||||
import Progress from "./Progress.svelte";
|
||||
|
||||
export let goal: GoalResult = null;
|
||||
export let showAllOptions = false;
|
||||
|
||||
let iconName: IconName = "question";
|
||||
let mdGoalEdit: ModalData;
|
||||
let mdGoalDelete: ModalData;
|
||||
|
||||
$: iconName = goal.group.icon as IconName;
|
||||
$: mdGoalEdit = {name:"goal.edit", goal};
|
||||
$: mdGoalDelete = {name:"goal.delete", goal};
|
||||
</script>
|
||||
|
||||
<div class="goal" class:full={showAllOptions}>
|
||||
<div class="icon"><Icon block name={iconName} /></div>
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
<div class="name">{goal.name}</div>
|
||||
<div class="times">
|
||||
<DaysLeft startTime={goal.startTime} endTime={goal.endTime} />
|
||||
</div>
|
||||
</div>
|
||||
{#if showAllOptions}
|
||||
<div class="description">
|
||||
<p>{goal.description}</p>
|
||||
</div>
|
||||
<OptionRow>
|
||||
<Option open={mdGoalEdit}>Edit</Option>
|
||||
<Option open={mdGoalDelete}>Delete</Option>
|
||||
</OptionRow>
|
||||
{/if}
|
||||
<div class="progress">
|
||||
<Progress count={goal.completedAmount} target={goal.amount} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.goal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
div.goal.full {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
div.icon {
|
||||
font-size: 2em;
|
||||
padding: 0 0.5ch;
|
||||
width: 2ch;
|
||||
padding-top: 0.125em;
|
||||
color: #333;
|
||||
}
|
||||
div.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
div.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
div.name {
|
||||
font-size: 1em;
|
||||
margin: auto 0;
|
||||
vertical-align: middle;
|
||||
font-weight: 100;
|
||||
}
|
||||
div.times {
|
||||
margin-left: auto;
|
||||
margin-right: 0.25ch;
|
||||
}
|
||||
|
||||
div.progress {
|
||||
padding-top: 0.125em;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
div.description > p {
|
||||
padding: 0;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import type { IconName } from "../external/icons";
|
||||
import type { GroupResult } from "../models/group";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import DaysLeft from "./DaysLeft.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import ItemEntry from "./ItemEntry.svelte";
|
||||
import Option from "./Option.svelte";
|
||||
import OptionRow from "./OptionRow.svelte";
|
||||
import TaskEntry from "./TaskEntry.svelte";
|
||||
|
||||
export let group: GroupResult = null;
|
||||
export let showAllOptions: boolean = false;
|
||||
|
||||
let iconName: IconName = "question";
|
||||
let mdItemAdd: ModalData;
|
||||
let mdGroupEdit: ModalData;
|
||||
let mdGroupDelete: ModalData;
|
||||
|
||||
$: iconName = group.icon as IconName;
|
||||
$: mdItemAdd = {name:"item.add", group};
|
||||
$: mdGroupEdit = {name:"group.edit", group};
|
||||
$: mdGroupDelete = {name:"group.delete", group};
|
||||
</script>
|
||||
|
||||
<div class="group">
|
||||
<div class="icon"><Icon block name={iconName} /></div>
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
<div class="name">{group.name}</div>
|
||||
</div>
|
||||
{#if showAllOptions}
|
||||
<div class="description">
|
||||
<p>{group.description}</p>
|
||||
</div>
|
||||
<OptionRow>
|
||||
<Option open={mdItemAdd}>Add Item</Option>
|
||||
<Option open={mdGroupEdit}>Edit</Option>
|
||||
<Option open={mdGroupDelete}>Delete</Option>
|
||||
</OptionRow>
|
||||
{/if}
|
||||
<div class="list" class:full={showAllOptions}>
|
||||
{#each group.items as item (item.id)}
|
||||
<ItemEntry item={item} group={group} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
div.icon {
|
||||
font-size: 2em;
|
||||
padding: 0 0.5ch;
|
||||
width: 2ch;
|
||||
padding-top: 0.125em;
|
||||
color: #333;
|
||||
}
|
||||
div.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
div.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
div.name {
|
||||
font-size: 1em;
|
||||
font-weight: 100;
|
||||
margin: auto 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.description > p {
|
||||
padding: 0;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import groupStore from "../stores/group";
|
||||
|
||||
export let value = "";
|
||||
export let name = "";
|
||||
export let disabled = false;
|
||||
|
||||
$: {
|
||||
if ($groupStore.stale && !$groupStore.loading) {
|
||||
groupStore.load();
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (!disabled && $groupStore.groups.length > 0 && value === "") {
|
||||
const nonEmpty = $groupStore.groups.find(g => g.items.length > 0);
|
||||
if (nonEmpty != null) {
|
||||
value = nonEmpty.id;
|
||||
} else {
|
||||
value = $groupStore.groups[0].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<select disabled={disabled || $groupStore.loading} name={name} bind:value={value}>
|
||||
{#each $groupStore.groups as group (group.id)}
|
||||
<option value={group.id} selected={group.id === value}>{group.name} ({group.items.length} items)</option>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import Icon from "fa-svelte"
|
||||
import icons from "../external/icons";
|
||||
import type { IconName } from "../external/icons";
|
||||
|
||||
export let name: IconName = "question";
|
||||
export let block: boolean = false;
|
||||
</script>
|
||||
|
||||
{#if block}
|
||||
<div>
|
||||
<Icon class="activity-icon" icon={icons[name] || icons.question} />
|
||||
</div>
|
||||
{:else}
|
||||
<Icon class="activity-icon" icon={icons[name] || icons.question} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
div {
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import type { IconName } from "../external/icons";
|
||||
import { iconNames } from "../external/icons";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
export let value: IconName;
|
||||
export let disabled: boolean;
|
||||
</script>
|
||||
|
||||
<div class:disabled class="icon-select">
|
||||
{#each iconNames as iconName (iconName)}
|
||||
<div class="icon-item" class:selected={value===iconName} on:click={() => {if (!disabled) { value = iconName }}}>
|
||||
<Icon name={iconName} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.icon-select {
|
||||
background: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0.05em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
div.icon-select.disabled {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
div.icon-item {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
width: calc(100% / 8);
|
||||
padding: 0.35em 0 0.25em 0;
|
||||
text-align: center;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
div.icon-item:hover {
|
||||
background-color: #292929;
|
||||
}
|
||||
div.icon-item.selected {
|
||||
background-color: rgb(18, 63, 75);
|
||||
}
|
||||
div.icon-item.selected:hover {
|
||||
background-color: rgb(24, 83, 99);
|
||||
}
|
||||
div.icon-select.disabled > div.icon-item {
|
||||
background-color: #444;
|
||||
cursor: default;
|
||||
}
|
||||
div.icon-select.disabled > div.icon-item.selected {
|
||||
background-color: #555;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import type { IconName } from "../external/icons";
|
||||
import type { GroupResult } from "../models/group";
|
||||
import type { default as Item } from "../models/item";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import Option from "./Option.svelte";
|
||||
import OptionRow from "./OptionRow.svelte";
|
||||
|
||||
export let item: Item = null;
|
||||
export let group: GroupResult = null;
|
||||
|
||||
let mdItemEdit: ModalData;
|
||||
let mdItemDelete: ModalData;
|
||||
|
||||
$: mdItemEdit = {name: "item.edit", item, group};
|
||||
$: mdItemDelete = {name: "item.delete", item, group};
|
||||
</script>
|
||||
|
||||
<div class="item">
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
<div class="icon">
|
||||
{item.groupWeight}
|
||||
</div>
|
||||
<div class="name">{item.name}</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>{item.description}</p>
|
||||
|
||||
<OptionRow>
|
||||
<Option open={mdItemEdit}>Edit</Option>
|
||||
<Option open={mdItemDelete}>Delete</Option>
|
||||
</OptionRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0.25em 0 0.75em 0;
|
||||
}
|
||||
div.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
div.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
div.icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1em;
|
||||
padding: 0.125em .5ch;
|
||||
min-width: 2ch;
|
||||
text-align: center;
|
||||
|
||||
margin-right: 0.5em;
|
||||
background: #444;
|
||||
color: #CCC;
|
||||
}
|
||||
|
||||
div.name {
|
||||
font-size: 1em;
|
||||
font-weight: 100;
|
||||
margin: auto 0;
|
||||
vertical-align: middle;
|
||||
padding: 0.125em .5ch;
|
||||
}
|
||||
|
||||
div.description {
|
||||
padding: 0.25em 1ch;
|
||||
background: #222;
|
||||
color: #aaa;
|
||||
border-bottom-right-radius: 0.5em;
|
||||
}
|
||||
div.description p {
|
||||
padding: 0;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import groupStore from "../stores/group";
|
||||
|
||||
export let value = "";
|
||||
export let name = "";
|
||||
|
||||
$: {
|
||||
if ($groupStore.stale && !$groupStore.loading) {
|
||||
groupStore.load();
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($groupStore.groups.length > 0 && value === "") {
|
||||
const nonEmpty = $groupStore.groups.find(g => g.items.length > 0);
|
||||
if (nonEmpty != null) {
|
||||
value = nonEmpty.items[0].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<select name={name} bind:value={value} disabled={$groupStore.loading}>
|
||||
{#each $groupStore.groups as group (group.id)}
|
||||
<optgroup label={group.name}>
|
||||
{#each group.items as item (item.id)}
|
||||
<option value={item.id} selected={item.id === value}>{item.name} ({item.groupWeight})</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
{/each}
|
||||
</select>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import type { IconName } from "../external/icons";
|
||||
import type { LogResult } from "../models/log";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import { formatTime } from "../utils/time";
|
||||
import Icon from "./Icon.svelte";
|
||||
import Option from "./Option.svelte";
|
||||
import OptionRow from "./OptionRow.svelte";
|
||||
|
||||
export let log: LogResult = null;
|
||||
|
||||
let taskIconName: IconName = "question";
|
||||
let mdLogEdit: ModalData;
|
||||
let mdLogDelete: ModalData;
|
||||
|
||||
$: taskIconName = log.task.icon as IconName;
|
||||
$: mdLogEdit = {name: "log.edit", log};
|
||||
$: mdLogDelete = {name: "log.delete", log};
|
||||
</script>
|
||||
|
||||
<div class="log">
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
<div class="icon">
|
||||
<Icon name={taskIconName} />
|
||||
</div>
|
||||
<div class="name">{log.task.name}</div>
|
||||
<div class="times">{formatTime(log.loggedTime)}</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>{log.description}</p>
|
||||
|
||||
<OptionRow>
|
||||
<Option open={mdLogEdit}>Edit Log</Option>
|
||||
<Option open={mdLogDelete}>Delete Log</Option>
|
||||
</OptionRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.log {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
div.icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1em;
|
||||
padding: 0.125em .5ch;
|
||||
padding-top: 0.2em;
|
||||
|
||||
margin-right: 0.5em;
|
||||
background: #444;
|
||||
color: #CCC;
|
||||
}
|
||||
div.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
div.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
div.name {
|
||||
font-size: 1em;
|
||||
font-weight: 100;
|
||||
margin: auto 0;
|
||||
vertical-align: middle;
|
||||
padding: 0.125em .5ch;
|
||||
}
|
||||
div.times {
|
||||
margin-left: auto;
|
||||
margin-right: 0.25ch;
|
||||
}
|
||||
|
||||
div.description {
|
||||
padding: 0.25em 1ch;
|
||||
background: #222;
|
||||
color: #aaa;
|
||||
border-bottom-right-radius: 0.5em;
|
||||
}
|
||||
div.description p {
|
||||
padding: 0;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
div.log {
|
||||
padding: 0.25em 1ch;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { link } from "svelte-routing";
|
||||
|
||||
export let location: string = window.location.pathname.split("?")[0];
|
||||
|
||||
function updateLocation() {
|
||||
setTimeout(() => {
|
||||
location = window.location.pathname.split("?")[0];
|
||||
}, 0);
|
||||
}
|
||||
|
||||
$: selected = {
|
||||
home: location == "/",
|
||||
goals: location.startsWith("/goals"),
|
||||
projects: location.startsWith("/projects"),
|
||||
items: location.startsWith("/items"),
|
||||
logs: location.startsWith("/logs"),
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<a on:click={updateLocation} class:selected={selected.home} use:link href="/">Stufflog</a>
|
||||
<a on:click={updateLocation} class:selected={selected.goals} use:link href="/goals">Goals</a>
|
||||
<a on:click={updateLocation} class:selected={selected.projects} use:link href="/projects">Projects</a>
|
||||
<a on:click={updateLocation} class:selected={selected.items} use:link href="/items">Items</a>
|
||||
<a on:click={updateLocation} class:selected={selected.logs} use:link href="/logs">Logs</a>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 0.25em;
|
||||
color: #555;
|
||||
font-size: 1em;
|
||||
}
|
||||
a.selected {
|
||||
color: #AAA;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,215 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
|
||||
export let title: string = "";
|
||||
export let wide: boolean = false;
|
||||
export let error: string | null = null;
|
||||
export let closable: boolean = false;
|
||||
export let show: boolean = false;
|
||||
|
||||
onMount(() => {
|
||||
const listener = (ev: KeyboardEvent) => {
|
||||
console.log(ev.key);
|
||||
|
||||
if ((ev.ctrlKey || ev.altKey) && (ev.key === "Escape" || ev.key.toLowerCase() === "q")) {
|
||||
dispatch("close");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keyup", listener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keyup", listener);
|
||||
}
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div class="modal-background">
|
||||
<div class="modal" class:wide>
|
||||
<div class="header">
|
||||
<div class="title" class:noclose={!closable}>{title}</div>
|
||||
{#if (closable)}
|
||||
<div class="x">
|
||||
<div class="button" on:click={() => dispatch("close")}>
|
||||
<Icon name="times" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<hr />
|
||||
{#if (error != null)}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
<div class="body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
div.modal-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
div.modal {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: calc(100vw - 4em);
|
||||
max-width: 40ch;
|
||||
max-height: calc(100vh - 4em);
|
||||
overflow: auto;
|
||||
transform: translate(-50%,-50%);
|
||||
padding: 1em;
|
||||
border-radius: 0.2em;
|
||||
|
||||
background: #333;
|
||||
}
|
||||
div.modal.wide {
|
||||
max-width: 60ch;
|
||||
}
|
||||
|
||||
div.modal :global(hr) {
|
||||
border: 0.5px solid gray;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.error {
|
||||
margin: 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
border: 1px solid rgb(204, 65, 65);
|
||||
border-radius: 0.2em;
|
||||
background-color: rgb(133, 39, 39);
|
||||
color: rgb(211, 141, 141);
|
||||
|
||||
animation: fadein 0.5s;
|
||||
}
|
||||
|
||||
div.body {
|
||||
margin: 1em 0.25ch;
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: #CCC;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
div.title.noclose {
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
|
||||
div.x {
|
||||
position: relative;
|
||||
line-height: 1em;
|
||||
top: -1em;
|
||||
text-align: right;
|
||||
}
|
||||
div.x div.button {
|
||||
color: #CCC;
|
||||
display: inline-block;
|
||||
padding: 0em 0.5ch 0.1em 0.5ch;
|
||||
line-height: 1em;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
div.x div.button:hover {
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
div.modal :global(button) {
|
||||
display: inline-block;
|
||||
padding: 0.25em 0.75ch 0.26em 0.75ch;
|
||||
margin: 0.75em 0.25ch 0.25em 0.25ch;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0.2em;
|
||||
color: #CCC;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.modal :global(button:hover), div.modal :global(button:focus) {
|
||||
background: #222;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
div.modal :global(label) {
|
||||
padding: 0 0 0.125em 0.25ch;
|
||||
font-size: 0.75em;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
div.modal :global(input), div.modal :global(select), div.modal :global(textarea) {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
background: #222;
|
||||
color: #777;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
}
|
||||
div.modal :global(select) {
|
||||
padding-left: 0.5ch;
|
||||
}
|
||||
div.modal :global(input:disabled) {
|
||||
background: #444;
|
||||
color: #aaa;
|
||||
}
|
||||
div.modal :global(textarea) {
|
||||
height: 6em;
|
||||
}
|
||||
div.modal :global(textarea:disabled) {
|
||||
background: #444;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
div.modal :global(input:last-of-type) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
div.modal :global(input.nolast) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
div.modal :global(input[type="checkbox"]) {
|
||||
width: initial;
|
||||
display: inline-block;
|
||||
}
|
||||
div.modal :global(input[type="checkbox"] + label) {
|
||||
width: initial;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.modal :global(input:focus), div.modal :global(select:focus), div.modal :global(textarea:focus) {
|
||||
background: #111;
|
||||
color: #CCC;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
div.modal :global(p) {
|
||||
margin: 0.25em 1ch 1em 1ch;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import modalStore from "../stores/modal";
|
||||
|
||||
export let name: ModalData["name"] = "none";
|
||||
</script>
|
||||
|
||||
{#if $modalStore.name === name}
|
||||
<slot></slot>
|
||||
{/if}
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import modalStore from "../stores/modal";
|
||||
|
||||
export let open: ModalData = {name: "none"};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleClick() {
|
||||
dispatch("click", {open});
|
||||
|
||||
if (open.name !== "none") {
|
||||
modalStore.set(open);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div on:click={handleClick} class="option"><slot></slot></div>
|
||||
|
||||
<style>
|
||||
div.option {
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
padding: 0.125em 0.75ch;
|
||||
cursor: pointer;
|
||||
color: #aa8822;
|
||||
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
div.option:hover {
|
||||
color: #FC1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="option-row">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.option-row {
|
||||
margin-left: -0.5ch;
|
||||
margin-right: -0.5ch;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts" context="module">
|
||||
const COLORS = [
|
||||
"none",
|
||||
"bronze",
|
||||
"silver",
|
||||
"gold",
|
||||
"diamond"
|
||||
]
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let target = 1;
|
||||
export let count = 0;
|
||||
|
||||
let offClass = COLORS[0];
|
||||
let onClass = COLORS[1];
|
||||
let ons = 0;
|
||||
let offs = 1;
|
||||
|
||||
$: {
|
||||
let level = Math.floor(count / target);
|
||||
if (level >= COLORS.length - 1) {
|
||||
offs = 0;
|
||||
ons = target;
|
||||
offClass = "gold";
|
||||
onClass = "diamond";
|
||||
} else {
|
||||
if (count > 0 && count == (level * target)) {
|
||||
level -= 1;
|
||||
}
|
||||
|
||||
ons = count - (level * target);
|
||||
offs = target - ons;
|
||||
offClass = COLORS[level];
|
||||
onClass = COLORS[level + 1];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bar">
|
||||
{#each {length: ons} as _}
|
||||
<div class={"on " + onClass}></div>
|
||||
{/each}
|
||||
{#each {length: offs} as _}
|
||||
<div class={"off " + offClass}></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
div.bar > div {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
border: 0.1px solid #000;
|
||||
}
|
||||
|
||||
div.none { background-color: #555555; }
|
||||
div.bronze { background-color: #f4b083; }
|
||||
div.silver { background-color: #d8dce4; }
|
||||
div.gold { background-color: #ffd966; }
|
||||
div.diamond { background-color: #84f5ff; }
|
||||
|
||||
div.on {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
div.off {
|
||||
opacity: 0.33;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import type { IconName } from "../external/icons";
|
||||
import type { ProjectResult } from "../models/project";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import DaysLeft from "./DaysLeft.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import Option from "./Option.svelte";
|
||||
import OptionRow from "./OptionRow.svelte";
|
||||
import TaskEntry from "./TaskEntry.svelte";
|
||||
|
||||
export let project: ProjectResult = null;
|
||||
export let showAllOptions: boolean = false;
|
||||
|
||||
let iconName: IconName = "question";
|
||||
let mdAddTask: ModalData;
|
||||
let mdProjectAdd: ModalData;
|
||||
let mdProjectEdit: ModalData;
|
||||
let mdProjectDelete: ModalData;
|
||||
|
||||
$: iconName = project.icon as IconName;
|
||||
$: mdAddTask = {name:"task.add", project};
|
||||
$: mdProjectAdd = {name:"project.add"};
|
||||
$: mdProjectEdit = {name:"project.edit", project};
|
||||
$: mdProjectDelete = {name:"project.delete", project};
|
||||
</script>
|
||||
|
||||
<div class="project">
|
||||
<div class="icon"><Icon block name={iconName} /></div>
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
<div class="name">{project.name}</div>
|
||||
{#if (project.endTime != null)}
|
||||
<div class="times">
|
||||
<DaysLeft startTime={project.createdTime} endTime={project.endTime} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showAllOptions}
|
||||
<div class="description">
|
||||
<p>{project.description}</p>
|
||||
</div>
|
||||
<OptionRow>
|
||||
<Option open={mdAddTask}>Add Task</Option>
|
||||
<Option open={mdProjectEdit}>Edit</Option>
|
||||
<Option open={mdProjectDelete}>Delete</Option>
|
||||
</OptionRow>
|
||||
{/if}
|
||||
<div class="list" class:full={showAllOptions}>
|
||||
{#each project.tasks as task (task.id)}
|
||||
<TaskEntry showAllOptions={showAllOptions} task={task} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.project {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
div.icon {
|
||||
font-size: 2em;
|
||||
padding: 0 0.5ch;
|
||||
width: 2ch;
|
||||
padding-top: 0.125em;
|
||||
color: #333;
|
||||
}
|
||||
div.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
div.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
div.name {
|
||||
font-size: 1em;
|
||||
font-weight: 100;
|
||||
margin: auto 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
div.times {
|
||||
margin-left: auto;
|
||||
margin-right: 0.25ch;
|
||||
}
|
||||
|
||||
div.description > p {
|
||||
padding: 0;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,161 @@
|
||||
<script lang="ts">
|
||||
import type { IconName } from "../external/icons";
|
||||
import type { TaskResult } from "../models/task";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import DateSpan from "./DateSpan.svelte";
|
||||
import DaysLeft from "./DaysLeft.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import Option from "./Option.svelte";
|
||||
import OptionRow from "./OptionRow.svelte";
|
||||
|
||||
export let task: TaskResult = null;
|
||||
export let showAllOptions: boolean = false;
|
||||
|
||||
let itomIconName: IconName = "question";
|
||||
let showLogs = false;
|
||||
let mdLogAdd: ModalData;
|
||||
let mdTaskEdit: ModalData;
|
||||
let mdTaskDelete: ModalData;
|
||||
|
||||
function toggleShowLogs() {
|
||||
showLogs = !showLogs;
|
||||
}
|
||||
|
||||
$: itomIconName = task.item.icon as IconName;
|
||||
$: mdLogAdd = {name: "log.add", task};
|
||||
$: mdTaskEdit = {name: "task.edit", task};
|
||||
$: mdTaskDelete = {name: "task.delete", task};
|
||||
</script>
|
||||
|
||||
<div class="task">
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
{#if !task.active}
|
||||
<div class="icon done">
|
||||
<Icon name="check" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="icon">
|
||||
{task.completedAmount} / {task.itemAmount}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="name">{task.name}</div>
|
||||
{#if (task.endTime != null)}
|
||||
<div class="times">
|
||||
<DaysLeft startTime={task.createdTime} endTime={task.endTime} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>{task.description}</p>
|
||||
<div class="item">
|
||||
<div class="item-icon">
|
||||
<Icon name={itomIconName} />
|
||||
</div>
|
||||
<div class="item-name">{task.item.name} ({task.item.groupWeight})</div>
|
||||
</div>
|
||||
<OptionRow>
|
||||
{#if task.logs.length > 0}
|
||||
<Option on:click={toggleShowLogs}>{showLogs ? "Hide Logs" : "Show Logs"}</Option>
|
||||
{/if}
|
||||
<Option open={mdLogAdd}>Add Log</Option>
|
||||
{#if showAllOptions}
|
||||
<Option open={mdTaskEdit}>Edit</Option>
|
||||
<Option open={mdTaskDelete}>Delete</Option>
|
||||
{/if}
|
||||
</OptionRow>
|
||||
{#if showLogs && task.logs.length > 0}
|
||||
<div class="log-list">
|
||||
{#each task.logs as log (log.id)}
|
||||
<div class="log">
|
||||
<div class="log-time"><DateSpan time={log.loggedTime} /></div>
|
||||
<div class="log-description">{log.description}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.task {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0.25em 0 0.75em 0;
|
||||
}
|
||||
div.icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1em;
|
||||
padding: 0.125em .5ch;
|
||||
|
||||
margin-right: 0.5em;
|
||||
background: #444;
|
||||
color: #CCC;
|
||||
}
|
||||
div.icon.done {
|
||||
padding-top: 0.2em;
|
||||
background: #484;
|
||||
color: #78ff78;
|
||||
}
|
||||
div.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
div.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
div.name {
|
||||
font-size: 1em;
|
||||
font-weight: 100;
|
||||
margin: auto 0;
|
||||
vertical-align: middle;
|
||||
padding: 0.125em .5ch;
|
||||
}
|
||||
div.times {
|
||||
margin-left: auto;
|
||||
margin-right: 0.25ch;
|
||||
padding: 0.125em 0;
|
||||
}
|
||||
|
||||
div.description {
|
||||
padding: 0.25em 1ch;
|
||||
background: #222;
|
||||
color: #aaa;
|
||||
border-bottom-right-radius: 0.5em;
|
||||
}
|
||||
div.description p {
|
||||
padding: 0;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
div.log-list {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
div.log {
|
||||
padding: 0.25em 1ch;
|
||||
}
|
||||
div.log-time {
|
||||
font-size: 0.75em;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
div.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0em;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
div.item div.item-icon {
|
||||
padding: 0.25em 0.5ch 0.25em 0;
|
||||
}
|
||||
div.item div.item-name {
|
||||
padding: 0.125em;
|
||||
}
|
||||
</style>
|
||||
Vendored
+138
@@ -0,0 +1,138 @@
|
||||
import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus";
|
||||
import { faCube } from "@fortawesome/free-solid-svg-icons/faCube";
|
||||
import { faCubes } from "@fortawesome/free-solid-svg-icons/faCubes";
|
||||
import { faBook } from "@fortawesome/free-solid-svg-icons/faBook";
|
||||
import { faBookOpen } from "@fortawesome/free-solid-svg-icons/faBookOpen";
|
||||
import { faBookDead } from "@fortawesome/free-solid-svg-icons/faBookDead";
|
||||
import { faPen } from "@fortawesome/free-solid-svg-icons/faPen";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons/faPencilAlt";
|
||||
import { faDiceD20 } from "@fortawesome/free-solid-svg-icons/faDiceD20";
|
||||
import { faDiceD6 } from "@fortawesome/free-solid-svg-icons/faDiceD6";
|
||||
import { faDungeon } from "@fortawesome/free-solid-svg-icons/faDungeon";
|
||||
import { faGamepad } from "@fortawesome/free-solid-svg-icons/faGamepad";
|
||||
import { faHeadphones } from "@fortawesome/free-solid-svg-icons/faHeadphones";
|
||||
import { faLanguage } from "@fortawesome/free-solid-svg-icons/faLanguage";
|
||||
import { faCode } from "@fortawesome/free-solid-svg-icons/faCode";
|
||||
import { faCodeBranch } from "@fortawesome/free-solid-svg-icons/faCodeBranch";
|
||||
import { faGuitar } from "@fortawesome/free-solid-svg-icons/faGuitar";
|
||||
import { faMusic } from "@fortawesome/free-solid-svg-icons/faMusic";
|
||||
import { faArchive } from "@fortawesome/free-solid-svg-icons/faArchive";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck";
|
||||
import { faDrawPolygon } from "@fortawesome/free-solid-svg-icons/faDrawPolygon";
|
||||
import { faComment } from "@fortawesome/free-solid-svg-icons/faComment";
|
||||
import { faDatabase } from "@fortawesome/free-solid-svg-icons/faDatabase";
|
||||
import { faCog } from "@fortawesome/free-solid-svg-icons/faCog";
|
||||
import { faLink } from "@fortawesome/free-solid-svg-icons/faLink";
|
||||
import { faStar } from "@fortawesome/free-solid-svg-icons/faStar";
|
||||
import { faStarOfLife } from "@fortawesome/free-solid-svg-icons/faStarOfLife";
|
||||
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
|
||||
import { faHdd } from "@fortawesome/free-solid-svg-icons/faHdd";
|
||||
import { faServer } from "@fortawesome/free-solid-svg-icons/faServer";
|
||||
import { faBlender } from "@fortawesome/free-solid-svg-icons/faBlender";
|
||||
import { faCross } from "@fortawesome/free-solid-svg-icons/faCross";
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes";
|
||||
import { faSkullCrossbones } from "@fortawesome/free-solid-svg-icons/faSkullCrossbones";
|
||||
import { faCrosshairs } from "@fortawesome/free-solid-svg-icons/faCrosshairs";
|
||||
import { faLaptop } from "@fortawesome/free-solid-svg-icons/faLaptop";
|
||||
import { faMemory } from "@fortawesome/free-solid-svg-icons/faMemory";
|
||||
import { faKeyboard } from "@fortawesome/free-solid-svg-icons/faKeyboard";
|
||||
import { faCookie } from "@fortawesome/free-solid-svg-icons/faCookie";
|
||||
import { faMicrochip } from "@fortawesome/free-solid-svg-icons/faMicrochip";
|
||||
import { faClipboard } from "@fortawesome/free-solid-svg-icons/faClipboard";
|
||||
import { faPizzaSlice } from "@fortawesome/free-solid-svg-icons/faPizzaSlice";
|
||||
import { faPaperclip } from "@fortawesome/free-solid-svg-icons/faPaperclip";
|
||||
import { faReceipt } from "@fortawesome/free-solid-svg-icons/faReceipt";
|
||||
import { faSuperscript } from "@fortawesome/free-solid-svg-icons/faSuperscript";
|
||||
import { faCouch } from "@fortawesome/free-solid-svg-icons/faCouch";
|
||||
import { faTerminal } from "@fortawesome/free-solid-svg-icons/faTerminal";
|
||||
import { faGift } from "@fortawesome/free-solid-svg-icons/faGift";
|
||||
import { faGifts } from "@fortawesome/free-solid-svg-icons/faGifts";
|
||||
import { faImage } from "@fortawesome/free-solid-svg-icons/faImage";
|
||||
import { faImages } from "@fortawesome/free-solid-svg-icons/faImages";
|
||||
import { faDragon } from "@fortawesome/free-solid-svg-icons/faDragon";
|
||||
import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb";
|
||||
import { faTools } from "@fortawesome/free-solid-svg-icons/faTools";
|
||||
import { faHammer } from "@fortawesome/free-solid-svg-icons/faHammer";
|
||||
import { faScrewdriver } from "@fortawesome/free-solid-svg-icons/faScrewdriver";
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons/faWrench";
|
||||
import { faBug } from "@fortawesome/free-solid-svg-icons/faBug";
|
||||
import { faUtensils } from "@fortawesome/free-solid-svg-icons/faUtensils";
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons/faHome";
|
||||
import { faIgloo } from "@fortawesome/free-solid-svg-icons/faIgloo";
|
||||
import { faWarehouse } from "@fortawesome/free-solid-svg-icons/faWarehouse";
|
||||
import { faToiletPaperSlash } from "@fortawesome/free-solid-svg-icons/faToiletPaperSlash";
|
||||
|
||||
const icons = {
|
||||
"question": faQuestion,
|
||||
"plus": faPlus,
|
||||
"cube": faCube,
|
||||
"cubes": faCubes,
|
||||
"book": faBook,
|
||||
"book_open": faBookOpen,
|
||||
"book_dead": faBookDead,
|
||||
"pen": faPen,
|
||||
"pencil_alt": faPencilAlt,
|
||||
"draw_poligon": faDrawPolygon,
|
||||
"dice_d20": faDiceD20,
|
||||
"dice_d6": faDiceD6,
|
||||
"dungeon": faDungeon,
|
||||
"gamepad": faGamepad,
|
||||
"headphones": faHeadphones,
|
||||
"language": faLanguage,
|
||||
"code": faCode,
|
||||
"code_branch": faCodeBranch,
|
||||
"guitar": faGuitar,
|
||||
"archive": faArchive,
|
||||
"check": faCheck,
|
||||
"music": faMusic,
|
||||
"comment": faComment,
|
||||
"database": faDatabase,
|
||||
"cog": faCog,
|
||||
"link": faLink,
|
||||
"star": faStar,
|
||||
"star_of_life": faStarOfLife,
|
||||
"sun": faSun,
|
||||
"hdd": faHdd,
|
||||
"server": faServer,
|
||||
"blender": faBlender,
|
||||
"cross": faCross,
|
||||
"times": faTimes,
|
||||
"crosshairs": faCrosshairs,
|
||||
"skull_crossbones": faSkullCrossbones,
|
||||
"laptop": faLaptop,
|
||||
"memory": faMemory,
|
||||
"keyboard": faKeyboard,
|
||||
"cookie": faCookie,
|
||||
"microchip": faMicrochip,
|
||||
"clipboard": faClipboard,
|
||||
"pizza_slice": faPizzaSlice,
|
||||
"paperclip": faPaperclip,
|
||||
"receipt": faReceipt,
|
||||
"superscript": faSuperscript,
|
||||
"couch": faCouch,
|
||||
"terminal": faTerminal,
|
||||
"gift": faGift,
|
||||
"gifts": faGifts,
|
||||
"image": faImage,
|
||||
"images": faImages,
|
||||
"dragon": faDragon,
|
||||
"lightbulb": faLightbulb,
|
||||
"tools": faTools,
|
||||
"hammer": faHammer,
|
||||
"screwdriver": faScrewdriver,
|
||||
"wrench": faWrench,
|
||||
"bug": faBug,
|
||||
"utensils": faUtensils,
|
||||
"home": faHome,
|
||||
"igloo": faIgloo,
|
||||
"warehouse": faWarehouse,
|
||||
"toilet_paper_slash": faToiletPaperSlash,
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
export const iconNames = Object.keys(icons).sort() as IconName[];
|
||||
export const DEFAULT_ICON = iconNames[0] as IconName;
|
||||
|
||||
export default icons;
|
||||
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import modalStore from "../stores/modal";
|
||||
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||
import groupStore from "../stores/goal";
|
||||
import IconSelect from "../components/IconSelect.svelte";
|
||||
import { DEFAULT_ICON } from "../external/icons";
|
||||
import type { IconName } from "../external/icons";
|
||||
import type { GoalResult } from "../models/goal";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
import { formatFormTime, nextMonth } from "../utils/time";
|
||||
import GroupSelect from "../components/GroupSelect.svelte";
|
||||
|
||||
export let deletion = false;
|
||||
export let creation = false;
|
||||
|
||||
const md = $modalStore;
|
||||
|
||||
let goal: GoalResult = {
|
||||
id: "",
|
||||
groupId: "",
|
||||
startTime: nextMonth(new Date()).toISOString(),
|
||||
endTime: new Date(nextMonth(nextMonth(new Date())).getTime() - 1).toISOString(),
|
||||
amount: 1,
|
||||
name: "",
|
||||
description: "",
|
||||
completedAmount: 0,
|
||||
group: {id: "", name: "", icon: "question", description: ""},
|
||||
items: [],
|
||||
logs: [],
|
||||
};
|
||||
let verb = "Add";
|
||||
if (md.name === "goal.edit" || md.name === "goal.delete") {
|
||||
goal = md.goal;
|
||||
verb = (md.name === "goal.edit") ? "Edit" : "Delete";
|
||||
} else if (md.name !== "goal.add") {
|
||||
throw new Error("Wrong form")
|
||||
}
|
||||
|
||||
let name = goal.name;
|
||||
let description = goal.description;
|
||||
let groupId = goal.groupId;
|
||||
let amount = goal.amount;
|
||||
let startTime = formatFormTime(goal.startTime);
|
||||
let endTime = formatFormTime(goal.endTime);
|
||||
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
if (creation) {
|
||||
stuffLogClient.createGoal({
|
||||
startTime: new Date(startTime),
|
||||
endTime: new Date(endTime),
|
||||
groupId, name, description, amount,
|
||||
}).then(() => {
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
} else if (deletion) {
|
||||
stuffLogClient.deleteGoal(goal.id).then(() => {
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
} else {
|
||||
stuffLogClient.updateGoal(goal.id, {
|
||||
startTime: new Date(startTime),
|
||||
endTime: new Date(endTime),
|
||||
name, description, amount,
|
||||
}).then(() => {
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
}
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="{verb} Goal" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="name">Name</label>
|
||||
<input disabled={deletion} name="name" type="text" bind:value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea disabled={deletion} name="description" bind:value={description} />
|
||||
<label for="groupId">Group</label>
|
||||
<GroupSelect disabled={!creation} name="groupId" bind:value={groupId}/>
|
||||
<label for="amount">Amount</label>
|
||||
<input disabled={deletion} name="amount" type="number" bind:value={amount} />
|
||||
<label for="startTime">Start Time</label>
|
||||
<input disabled={deletion} name="startTime" type="datetime-local" bind:value={startTime} />
|
||||
<label for="endTime">End Time</label>
|
||||
<input disabled={deletion} name="endTime" type="datetime-local" bind:value={endTime} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">{verb} Goal</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import modalStore from "../stores/modal";
|
||||
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||
import groupStore from "../stores/group";
|
||||
import IconSelect from "../components/IconSelect.svelte";
|
||||
import { DEFAULT_ICON } from "../external/icons";
|
||||
import type { IconName } from "../external/icons";
|
||||
import type { GroupResult } from "../models/group";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
|
||||
export let deletion = false;
|
||||
export let creation = false;
|
||||
|
||||
const md = $modalStore;
|
||||
let group: GroupResult = {
|
||||
id: "",
|
||||
name: "",
|
||||
description: "",
|
||||
icon: DEFAULT_ICON,
|
||||
items: [],
|
||||
};
|
||||
let verb = "Add";
|
||||
if (md.name === "group.edit" || md.name === "group.delete") {
|
||||
group = md.group;
|
||||
verb = (md.name === "group.edit") ? "Edit" : "Delete";
|
||||
} else if (md.name !== "group.add") {
|
||||
throw new Error("Wrong form")
|
||||
}
|
||||
|
||||
let name = group.name;
|
||||
let description = group.description;
|
||||
let icon = group.icon as IconName;
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
if (creation) {
|
||||
stuffLogClient.createGroup({
|
||||
name, description, icon,
|
||||
}).then(() => {
|
||||
groupStore.markStale();
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
} else if (deletion) {
|
||||
stuffLogClient.deleteGroup(group.id).then(() => {
|
||||
groupStore.markStale();
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
} else {
|
||||
stuffLogClient.updateGroup(group.id, {
|
||||
name, description, icon,
|
||||
}).then(() => {
|
||||
groupStore.markStale();
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
projectStore.markStale();
|
||||
fpProjectStore.markStale();
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
}
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="{verb} Group" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="name">Name</label>
|
||||
<input disabled={deletion} name="name" type="text" bind:value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea disabled={deletion} name="description" bind:value={description} />
|
||||
<label for="icon">Icon</label>
|
||||
<IconSelect disabled={deletion} bind:value={icon} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">{verb} Group</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import modalStore from "../stores/modal";
|
||||
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||
import groupStore from "../stores/group";
|
||||
|
||||
const md = $modalStore;
|
||||
if (md.name !== "item.add") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
let group = md.group;
|
||||
let name = "";
|
||||
let description = "";
|
||||
let groupWeight = 1;
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
stuffLogClient.createItem({
|
||||
groupId: group.id,
|
||||
|
||||
name, description, groupWeight,
|
||||
}).then(() => {
|
||||
groupStore.markStale();
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
modalStore.close();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Add Item" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="groupName">Group</label>
|
||||
<input disabled name="groupName" type="text" value={group.name} />
|
||||
<label for="name">Name</label>
|
||||
<input name="name" type="text" bind:value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={description} />
|
||||
<label for="groupWeight">Group Weight</label>
|
||||
<input name="groupWeight" type="number" bind:value={groupWeight} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Add Item</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import modalStore from "../stores/modal";
|
||||
import groupStore from "../stores/group";
|
||||
|
||||
const md = $modalStore;
|
||||
if (md.name !== "item.delete") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
let item = md.item;
|
||||
let group = md.group;
|
||||
let name = item.name;
|
||||
let description = item.description;
|
||||
let groupWeight = item.groupWeight;
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
stuffLogClient.deleteItem(item.id).then(() => {
|
||||
groupStore.markStale();
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Edit Item" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="groupName">Group</label>
|
||||
<input disabled name="groupName" type="text" value={group.name} />
|
||||
<label for="name">Name</label>
|
||||
<input disabled name="name" type="text" value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea disabled name="description" value={description} />
|
||||
<label for="groupWeight">Group Weight</label>
|
||||
<input disabled name="groupWeight" type="number" value={groupWeight} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Delete Item</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import modalStore from "../stores/modal";
|
||||
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||
import groupStore from "../stores/group";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
|
||||
const md = $modalStore;
|
||||
if (md.name !== "item.edit") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
let item = md.item;
|
||||
let group = md.group;
|
||||
let name = item.name;
|
||||
let description = item.description;
|
||||
let groupWeight = item.groupWeight;
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
stuffLogClient.updateItem(item.id, {
|
||||
name, description, groupWeight,
|
||||
}).then(() => {
|
||||
groupStore.markStale();
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
projectStore.markStale();
|
||||
fpProjectStore.markStale();
|
||||
modalStore.close();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Edit Item" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="groupName">Group</label>
|
||||
<input disabled name="groupName" type="text" value={group.name} />
|
||||
<label for="name">Name</label>
|
||||
<input name="name" type="text" bind:value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={description} />
|
||||
<label for="groupWeight">Group Weight</label>
|
||||
<input name="groupWeight" type="number" bind:value={groupWeight} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Edit Item</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||
import logStore from "../stores/logs";
|
||||
import modalStore from "../stores/modal";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
import { formatFormTime } from "../utils/time";
|
||||
|
||||
let loggedTime = formatFormTime(new Date);
|
||||
let taskName = "";
|
||||
let description = "";
|
||||
let markInactive = false;
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
const md = $modalStore;
|
||||
if (md.name !== "log.add") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
stuffLogClient.createLog({
|
||||
taskId: md.task.id,
|
||||
loggedTime: new Date(loggedTime).toISOString(),
|
||||
description,
|
||||
}).then(() => {
|
||||
if (markInactive) {
|
||||
return stuffLogClient.updateTask(md.task.id, {active: false})
|
||||
}
|
||||
}).then(() => {
|
||||
modalStore.close();
|
||||
}).finally(() => {
|
||||
projectStore.markStale();
|
||||
fpProjectStore.markStale();
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
logStore.markStale();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
|
||||
$: {
|
||||
const md = $modalStore;
|
||||
if (md.name === "log.add") {
|
||||
taskName = md.task.name;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Add Log" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="taskName">Task</label>
|
||||
<input disabled name="taskName" type="text" bind:value={taskName} />
|
||||
|
||||
<label for="loggedTime">Logged Time</label>
|
||||
<input name="loggedTime" type="datetime-local" bind:value={loggedTime} />
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={description} />
|
||||
<input id="markInactive" type="checkbox" bind:checked={markInactive} />
|
||||
<label for="markInactive">Complete Task</label>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Add Log</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import modalStore from "../stores/modal";
|
||||
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
import { formatFormTime } from "../utils/time";
|
||||
import logStore from "../stores/logs";
|
||||
|
||||
const md = $modalStore;
|
||||
if (md.name !== "log.delete") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
let loggedTime = formatFormTime(new Date(md.log.loggedTime));
|
||||
let description = md.log.description;
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
const md = $modalStore;
|
||||
if (md.name !== "log.delete") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
stuffLogClient.deleteLog(md.log.id).then(() => {
|
||||
projectStore.markStale();
|
||||
fpProjectStore.markStale();
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
logStore.markStale();
|
||||
|
||||
modalStore.close();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Delete Log" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="taskName">Task</label>
|
||||
<input disabled name="taskName" type="text" bind:value={md.log.task.name} />
|
||||
<label for="loggedTime">Logged Time</label>
|
||||
<input disabled name="loggedTime" type="datetime-local" bind:value={loggedTime} />
|
||||
<label for="description">Description</label>
|
||||
<textarea disabled name="description" bind:value={description} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Delete Log</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import modalStore from "../stores/modal";
|
||||
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
import { formatFormTime } from "../utils/time";
|
||||
import logStore from "../stores/logs";
|
||||
|
||||
const md = $modalStore;
|
||||
if (md.name !== "log.edit") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
let loggedTime = formatFormTime(new Date(md.log.loggedTime));
|
||||
let description = md.log.description;
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
const md = $modalStore;
|
||||
if (md.name !== "log.edit") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
stuffLogClient.updateLog(md.log.id, {
|
||||
loggedTime: new Date(loggedTime).toISOString(),
|
||||
description,
|
||||
}).then(() => {
|
||||
projectStore.markStale();
|
||||
fpProjectStore.markStale();
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
logStore.markStale();
|
||||
|
||||
modalStore.close();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Edit Log" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="taskName">Task</label>
|
||||
<input disabled name="taskName" type="text" bind:value={md.log.task.name} />
|
||||
<label for="loggedTime">Logged Time</label>
|
||||
<input name="loggedTime" type="datetime-local" bind:value={loggedTime} />
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={description} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Edit Log</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import type {CognitoUser} from "amazon-cognito-identity-js";
|
||||
|
||||
import { signIn } from "../clients/amplify";
|
||||
import authStore from "../stores/auth";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
|
||||
let user: CognitoUser | null = null;
|
||||
let username = "";
|
||||
let password = "";
|
||||
let newPassword = "";
|
||||
let newPasswordRepeat = "";
|
||||
let settingNewPassword = false;
|
||||
let error = null;
|
||||
let done = false;
|
||||
|
||||
function login() {
|
||||
error = null;
|
||||
|
||||
if (settingNewPassword) {
|
||||
if (newPasswordRepeat !== newPassword) {
|
||||
error = "New passwords do not match.";
|
||||
return;
|
||||
}
|
||||
|
||||
user.completeNewPasswordChallenge(newPassword, null, {
|
||||
onSuccess: () => {
|
||||
done = true;
|
||||
authStore.check();
|
||||
},
|
||||
onFailure: err => {
|
||||
error = err
|
||||
},
|
||||
})
|
||||
} else {
|
||||
signIn(username, password).then(newUser => {
|
||||
if (!newUser) {
|
||||
error = "Incorrect username or password."
|
||||
return
|
||||
}
|
||||
|
||||
if ((newUser as any).challengeName === "NEW_PASSWORD_REQUIRED") {
|
||||
error = "Password is expired, please update it."
|
||||
settingNewPassword = true;
|
||||
} else {
|
||||
authStore.check();
|
||||
done = true;
|
||||
}
|
||||
|
||||
user = newUser;
|
||||
}).catch(err => {
|
||||
error = err
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show={!done} title="Login" error={error}>
|
||||
<form on:submit|preventDefault={login}>
|
||||
<label for="username">Username</label>
|
||||
<input name="username" type="text" bind:value={username} />
|
||||
<label for="password">Password</label>
|
||||
<input name="password" type="password" bind:value={password} />
|
||||
{#if settingNewPassword}
|
||||
<label for="newPassword">New Password</label>
|
||||
<input name="newPassword" type="password" bind:value={newPassword} />
|
||||
<label for="newPasswordRepeat">New Password</label>
|
||||
<input name="newPasswordRepeat" type="password" bind:value={newPasswordRepeat} />
|
||||
{/if}
|
||||
|
||||
<hr />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import IconSelect from "../components/IconSelect.svelte";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import { iconNames } from "../external/icons";
|
||||
import modalStore from "../stores/modal";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
|
||||
let endTime = "";
|
||||
let name = "";
|
||||
let description = "";
|
||||
let icon = iconNames[0];
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
stuffLogClient.createProject({
|
||||
active: true,
|
||||
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
||||
|
||||
name, description, icon,
|
||||
}).then(() => {
|
||||
projectStore.markStale();
|
||||
if (endTime !== "") {
|
||||
fpProjectStore.markStale();
|
||||
}
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Add Project" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="name">Name</label>
|
||||
<input name="name" type="text" bind:value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={description} />
|
||||
<label for="itemId">Icon</label>
|
||||
<IconSelect bind:value={icon} />
|
||||
<label for="endTime">Deadline (Optional)</label>
|
||||
<input name="endTime" type="datetime-local" bind:value={endTime} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Add Project</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import IconSelect from "../components/IconSelect.svelte";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import type { IconName } from "../external/icons";
|
||||
import modalStore from "../stores/modal";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
import { formatFormTime } from "../utils/time";
|
||||
|
||||
const md = $modalStore;
|
||||
if (md.name !== "project.delete") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
const project = md.project;
|
||||
|
||||
let endTime = project.endTime ? formatFormTime(project.endTime) : "";
|
||||
let name = project.name;
|
||||
let description = project.description;
|
||||
let icon = project.icon as IconName;
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
stuffLogClient.deleteProject(project.id).then(() => {
|
||||
projectStore.markStale();
|
||||
if (endTime !== "") {
|
||||
fpProjectStore.markStale();
|
||||
}
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Delete Project" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="name">Name</label>
|
||||
<input disabled name="name" type="text" value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea disabled name="description" value={description} />
|
||||
<label for="itemId">Icon</label>
|
||||
<IconSelect disabled value={icon} />
|
||||
<label for="endTime">Deadline (Optional)</label>
|
||||
<input disabled name="endTime" type="datetime-local" value={endTime} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Delete Project</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import IconSelect from "../components/IconSelect.svelte";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import type { IconName } from "../external/icons";
|
||||
import modalStore from "../stores/modal";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
import { formatFormTime } from "../utils/time";
|
||||
|
||||
const md = $modalStore;
|
||||
if (md.name !== "project.edit") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
const project = md.project;
|
||||
|
||||
let endTime = project.endTime ? formatFormTime(project.endTime) : "";
|
||||
let name = project.name;
|
||||
let description = project.description;
|
||||
let icon = project.icon as IconName;
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
stuffLogClient.updateProject(project.id, {
|
||||
active: true,
|
||||
endTime: ( endTime == "" ) ? null : new Date(endTime),
|
||||
clearEndTime: ( endTime == "" ),
|
||||
|
||||
name, description, icon,
|
||||
}).then(() => {
|
||||
projectStore.markStale();
|
||||
fpProjectStore.markStale();
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Edit Project" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="name">Name</label>
|
||||
<input name="name" type="text" bind:value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={description} />
|
||||
<label for="itemId">Icon</label>
|
||||
<IconSelect bind:value={icon} />
|
||||
<label for="endTime">Deadline (Optional)</label>
|
||||
<input name="endTime" type="datetime-local" bind:value={endTime} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Edit Project</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import ItemSelect from "../components/ItemSelect.svelte";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import type { ProjectResult } from "../models/project";
|
||||
import modalStore from "../stores/modal";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
|
||||
let project: ProjectResult
|
||||
let endTime = "";
|
||||
let itemId = "";
|
||||
let name = "";
|
||||
let description = "";
|
||||
let itemAmount = 1;
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
stuffLogClient.createTask({
|
||||
projectId: project.id,
|
||||
itemId: itemId,
|
||||
active: true,
|
||||
endTime: ( endTime == "") ? null : new Date(endTime),
|
||||
|
||||
name, description, itemAmount,
|
||||
}).then(() => {
|
||||
projectStore.markStale();
|
||||
fpProjectStore.markStale();
|
||||
modalStore.close();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
|
||||
$: {
|
||||
const md = $modalStore;
|
||||
if (md.name !== "task.add") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
if (itemId === "") {
|
||||
project = md.project;
|
||||
if (project.tasks.length > 0) {
|
||||
itemId = project.tasks[0].itemId;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Add Task" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="projectName">Project</label>
|
||||
<input disabled name="projectName" type="text" value={project.name} />
|
||||
<label for="name">Name</label>
|
||||
<input name="name" type="text" bind:value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={description} />
|
||||
<label for="itemId">Item {itemId}</label>
|
||||
<ItemSelect name="itemId" bind:value={itemId} />
|
||||
<label for="itemAmount">Amount</label>
|
||||
<input name="itemAmount" type="number" bind:value={itemAmount} />
|
||||
<label for="endTime">Deadline (Optional)</label>
|
||||
<input name="endTime" type="datetime-local" bind:value={endTime} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Add Task</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||
import modalStore from "../stores/modal";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
import { formatFormTime } from "../utils/time";
|
||||
|
||||
const md = $modalStore;
|
||||
if (md.name !== "task.delete") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
let task = md.task
|
||||
let name = task.name;
|
||||
let description = task.description;
|
||||
let itemAmount = task.itemAmount;
|
||||
let active = task.active;
|
||||
let endTime = task.endTime ? formatFormTime(task.endTime) : "";
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
stuffLogClient.deleteTask(task.id).then(() => {
|
||||
projectStore.markStale();
|
||||
fpProjectStore.markStale();
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
modalStore.close();
|
||||
}).catch(err => {
|
||||
error = err.message ? err.message : err.toString();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Delete Task" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="name">Name</label>
|
||||
<input disabled name="name" type="text" value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea disabled name="description" value={description} />
|
||||
<label for="name">Item</label>
|
||||
<input disabled name="name" type="text" value={task.item.name} />
|
||||
<label for="itemAmount">Amount</label>
|
||||
<input disabled name="itemAmount" type="number" value={itemAmount} />
|
||||
<label for="endTime">Deadline (Optional)</label>
|
||||
<input disabled name="endTime" type="datetime-local" value={endTime} />
|
||||
<input id="active" type="checkbox" checked={active} />
|
||||
<label for="active">Task is active/incomplete</label>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Delete1 Task</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import goalStore, { fpGoalStore } from "../stores/goal";
|
||||
import modalStore from "../stores/modal";
|
||||
import projectStore, { fpProjectStore } from "../stores/project";
|
||||
import { formatFormTime } from "../utils/time";
|
||||
|
||||
const md = $modalStore;
|
||||
if (md.name !== "task.edit") {
|
||||
throw new Error("Wrong form");
|
||||
}
|
||||
|
||||
let task = md.task
|
||||
let name = task.name;
|
||||
let description = task.description;
|
||||
let itemAmount = task.itemAmount;
|
||||
let active = task.active;
|
||||
let endTime = task.endTime ? formatFormTime(task.endTime) : "";
|
||||
let error = null;
|
||||
|
||||
function onSubmit() {
|
||||
stuffLogClient.updateTask(task.id, {
|
||||
endTime: (endTime == "") ? null : new Date(endTime),
|
||||
clearEndTime: endTime == "",
|
||||
|
||||
name, description, itemAmount, active,
|
||||
}).then(() => {
|
||||
projectStore.markStale();
|
||||
fpProjectStore.markStale();
|
||||
goalStore.markStale();
|
||||
fpGoalStore.markStale();
|
||||
modalStore.close();
|
||||
})
|
||||
|
||||
error = null;
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
modalStore.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal show title="Add Task" error={error} closable on:close={onClose}>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<label for="name">Name</label>
|
||||
<input name="name" type="text" bind:value={name} />
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" bind:value={description} />
|
||||
<label for="name">Item</label>
|
||||
<input disabled name="name" type="text" value={task.item.name} />
|
||||
<label for="itemAmount">Amount</label>
|
||||
<input name="itemAmount" type="number" bind:value={itemAmount} />
|
||||
<label for="endTime">Deadline (Optional)</label>
|
||||
<input name="endTime" type="datetime-local" bind:value={endTime} />
|
||||
<input id="active" type="checkbox" bind:checked={active} />
|
||||
<label for="active">Task is active/incomplete</label>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Add Task</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -0,0 +1,8 @@
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
props: {}
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,47 @@
|
||||
import type Group from "./group";
|
||||
import type Item from "./item";
|
||||
import type { LogResult } from "./log";
|
||||
|
||||
export default interface Goal {
|
||||
id: string
|
||||
groupId: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
amount: number
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface GoalFilter {
|
||||
minTime?: Date
|
||||
maxTime?: Date
|
||||
includesTime?: Date
|
||||
}
|
||||
|
||||
export interface GoalResult extends Goal {
|
||||
group: Group
|
||||
items: GoalResultItem[]
|
||||
logs: LogResult[]
|
||||
completedAmount: number
|
||||
}
|
||||
|
||||
interface GoalResultItem extends Item {
|
||||
completedAmount: number
|
||||
}
|
||||
|
||||
export interface GoalInput {
|
||||
groupId: string
|
||||
startTime: string | Date
|
||||
endTime: string | Date
|
||||
amount: number
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface GoalUpdate {
|
||||
startTime?: string | Date
|
||||
endTime?: string | Date
|
||||
amount?: number
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type Item from "./item";
|
||||
|
||||
export default interface Group {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface GroupResult extends Group {
|
||||
items: Item[]
|
||||
}
|
||||
|
||||
export interface GroupInput {
|
||||
name: string
|
||||
icon: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface GroupUpdate {
|
||||
name?: string
|
||||
icon?: string
|
||||
description?: string
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type Group from "./group";
|
||||
|
||||
export default interface Item {
|
||||
id: string
|
||||
groupId: string
|
||||
groupWeight: number
|
||||
icon: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface ItemResult extends Item {
|
||||
group: Group
|
||||
}
|
||||
|
||||
export interface ItemInput {
|
||||
groupId: string
|
||||
groupWeight: number
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface ItemUpdate {
|
||||
groupWeight?: number
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type Task from "./task";
|
||||
|
||||
export default interface Log {
|
||||
id: string
|
||||
taskId: string
|
||||
itemId: string
|
||||
loggedTime: string
|
||||
description: string
|
||||
}
|
||||
|
||||
|
||||
export interface LogFilter {
|
||||
minTime?: Date
|
||||
maxTime?: Date
|
||||
}
|
||||
|
||||
export interface LogResult extends Log {
|
||||
task: Task
|
||||
}
|
||||
|
||||
export interface LogInput {
|
||||
taskId: string
|
||||
loggedTime?: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface LogUpdate {
|
||||
loggedTime?: string
|
||||
description?: string
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { TaskResult } from "./task";
|
||||
|
||||
export default interface Project {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
active: string
|
||||
createdTime: string
|
||||
endTime?: string
|
||||
}
|
||||
|
||||
export interface ProjectResult extends Project {
|
||||
tasks: TaskResult[]
|
||||
}
|
||||
|
||||
export interface ProjectFilter {
|
||||
active?: boolean
|
||||
expiring?: boolean
|
||||
}
|
||||
|
||||
export interface ProjectInput {
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
active: boolean
|
||||
endTime?: string | Date
|
||||
}
|
||||
|
||||
export interface ProjectUpdate {
|
||||
name?: string
|
||||
description?: string
|
||||
icon?: string
|
||||
active?: boolean
|
||||
endTime?: string | Date
|
||||
clearEndTime?: boolean
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type Item from "./item";
|
||||
import type Log from "./log";
|
||||
|
||||
export default interface Task {
|
||||
id: string
|
||||
itemId: string
|
||||
projectId: string
|
||||
itemAmount: number
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
active: boolean
|
||||
createdTime: string
|
||||
endTime?: string
|
||||
}
|
||||
|
||||
export interface TaskResult extends Task {
|
||||
item: Item
|
||||
logs: Log[]
|
||||
completedAmount: number
|
||||
}
|
||||
|
||||
export interface TaskInput {
|
||||
itemId: string
|
||||
projectId: string
|
||||
itemAmount: number
|
||||
name: string
|
||||
description: string
|
||||
active: boolean
|
||||
endTime?: string | Date
|
||||
}
|
||||
|
||||
export interface TaskUpdate {
|
||||
itemAmount?: number
|
||||
name?: string
|
||||
description?: string
|
||||
active?: boolean
|
||||
endTime?: string | Date
|
||||
clearEndTime?: boolean
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<script>
|
||||
import GoalEntry from "../components/GoalEntry.svelte";
|
||||
import ProjectEntry from "../components/ProjectEntry.svelte";
|
||||
import { fpGoalStore } from "../stores/goal";
|
||||
import { fpProjectStore } from "../stores/project";
|
||||
|
||||
$: {
|
||||
if ($fpGoalStore.stale && !$fpGoalStore.loading) {
|
||||
fpGoalStore.load({
|
||||
maxTime: new Date(Date.now() + (86400000 * 30)),
|
||||
minTime: new Date(Date.now() - (86400000 * 3)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($fpProjectStore.stale && !$fpProjectStore.loading) {
|
||||
fpProjectStore.load({
|
||||
active: true,
|
||||
expiring: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<div class="left">
|
||||
{#if !$fpGoalStore.loading || $fpGoalStore.goals.length > 0}
|
||||
<h1>Active Goals</h1>
|
||||
{/if}
|
||||
{#each $fpGoalStore.goals as goal (goal.id)}
|
||||
<GoalEntry goal={goal} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="right">
|
||||
{#if !$fpProjectStore.loading || $fpProjectStore.projects.length > 0}
|
||||
<h1>Upcoming Deadlines</h1>
|
||||
{/if}
|
||||
{#each $fpProjectStore.projects as project (project.id)}
|
||||
<ProjectEntry project={project} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.page {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
padding: 0 1ch;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
div.left, div.right {
|
||||
width: 50%;
|
||||
padding: 0 1ch;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
div.page {
|
||||
display: block;
|
||||
}
|
||||
div.left, div.right {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 2em;
|
||||
max-width: 100%;
|
||||
width: 640px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import Boi from "../components/Boi.svelte";
|
||||
import GoalEntry from "../components/GoalEntry.svelte";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import goalStore from "../stores/goal";
|
||||
|
||||
const mdGoalAdd: ModalData = {name: "goal.add"};
|
||||
|
||||
let minTime = new Date(Date.now() - 366 * 86400000);
|
||||
|
||||
$: {
|
||||
if ($goalStore.stale && !$goalStore.loading) {
|
||||
goalStore.load({minTime});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
{#each $goalStore.goals as goal (goal.id)}
|
||||
<GoalEntry showAllOptions goal={goal} />
|
||||
{/each}
|
||||
<Boi open={mdGoalAdd}>Add Goal</Boi>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.page {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
width: 640px;
|
||||
margin-top: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import Boi from "../components/Boi.svelte";
|
||||
import GroupEntry from "../components/GroupEntry.svelte";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import groupStore from "../stores/group";
|
||||
|
||||
const mdGroupAdd: ModalData = {name: "group.add"};
|
||||
|
||||
$: {
|
||||
if ($groupStore.stale && !$groupStore.loading) {
|
||||
groupStore.load();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
{#each $groupStore.groups as group (group.id)}
|
||||
<GroupEntry showAllOptions group={group} />
|
||||
{/each}
|
||||
<Boi open={mdGroupAdd}>Add Group</Boi>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.page {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
width: 640px;
|
||||
margin-top: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import LogEntry from "../components/LogEntry.svelte";
|
||||
import type { LogResult } from "../models/log";
|
||||
import logStore from "../stores/logs";
|
||||
import { formatDate, formatTime, formatWeekdayDate } from "../utils/time";
|
||||
import Boi from "../components/Boi.svelte";
|
||||
|
||||
let groupedLogs: {day: number, text: string, logs: LogResult[]}[] = [];
|
||||
let earliestDate = $logStore.filter.minTime || new Date(Date.now() - (86400000*30));
|
||||
|
||||
function loadMore() {
|
||||
if (!$logStore.stale && !$logStore.loading) {
|
||||
earliestDate = new Date(earliestDate.getTime() - (86400000*30));
|
||||
logStore.markStale();
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($logStore.stale && !$logStore.loading) {
|
||||
logStore.load({
|
||||
minTime: earliestDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (!$logStore.loading) {
|
||||
groupedLogs = [];
|
||||
|
||||
if ($logStore.logs.length > 0) {
|
||||
const now = new Date();
|
||||
const firstUtc = Math.floor(Date.parse($logStore.logs[0].loggedTime) / 86400000) * 86400000;
|
||||
const todayUtc = Math.floor(now.getTime() / 86400000) * 86400000;
|
||||
const first = firstUtc + (now.getTimezoneOffset() * 60000);
|
||||
const today = todayUtc + (now.getTimezoneOffset() * 60000);
|
||||
const yesterday = today - 86400000;
|
||||
const tomorrow = today + 86400000;
|
||||
|
||||
let currentDay = first;
|
||||
let remainingLogs = $logStore.logs;
|
||||
|
||||
while (remainingLogs.length > 0) {
|
||||
const currentLogs: LogResult[] = [];
|
||||
for (const log of remainingLogs) {
|
||||
if (Date.parse(log.loggedTime) >= currentDay) {
|
||||
currentLogs.push(log);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLogs.length > 0) {
|
||||
remainingLogs = remainingLogs.slice(currentLogs.length);
|
||||
|
||||
let text = formatWeekdayDate(currentDay);
|
||||
if (currentDay === tomorrow) {
|
||||
text = "Tomorrow";
|
||||
} else if (currentDay === today) {
|
||||
text = "Today";
|
||||
} else if (currentDay === yesterday) {
|
||||
text = "Yesterday";
|
||||
}
|
||||
|
||||
groupedLogs.push({day: currentDay, text, logs: currentLogs});
|
||||
}
|
||||
|
||||
currentDay -= 86400000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
{#each groupedLogs as logGroup (logGroup.day)}
|
||||
<h2>{logGroup.text}</h2>
|
||||
{#each logGroup.logs as log (log.id)}
|
||||
<LogEntry log={log} />
|
||||
{/each}
|
||||
{/each}
|
||||
{#if !$logStore.loading && !$logStore.stale}
|
||||
<Boi on:click={loadMore}>Load More</Boi>
|
||||
{:else}
|
||||
<Boi disabled>Loading...</Boi>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.page {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
width: 640px;
|
||||
margin-top: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import Boi from "../components/Boi.svelte";
|
||||
import ProjectEntry from "../components/ProjectEntry.svelte";
|
||||
import type { ModalData } from "../stores/modal";
|
||||
import projectStore from "../stores/project";
|
||||
|
||||
const mdProjectAdd: ModalData = {name: "project.add"};
|
||||
|
||||
$: {
|
||||
if ($projectStore.stale && !$projectStore.loading) {
|
||||
projectStore.load({
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
{#each $projectStore.projects as project (project.id)}
|
||||
<ProjectEntry showAllOptions project={project} />
|
||||
{/each}
|
||||
<Boi open={mdProjectAdd}>Add Project</Boi>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.page {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
width: 640px;
|
||||
margin-top: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { checkSession } from "../clients/amplify";
|
||||
|
||||
function createAuthStore() {
|
||||
const {set, subscribe} = writable({checked: false, loggedIn: false})
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
async check() {
|
||||
try {
|
||||
const loggedIn = await checkSession();
|
||||
set({checked: true, loggedIn });
|
||||
} catch(err) {
|
||||
set({checked: true, loggedIn: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const authStore = createAuthStore();
|
||||
|
||||
export default authStore;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { writable } from "svelte/store";
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import type { GoalFilter, GoalResult } from "../models/goal";
|
||||
|
||||
interface GoalStoreData {
|
||||
loading: boolean
|
||||
stale: boolean
|
||||
goals: GoalResult[]
|
||||
}
|
||||
|
||||
function createGoalStore() {
|
||||
const {update, subscribe} = writable<GoalStoreData>({
|
||||
loading: false,
|
||||
stale: true,
|
||||
goals: [],
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
markStale() {
|
||||
update(v => ({...v, stale: true}));
|
||||
},
|
||||
|
||||
async load(filter: GoalFilter) {
|
||||
update(v => ({...v, loading: true, filter}));
|
||||
const goals = await stuffLogClient.listGoals(filter);
|
||||
update(v => ({...v, loading: false, stale: false, goals: goals.reverse()}));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const goalStore = createGoalStore();
|
||||
|
||||
export default goalStore;
|
||||
export const fpGoalStore = createGoalStore();
|
||||
@@ -0,0 +1,35 @@
|
||||
import { writable } from "svelte/store";
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import type { GroupResult } from "../models/group";
|
||||
|
||||
interface GroupStoreData {
|
||||
loading: boolean
|
||||
stale: boolean
|
||||
groups: GroupResult[]
|
||||
}
|
||||
|
||||
function createGroupStore() {
|
||||
const {update, subscribe} = writable<GroupStoreData>({
|
||||
loading: false,
|
||||
stale: true,
|
||||
groups: [],
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
markStale() {
|
||||
update(v => ({...v, stale: true}));
|
||||
},
|
||||
|
||||
async load() {
|
||||
update(v => ({...v, loading: true}));
|
||||
const groups = await stuffLogClient.listGroups();
|
||||
update(v => ({...v, loading: false, stale: false, groups}));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const groupStore = createGroupStore();
|
||||
|
||||
export default groupStore;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { writable } from "svelte/store";
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import type { LogFilter, LogResult } from "../models/log";
|
||||
|
||||
interface ProjectStoreData {
|
||||
loading: boolean
|
||||
stale: boolean
|
||||
logs: LogResult[]
|
||||
filter: LogFilter
|
||||
}
|
||||
|
||||
function createProjectStore() {
|
||||
const {update, subscribe} = writable<ProjectStoreData>({
|
||||
loading: false,
|
||||
stale: true,
|
||||
logs: [],
|
||||
filter: {},
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
markStale() {
|
||||
update(v => ({...v, stale: true}));
|
||||
},
|
||||
|
||||
async load(filter: LogFilter) {
|
||||
update(v => ({...v, loading: true, filter}));
|
||||
const logs = await stuffLogClient.listLogs(filter);
|
||||
update(v => ({...v, loading: false, stale: false, logs: logs.reverse()}));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const logStore = createProjectStore();
|
||||
|
||||
export default logStore;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { writable } from "svelte/store";
|
||||
import type { GoalResult } from "../models/goal";
|
||||
import type { GroupResult } from "../models/group";
|
||||
import type Item from "../models/item";
|
||||
import type { LogResult } from "../models/log";
|
||||
import type { ProjectResult } from "../models/project";
|
||||
import type { TaskResult } from "../models/task";
|
||||
|
||||
export type ModalData =
|
||||
| { name: "none" }
|
||||
| { name: "log.add", task: TaskResult }
|
||||
| { name: "log.edit", log: LogResult }
|
||||
| { name: "log.delete", log: LogResult }
|
||||
| { name: "task.add", project: ProjectResult }
|
||||
| { name: "task.edit", task: TaskResult }
|
||||
| { name: "task.delete", task: TaskResult }
|
||||
| { name: "project.add" }
|
||||
| { name: "project.edit", project: ProjectResult }
|
||||
| { name: "project.delete", project: ProjectResult }
|
||||
| { name: "group.add" }
|
||||
| { name: "group.edit", group: GroupResult }
|
||||
| { name: "group.delete", group: GroupResult }
|
||||
| { name: "item.add", group: GroupResult }
|
||||
| { name: "item.edit", item: Item, group: GroupResult }
|
||||
| { name: "item.delete", item: Item, group: GroupResult }
|
||||
| { name: "goal.add" }
|
||||
| { name: "goal.edit", goal: GoalResult }
|
||||
| { name: "goal.delete", goal: GoalResult }
|
||||
|
||||
function createModalStore() {
|
||||
const {set, subscribe} = writable<ModalData>({name: "none"});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
set,
|
||||
|
||||
close() {
|
||||
set({name: "none"})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const modalStore = createModalStore();
|
||||
|
||||
export default modalStore;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { writable } from "svelte/store";
|
||||
import stuffLogClient from "../clients/stufflog";
|
||||
import type { ProjectFilter, ProjectResult } from "../models/project";
|
||||
|
||||
interface ProjectStoreData {
|
||||
loading: boolean
|
||||
stale: boolean
|
||||
projects: ProjectResult[]
|
||||
}
|
||||
|
||||
function createProjectStore() {
|
||||
const {update, subscribe} = writable<ProjectStoreData>({
|
||||
loading: false,
|
||||
stale: true,
|
||||
projects: [],
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
markStale() {
|
||||
update(v => ({...v, stale: true}));
|
||||
},
|
||||
|
||||
async load(filter: ProjectFilter) {
|
||||
update(v => ({...v, loading: true, filter}));
|
||||
const projects = await stuffLogClient.listProjects(filter);
|
||||
update(v => ({...v, loading: false, stale: false, projects: projects.reverse()}));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const projectStore = createProjectStore();
|
||||
|
||||
export default projectStore;
|
||||
export const fpProjectStore = createProjectStore();
|
||||
@@ -0,0 +1,45 @@
|
||||
const pad = (n:number) => n < 10 ? '0'+n : n.toString();
|
||||
const weekDay = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"];
|
||||
|
||||
export function formatTime(time: Date | string): string {
|
||||
if (!(time instanceof Date)) {
|
||||
time = new Date(time);
|
||||
}
|
||||
|
||||
return `${pad(time.getHours())}:${pad(time.getMinutes())}`;
|
||||
}
|
||||
|
||||
export function formatDate(time: Date | string | number): string {
|
||||
if (!(time instanceof Date)) {
|
||||
time = new Date(time);
|
||||
}
|
||||
|
||||
return `${time.getFullYear()}-${pad(time.getMonth()+1)}-${pad(time.getDate())}`;
|
||||
}
|
||||
|
||||
export function formatWeekdayDate(time: Date | string | number): string {
|
||||
if (!(time instanceof Date)) {
|
||||
time = new Date(time);
|
||||
}
|
||||
|
||||
return `${time.getFullYear()}-${pad(time.getMonth()+1)}-${pad(time.getDate())} (${weekDay[time.getDay()]})`;
|
||||
}
|
||||
|
||||
export function formatFormTime(time: Date | string): string {
|
||||
if (!(time instanceof Date)) {
|
||||
time = new Date(time);
|
||||
}
|
||||
|
||||
return `${time.getFullYear()}-${pad(time.getMonth()+1)}-${pad(time.getDate())}T${pad(time.getHours())}:${pad(time.getMinutes())}`;
|
||||
}
|
||||
|
||||
export function nextMonth(now: Date): Date {
|
||||
let result: Date;
|
||||
if (now.getMonth() == 11) {
|
||||
result = new Date(now.getFullYear() + 1, 0, 1);
|
||||
} else {
|
||||
result = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
||||
}
|
||||
|
||||
return new Date(result.getTime());
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"types": ["svelte", "node"],
|
||||
},
|
||||
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user