first commit

This commit is contained in:
2020-12-31 17:49:54 +01:00
commit 2240985aa1
120 changed files with 11574 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
/node_modules/
/public/build/
/build.env
/.idea
/.vscode
.DS_Store
+4368
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -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

+62
View File
@@ -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;
}
+17
View File
@@ -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>
+99
View File
@@ -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 );
}
};
+77
View File
@@ -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>
+46
View File
@@ -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());
}
+260
View File
@@ -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
+53
View File
@@ -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>
+17
View File
@@ -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>
+87
View File
@@ -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>
+93
View File
@@ -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>
+22
View File
@@ -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>
+86
View File
@@ -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>
+95
View File
@@ -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>
+44
View File
@@ -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>
+215
View File
@@ -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}
+38
View File
@@ -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>
+10
View File
@@ -0,0 +1,10 @@
<div class="option-row">
<slot></slot>
</div>
<style>
div.option-row {
margin-left: -0.5ch;
margin-right: -0.5ch;
}
</style>
+80
View File
@@ -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>
+161
View File
@@ -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>
+138
View File
@@ -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;
+112
View File
@@ -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>
+90
View File
@@ -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>
+54
View File
@@ -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>
+50
View File
@@ -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>
+56
View File
@@ -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>
+71
View File
@@ -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>
+56
View File
@@ -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>
+59
View File
@@ -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>
+74
View File
@@ -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>
+54
View File
@@ -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>
+72
View File
@@ -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>
+60
View File
@@ -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>
+63
View File
@@ -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>
+8
View File
@@ -0,0 +1,8 @@
import App from './App.svelte';
const app = new App({
target: document.body,
props: {}
});
export default app;
+47
View File
@@ -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
}
+24
View File
@@ -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
}
+27
View File
@@ -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
}
+30
View File
@@ -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
}
+37
View File
@@ -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
}
+40
View File
@@ -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
}
+80
View File
@@ -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>
+34
View File
@@ -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>
+32
View File
@@ -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>
+105
View File
@@ -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>
+34
View File
@@ -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>
+24
View File
@@ -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;
+36
View File
@@ -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();
+35
View File
@@ -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;
+37
View File
@@ -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;
+46
View File
@@ -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;
+36
View File
@@ -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();
+45
View File
@@ -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());
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"types": ["svelte", "node"],
},
"include": ["src/**/*"],
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
}