Browse Source

add quest log aesthetic.

main
Gisle Aune 4 years ago
parent
commit
d3a6487dc8
  1. 2
      database/postgres/tasks.go
  2. 2
      svelte-ui/src/App.svelte
  3. 30
      svelte-ui/src/components/Menu.svelte
  4. 23
      svelte-ui/src/components/ParentEntry.svelte
  5. 16
      svelte-ui/src/components/ProjectEntry.svelte
  6. 21
      svelte-ui/src/components/ProjectProgress.svelte
  7. 28
      svelte-ui/src/components/QLList.svelte
  8. 77
      svelte-ui/src/components/QLListItem.svelte
  9. 57
      svelte-ui/src/components/QuestLog.svelte
  10. 8
      svelte-ui/src/components/RefreshSelection.svelte
  11. 6
      svelte-ui/src/pages/FrontPage.svelte
  12. 2
      svelte-ui/src/pages/GoalPage.svelte
  13. 4
      svelte-ui/src/pages/GroupPage.svelte
  14. 2
      svelte-ui/src/pages/LogsPage.svelte
  15. 2
      svelte-ui/src/pages/ProjectPage.svelte
  16. 33
      svelte-ui/src/pages/QLPage.svelte
  17. 32
      svelte-ui/src/stores/selection.ts

2
database/postgres/tasks.go

@ -51,7 +51,7 @@ func (r *taskRepository) List(ctx context.Context, filter models.TaskFilter) ([]
} }
sq = sq.InnerJoin("project AS p ON task.project_id = p.project_id") sq = sq.InnerJoin("project AS p ON task.project_id = p.project_id")
sq = sq.OrderBy("created_time")
sq = sq.OrderBy("active DESC", "created_time")
query, args, err := sq.ToSql() query, args, err := sq.ToSql()
if err != nil { if err != nil {

2
svelte-ui/src/App.svelte

@ -7,6 +7,7 @@
import LogsPage from "./pages/LogsPage.svelte"; import LogsPage from "./pages/LogsPage.svelte";
import GroupPage from "./pages/GroupPage.svelte"; import GroupPage from "./pages/GroupPage.svelte";
import GoalPage from "./pages/GoalPage.svelte"; import GoalPage from "./pages/GoalPage.svelte";
import QlPage from "./pages/QLPage.svelte";
import GroupForm from "./forms/GroupForm.svelte"; import GroupForm from "./forms/GroupForm.svelte";
import GoalForm from "./forms/GoalForm.svelte"; import GoalForm from "./forms/GoalForm.svelte";
@ -36,6 +37,7 @@
<Route path="/" component={FrontPage} /> <Route path="/" component={FrontPage} />
<Route path="/goals/" component={GoalPage} /> <Route path="/goals/" component={GoalPage} />
<Route path="/projects/" component={ProjectPage} /> <Route path="/projects/" component={ProjectPage} />
<Route path="/questlog/" component={QlPage} />
<Route path="/logs/" component={LogsPage} /> <Route path="/logs/" component={LogsPage} />
<Route path="/items/" component={GroupPage} /> <Route path="/items/" component={GroupPage} />
</main> </main>

30
svelte-ui/src/components/Menu.svelte

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { link } from "svelte-routing"; import { link } from "svelte-routing";
import selectionStore from "../stores/selection";
export let location: string = window.location.pathname.split("?")[0]; export let location: string = window.location.pathname.split("?")[0];
@ -9,19 +10,23 @@
}, 0); }, 0);
} }
$: console.log($selectionStore);
$: selected = { $: selected = {
home: location == "/",
goals: location.startsWith("/goals"),
projects: location.startsWith("/projects"),
items: location.startsWith("/items"),
logs: location.startsWith("/logs"),
home: $selectionStore.path === "/",
goals: $selectionStore.path.startsWith("/goals"),
questlog: $selectionStore.path.startsWith("/questlog"),
projects: $selectionStore.path.startsWith("/projects"),
items: $selectionStore.path.startsWith("/items"),
logs: $selectionStore.path.startsWith("/logs"),
} }
</script> </script>
<nav> <nav>
<a on:click={updateLocation} class:selected={selected.home} use:link href="/">Stufflog</a> <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.goals} use:link href="/goals">Goals</a>
<a on:click={updateLocation} class:selected={selected.projects} use:link href="/projects">Projects</a>
<a class="desktop" on:click={updateLocation} class:selected={selected.questlog} use:link href="/questlog">Projects</a>
<a class="mobile" 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.items} use:link href="/items">Items</a>
<a on:click={updateLocation} class:selected={selected.logs} use:link href="/logs">Logs</a> <a on:click={updateLocation} class:selected={selected.logs} use:link href="/logs">Logs</a>
</nav> </nav>
@ -41,4 +46,17 @@
a.selected { a.selected {
color: #AAA; color: #AAA;
} }
a.mobile {
display: none;
}
@media screen and (max-width: 600px) {
a.mobile {
display: inline-block;
}
a.desktop {
display: none;
}
}
</style> </style>

23
svelte-ui/src/components/ParentEntry.svelte

@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { IconName } from "../external/icons"; import type { IconName } from "../external/icons";
import type { TaskResult } from "../models/task";
import DaysLeft from "./DaysLeft.svelte"; import DaysLeft from "./DaysLeft.svelte";
import Icon from "./Icon.svelte"; import Icon from "./Icon.svelte";
import LinkHook from "./LinkHook.svelte"; import LinkHook from "./LinkHook.svelte";
import Markdown from "./Markdown.svelte"; import Markdown from "./Markdown.svelte";
import Progress from "./Progress.svelte";
import ProjectProgress from "./ProjectProgress.svelte";
interface EntryIconHolder { interface EntryIconHolder {
icon: IconName icon: IconName
@ -20,14 +21,15 @@
createdTime?: string createdTime?: string
group?: EntryIconHolder group?: EntryIconHolder
project?: EntryIconHolder project?: EntryIconHolder
tasks?: TaskResult[]
active?: boolean
} }
export let entry: EntryCommon; export let entry: EntryCommon;
export let full = false; export let full = false;
export let headerLink = ""; export let headerLink = "";
export let progressAmount: number = null;
export let progressTarget: number = null;
export let hideProgress: boolean = false;
export let hideIcon: boolean = false;
let iconName: IconName; let iconName: IconName;
@ -44,7 +46,11 @@
<div class="parent-entry" class:full={full}> <div class="parent-entry" class:full={full}>
<LinkHook id={entry.id} /> <LinkHook id={entry.id} />
<div class="icon"><Icon block name={iconName} /></div>
{#if !hideIcon}
<div class="icon" class:completed={entry.active === false}>
<Icon block name={iconName} />
</div>
{/if}
<div class="body"> <div class="body">
<div class="header"> <div class="header">
<div class="name"> <div class="name">
@ -60,8 +66,8 @@
</div> </div>
{/if} {/if}
</div> </div>
{#if (progressAmount != null)}
<Progress thin green count={progressAmount} target={progressTarget} />
{#if (!hideProgress && entry.tasks != null)}
<ProjectProgress project={entry} />
{/if} {/if}
{#if (full)} {#if (full)}
<Markdown source={entry.description} /> <Markdown source={entry.description} />
@ -86,6 +92,9 @@
padding-top: 0.125em; padding-top: 0.125em;
color: #333; color: #333;
} }
div.icon.completed {
color: #484;
}
div.body { div.body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

16
svelte-ui/src/components/ProjectEntry.svelte

@ -1,6 +1,9 @@
<script lang="ts"> <script lang="ts">
import type { ProjectResult } from "../models/project"; import type { ProjectResult } from "../models/project";
import type { TaskResult } from "../models/task";
import type { ModalData } from "../stores/modal"; import type { ModalData } from "../stores/modal";
import projectStore from "../stores/project";
import taskStore from "../stores/tasks";
import Option from "./Option.svelte"; import Option from "./Option.svelte";
import OptionRow from "./OptionRow.svelte"; import OptionRow from "./OptionRow.svelte";
import ParentEntry from "./ParentEntry.svelte"; import ParentEntry from "./ParentEntry.svelte";
@ -9,30 +12,25 @@
export let project: ProjectResult = null; export let project: ProjectResult = null;
export let showAllOptions: boolean = false; export let showAllOptions: boolean = false;
export let hideInactive: boolean = false; export let hideInactive: boolean = false;
export let hideProgress: boolean = false;
export let linkProject: boolean = false; export let linkProject: boolean = false;
export let hideIcon: boolean = false;
let mdAddTask: ModalData; let mdAddTask: ModalData;
let mdProjectEdit: ModalData; let mdProjectEdit: ModalData;
let mdProjectDelete: ModalData; let mdProjectDelete: ModalData;
let progressAmount: number;
let progressTarget: number;
$: mdAddTask = {name:"task.add", project}; $: mdAddTask = {name:"task.add", project};
$: mdProjectEdit = {name:"project.edit", project}; $: mdProjectEdit = {name:"project.edit", project};
$: mdProjectDelete = {name:"project.delete", project}; $: mdProjectDelete = {name:"project.delete", project};
$: 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> </script>
<ParentEntry <ParentEntry
full={showAllOptions} full={showAllOptions}
entry={project} entry={project}
headerLink={linkProject ? "/projects#"+project.id : ""} headerLink={linkProject ? "/projects#"+project.id : ""}
progressAmount={progressAmount}
progressTarget={progressTarget}
hideProgress={hideProgress}
hideIcon={hideIcon}
> >
{#if showAllOptions} {#if showAllOptions}
<OptionRow> <OptionRow>

21
svelte-ui/src/components/ProjectProgress.svelte

@ -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} />

28
svelte-ui/src/components/QLList.svelte

@ -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>

77
svelte-ui/src/components/QLListItem.svelte

@ -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>

57
svelte-ui/src/components/QuestLog.svelte

@ -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>

8
svelte-ui/src/components/RefreshSelection.svelte

@ -0,0 +1,8 @@
<script lang="ts">
import { onMount } from "svelte";
import selectionStore from "../stores/selection";
onMount(() => {
selectionStore.refresh();
})
</script>

6
svelte-ui/src/pages/FrontPage.svelte

@ -2,10 +2,11 @@
import EmptyList from "../components/EmptyList.svelte"; import EmptyList from "../components/EmptyList.svelte";
import GoalEntry from "../components/GoalEntry.svelte"; import GoalEntry from "../components/GoalEntry.svelte";
import ProjectEntry from "../components/ProjectEntry.svelte"; import ProjectEntry from "../components/ProjectEntry.svelte";
import RefreshSelection from "../components/RefreshSelection.svelte";
import type { ProjectResult } from "../models/project"; import type { ProjectResult } from "../models/project";
import { fpGoalStore } from "../stores/goal"; import { fpGoalStore } from "../stores/goal";
import projectStore, { fpProjectStore } from "../stores/project";
import taskStore, { fpTaskStore } from "../stores/tasks";
import { fpProjectStore } from "../stores/project";
import { fpTaskStore } from "../stores/tasks";
let fakeProject: ProjectResult let fakeProject: ProjectResult
let sortedProjects: ProjectResult[] let sortedProjects: ProjectResult[]
@ -85,6 +86,7 @@
{/if} {/if}
</div> </div>
</div> </div>
<RefreshSelection />
<style> <style>
div.page { div.page {

2
svelte-ui/src/pages/GoalPage.svelte

@ -3,6 +3,7 @@
import GoalEntry from "../components/GoalEntry.svelte"; import GoalEntry from "../components/GoalEntry.svelte";
import type { ModalData } from "../stores/modal"; import type { ModalData } from "../stores/modal";
import goalStore from "../stores/goal"; import goalStore from "../stores/goal";
import RefreshSelection from "../components/RefreshSelection.svelte";
const mdGoalAdd: ModalData = {name: "goal.add"}; const mdGoalAdd: ModalData = {name: "goal.add"};
@ -21,6 +22,7 @@
{/each} {/each}
<Boi open={mdGoalAdd}>Add Goal</Boi> <Boi open={mdGoalAdd}>Add Goal</Boi>
</div> </div>
<RefreshSelection />
<style> <style>
div.page { div.page {

4
svelte-ui/src/pages/GroupPage.svelte

@ -3,7 +3,8 @@
import GroupEntry from "../components/GroupEntry.svelte"; import GroupEntry from "../components/GroupEntry.svelte";
import type { ModalData } from "../stores/modal"; import type { ModalData } from "../stores/modal";
import groupStore from "../stores/group"; import groupStore from "../stores/group";
import TableOfContent from "../components/TableOfContent.svelte";
import TableOfContent from "../components/TableOfContent.svelte";
import RefreshSelection from "../components/RefreshSelection.svelte";
const mdGroupAdd: ModalData = {name: "group.add"}; const mdGroupAdd: ModalData = {name: "group.add"};
@ -21,6 +22,7 @@ import TableOfContent from "../components/TableOfContent.svelte";
{/each} {/each}
<Boi open={mdGroupAdd}>Add Group</Boi> <Boi open={mdGroupAdd}>Add Group</Boi>
</div> </div>
<RefreshSelection />
<style> <style>
div.page { div.page {

2
svelte-ui/src/pages/LogsPage.svelte

@ -5,6 +5,7 @@
import { formatDate, formatTime, formatWeekdayDate } from "../utils/time"; import { formatDate, formatTime, formatWeekdayDate } from "../utils/time";
import Boi from "../components/Boi.svelte"; import Boi from "../components/Boi.svelte";
import EmptyList from "../components/EmptyList.svelte"; import EmptyList from "../components/EmptyList.svelte";
import RefreshSelection from "../components/RefreshSelection.svelte";
let groupedLogs: {day: number, text: string, logs: LogResult[]}[] = []; let groupedLogs: {day: number, text: string, logs: LogResult[]}[] = [];
let minTime = $logStore.filter.minTime || new Date(Date.now() - (86400000*30)); let minTime = $logStore.filter.minTime || new Date(Date.now() - (86400000*30));
@ -95,6 +96,7 @@
<Boi disabled>Loading...</Boi> <Boi disabled>Loading...</Boi>
{/if} {/if}
</div> </div>
<RefreshSelection />
<style> <style>
div.page { div.page {

2
svelte-ui/src/pages/ProjectPage.svelte

@ -2,6 +2,7 @@
import Boi from "../components/Boi.svelte"; import Boi from "../components/Boi.svelte";
import Checkbox from "../components/Checkbox.svelte"; import Checkbox from "../components/Checkbox.svelte";
import ProjectEntry from "../components/ProjectEntry.svelte"; import ProjectEntry from "../components/ProjectEntry.svelte";
import RefreshSelection from "../components/RefreshSelection.svelte";
import TableOfContent from "../components/TableOfContent.svelte"; import TableOfContent from "../components/TableOfContent.svelte";
import type { ModalData } from "../stores/modal"; import type { ModalData } from "../stores/modal";
import projectStore from "../stores/project"; import projectStore from "../stores/project";
@ -37,6 +38,7 @@
{/each} {/each}
<Boi open={mdProjectAdd}>Add Project</Boi> <Boi open={mdProjectAdd}>Add Project</Boi>
</div> </div>
<RefreshSelection />
<style> <style>
div.page { div.page {

33
svelte-ui/src/pages/QLPage.svelte

@ -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>

32
svelte-ui/src/stores/selection.ts

@ -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;
Loading…
Cancel
Save