Gisle Aune
4 years ago
17 changed files with 319 additions and 26 deletions
-
2database/postgres/tasks.go
-
2svelte-ui/src/App.svelte
-
30svelte-ui/src/components/Menu.svelte
-
23svelte-ui/src/components/ParentEntry.svelte
-
16svelte-ui/src/components/ProjectEntry.svelte
-
21svelte-ui/src/components/ProjectProgress.svelte
-
28svelte-ui/src/components/QLList.svelte
-
77svelte-ui/src/components/QLListItem.svelte
-
57svelte-ui/src/components/QuestLog.svelte
-
8svelte-ui/src/components/RefreshSelection.svelte
-
6svelte-ui/src/pages/FrontPage.svelte
-
2svelte-ui/src/pages/GoalPage.svelte
-
2svelte-ui/src/pages/GroupPage.svelte
-
2svelte-ui/src/pages/LogsPage.svelte
-
2svelte-ui/src/pages/ProjectPage.svelte
-
33svelte-ui/src/pages/QLPage.svelte
-
32svelte-ui/src/stores/selection.ts
@ -0,0 +1,21 @@ |
|||||
|
<script lang="ts"> |
||||
|
import type { TaskResult } from "../models/task"; |
||||
|
import Progress from "./Progress.svelte"; |
||||
|
|
||||
|
interface ProjectLike { |
||||
|
tasks?: TaskResult[] |
||||
|
} |
||||
|
|
||||
|
export let project: ProjectLike; |
||||
|
|
||||
|
let progressAmount: number; |
||||
|
let progressTarget: number; |
||||
|
|
||||
|
$: progressAmount = (project.tasks||[]).map(t => t.active |
||||
|
? Math.min(t.completedAmount, t.itemAmount) * t.item.groupWeight |
||||
|
: t.itemAmount * t.item.groupWeight |
||||
|
).reduce((n,m) => n+m, 0); |
||||
|
$: progressTarget = Math.max((project.tasks||[]).map(t => t.itemAmount * t.item.groupWeight).reduce((n,m) => n+m, 0), 1); |
||||
|
</script> |
||||
|
|
||||
|
<Progress thin green count={progressAmount} target={progressTarget} /> |
@ -0,0 +1,28 @@ |
|||||
|
<script lang="ts"> |
||||
|
import type { ProjectResult } from "../models/project"; |
||||
|
import QlListItem from "./QLListItem.svelte"; |
||||
|
|
||||
|
export let projects: ProjectResult[]; |
||||
|
export let label: string = ""; |
||||
|
</script> |
||||
|
|
||||
|
{#if projects.length > 0} |
||||
|
<div class="ql-list"> |
||||
|
<h2>{label}</h2> |
||||
|
{#each projects as project (project.id)} |
||||
|
<QlListItem project={project} /> |
||||
|
{/each} |
||||
|
</div> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
div.ql-list { |
||||
|
margin: 1em 0; |
||||
|
} |
||||
|
|
||||
|
h2 { |
||||
|
font-weight: 100; |
||||
|
margin: 0; |
||||
|
padding-bottom: 0.125em; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,77 @@ |
|||||
|
<script lang="ts"> |
||||
|
import type { ProjectResult } from "../models/project"; |
||||
|
import selectionStore from "../stores/selection"; |
||||
|
import Icon from "./Icon.svelte"; |
||||
|
import ProjectProgress from "./ProjectProgress.svelte"; |
||||
|
|
||||
|
export let project: ProjectResult; |
||||
|
|
||||
|
let selected: boolean; |
||||
|
let completed: boolean; |
||||
|
|
||||
|
function handleClick() { |
||||
|
window.location.hash = "#" + project.id; |
||||
|
selectionStore.change("hash", project.id); |
||||
|
} |
||||
|
|
||||
|
$: selected = $selectionStore.hash === project.id; |
||||
|
$: completed = !project.active; |
||||
|
</script> |
||||
|
|
||||
|
<div class="ql-list-item" on:click={handleClick} class:selected> |
||||
|
<div class="icon" class:completed> |
||||
|
<Icon block name={project.icon} /> |
||||
|
</div> |
||||
|
<div class="header"> |
||||
|
<div class="name">{project.name}</div> |
||||
|
<ProjectProgress project={project} /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
div.ql-list-item { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
-webkit-user-select: none; |
||||
|
-moz-user-select: none; |
||||
|
|
||||
|
color: #777; |
||||
|
padding: 0.2em; |
||||
|
padding-bottom: 0.05em; |
||||
|
|
||||
|
cursor: pointer; |
||||
|
} |
||||
|
div.ql-list-item:hover { |
||||
|
color: #ccc; |
||||
|
background-color: #191919; |
||||
|
} |
||||
|
|
||||
|
div.ql-list-item.selected { |
||||
|
background-color: #222; |
||||
|
} |
||||
|
|
||||
|
div.icon { |
||||
|
color: #444; |
||||
|
|
||||
|
padding: 0.3em 0.5ch; |
||||
|
padding-right: 1ch; |
||||
|
} |
||||
|
div.ql-list-item.selected div.icon { |
||||
|
color: #666; |
||||
|
} |
||||
|
div.icon.completed { |
||||
|
color: #484; |
||||
|
} |
||||
|
div.ql-list-item.selected div.icon.completed { |
||||
|
color: #78ff78; |
||||
|
} |
||||
|
div.ql-list-item:hover div.icon { |
||||
|
color: #ccc; |
||||
|
} |
||||
|
|
||||
|
div.header { |
||||
|
flex-grow: 1; |
||||
|
flex-shrink: 0; |
||||
|
padding-right: 0.5ch; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,57 @@ |
|||||
|
<script lang="ts"> |
||||
|
import type { ProjectResult } from "../models/project"; |
||||
|
import selectionStore from "../stores/selection"; |
||||
|
import ProjectEntry from "./ProjectEntry.svelte"; |
||||
|
import QlList from "./QLList.svelte"; |
||||
|
|
||||
|
export let projects: ProjectResult[]; |
||||
|
|
||||
|
let expiringProjects: ProjectResult[]; |
||||
|
let activeProjects: ProjectResult[]; |
||||
|
let completedProjects: ProjectResult[]; |
||||
|
let project: ProjectResult = null; |
||||
|
|
||||
|
$: project = $selectionStore.hash.startsWith("P") ? projects.find(p => p.id === $selectionStore.hash) : null; |
||||
|
$: expiringProjects = projects.filter(p => p.active && p.endTime).sort((a,b) => Date.parse(a.endTime) - Date.parse(b.endTime)); |
||||
|
$: activeProjects = projects.filter(p => p.active && !p.endTime).sort((a,b) => a.name.localeCompare(b.name)); |
||||
|
$: completedProjects = projects.filter(p => !p.active).sort((a,b) => a.name.localeCompare(b.name)); |
||||
|
|
||||
|
$: { |
||||
|
if (project === null && projects.length > 0) { |
||||
|
project = expiringProjects[0] || activeProjects[0] || completedProjects[0] || null; |
||||
|
if (project !== null) { |
||||
|
selectionStore.change("hash", project.id); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="quest-log"> |
||||
|
<div class="list"> |
||||
|
<QlList label="Deadlines" projects={expiringProjects} /> |
||||
|
<QlList label="Acitve" projects={activeProjects} /> |
||||
|
<QlList label="Completed" projects={completedProjects} /> |
||||
|
</div> |
||||
|
<div class="body"> |
||||
|
{#if project != null} |
||||
|
<ProjectEntry hideIcon project={project} showAllOptions /> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
div.quest-log { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
} |
||||
|
|
||||
|
div.list { |
||||
|
flex-shrink: 0; |
||||
|
width: 32ch; |
||||
|
} |
||||
|
|
||||
|
div.body { |
||||
|
flex-grow: 1; |
||||
|
margin: 1em 1ch; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,8 @@ |
|||||
|
<script lang="ts"> |
||||
|
import { onMount } from "svelte"; |
||||
|
import selectionStore from "../stores/selection"; |
||||
|
|
||||
|
onMount(() => { |
||||
|
selectionStore.refresh(); |
||||
|
}) |
||||
|
</script> |
@ -0,0 +1,33 @@ |
|||||
|
<script lang="ts"> |
||||
|
import Boi from "../components/Boi.svelte"; |
||||
|
import QuestLog from "../components/QuestLog.svelte"; |
||||
|
import RefreshSelection from "../components/RefreshSelection.svelte"; |
||||
|
import type { ModalData } from "../stores/modal"; |
||||
|
import projectStore from "../stores/project"; |
||||
|
|
||||
|
const mdProjectAdd: ModalData = {name: "project.add"}; |
||||
|
|
||||
|
$: { |
||||
|
if (($projectStore.stale || $projectStore.filter.active != null) && !$projectStore.loading) { |
||||
|
projectStore.load({}); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="page"> |
||||
|
<QuestLog projects={$projectStore.projects} /> |
||||
|
<Boi open={mdProjectAdd}>Add Project</Boi> |
||||
|
</div> |
||||
|
<RefreshSelection /> |
||||
|
|
||||
|
|
||||
|
<style> |
||||
|
div.page { |
||||
|
display: block; |
||||
|
margin: auto; |
||||
|
max-width: 100%; |
||||
|
width: 1600px; |
||||
|
margin-top: 0; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,32 @@ |
|||||
|
import { writable } from "svelte/store"; |
||||
|
|
||||
|
interface SelectionData { |
||||
|
path: string, |
||||
|
hash: string, |
||||
|
} |
||||
|
|
||||
|
function createSelectionStore() { |
||||
|
const {update, set, subscribe} = writable<SelectionData>({ |
||||
|
path: window.location.pathname, |
||||
|
hash: window.location.hash.slice(1), |
||||
|
}); |
||||
|
|
||||
|
return { |
||||
|
subscribe, |
||||
|
|
||||
|
refresh() { |
||||
|
set({ |
||||
|
path: window.location.pathname, |
||||
|
hash: window.location.hash.slice(1), |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
change(key: keyof(SelectionData), value: string) { |
||||
|
update(d => ({...d, [key]: value})); |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const selectionStore = createSelectionStore(); |
||||
|
|
||||
|
export default selectionStore; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue