clean up svelte ui big time, and add beginnings of item stuff.
continuous-integration/drone/push Build is passing

This commit is contained in:
2020-02-29 16:54:38 +01:00
parent e724eb57b5
commit bb13195f4b
42 changed files with 1051 additions and 504 deletions
+5 -1
View File
@@ -3,6 +3,7 @@
import LogPage from "./routes/LogPage";
import ActivitiesPage from "./routes/ActivitiesPage";
import ItemPage from "./routes/ItemPage";
import LoginModal from "./modals/LoginModal";
import CreateActivityModal from "./modals/CreateActivityModal";
@@ -31,7 +32,9 @@
</script>
<Menu>
<MenuItem href="/">Stufflog</MenuItem><MenuItem href="/activities/">Activities</MenuItem>
<MenuItem href="/">Stufflog</MenuItem>
<MenuItem href="/activities/">Activities</MenuItem>
<MenuItem href="/items/">Items</MenuItem>
<div slot="right">
{#if ($auth.checked && $auth.user !== null)}
@@ -43,6 +46,7 @@
<Router>
<Route path="/" component={LogPage} />
<Route path="/activities/" component={ActivitiesPage} />
<Route path="/items/" component={ItemPage} />
</Router>
<Modal name="login" component={LoginModal} />
+67 -11
View File
@@ -1,6 +1,7 @@
import ky from "ky";
import Activity, {ActivityUpdate} from "../models/activity";
import Period, {PeriodUpdate} from "../models/period";
import Item, {ItemUpdate} from "../models/item";
function json(data) {
return {
@@ -33,7 +34,7 @@ export class StuffLogAPI {
/**
* List activities
*
* @returns {Activity[]}
* @returns {Promise<Activity[]>}
*/
async listActivities() {
const data = await k.get("/api/activity/").json();
@@ -42,7 +43,7 @@ export class StuffLogAPI {
/**
* List periods
*
* @returns {{periods: Period[], activities: Activity[]}}
* @returns {Promise<{>periods: Period[], activities: Activity[]}}
*/
async listPeriods() {
const data = await k.get("/api/period/").json();
@@ -53,7 +54,7 @@ export class StuffLogAPI {
* Find activity
*
* @param {string} id
* @returns {Activity}
* @returns {Promise<Activity>}
*/
async findActivity(id) {
const data = await k.get(`/api/activity/${id}`).json();
@@ -63,7 +64,7 @@ export class StuffLogAPI {
* Find period
*
* @param {string} id
* @returns {Period}
* @returns {Promise<Period>}
*/
async findPeriod(id) {
const data = await k.get(`/api/period/${id}`).json();
@@ -73,7 +74,7 @@ export class StuffLogAPI {
* Create a new activity
*
* @param {Activity} activity
* @returns {Activity}
* @returns {Promise<Activity>}
*/
async postActivity(activity) {
const data = await k.post(`/api/activity/`, json(activity)).json();
@@ -83,7 +84,7 @@ export class StuffLogAPI {
* Create a new period
*
* @param {Period} period
* @returns {Period}
* @returns {Promise<Period>}
*/
async postPeriod(period) {
const data = await k.post(`/api/period/`, json(period)).json();
@@ -94,7 +95,7 @@ export class StuffLogAPI {
*
* @param {string} id
* @param {ActivityUpdate[]} updates
* @returns {Activity}
* @returns {Promise<Activity>}
*/
async patchActivity(id, ...updates) {
const data = await k.patch(`/api/activity/${id}`, json(updates)).json();
@@ -105,7 +106,7 @@ export class StuffLogAPI {
*
* @param {string} id
* @param {PeriodUpdate[]} updates
* @returns {Period}
* @returns {Promise<Period>}
*/
async patchPeriod(id, ...updates) {
const data = await k.patch(`/api/period/${id}`, json(updates)).json();
@@ -115,23 +116,78 @@ export class StuffLogAPI {
* Update an activity
*
* @param {string} id
* @returns {Activity}
* @returns {Promise<Activity>}
*/
async deleteActivity(id) {
const data = await k.delete(`/api/activity/${id}`).json();
return new Activity(data.activity);
}
/**
* Update an period
*
* @param {string} id
* @returns {Period}
* @returns {Promise<Period>}
*/
async deletePeriod(id) {
const data = await k.delete(`/api/period/${id}`).json();
return new Period(data.period);
}
/**
* Get items
*
* @param {string} id
* @returns {Promise<Item[]>}
*/
async listItems() {
const data = await k.get(`/api/item/`).json();
return data.items
}
/**
* Get an item
*
* @param {string} id
* @returns {Promise<Item>}
*/
async getItem(id) {
const data = await k.get(`/api/item/${id}`).json();
return data.item
}
/**
* Post an item
*
* @param {Item} item
* @returns {Promise<Item>}
*/
async postItem(item) {
const data = await k.post(`/api/item/`, json(item)).json();
return data.item
}
/**
* Delete an item
*
* @param {string} id
* @param {ItemUpdate} updates
* @returns {Promise<Item>}
*/
async updateItem(id, ...updates) {
const data = await k.patch(`/api/item/${id}`, json(updates)).json();
return data.item
}
/**
* Delete an item
*
* @param {string} id
* @returns {Promise<Item>}
*/
async deleteItem(id) {
const data = await k.delete(`/api/item/${id}`).json();
return data.item
}
}
const slApi = new StuffLogAPI();
@@ -0,0 +1,23 @@
<script>
import pluralize from "pluralize";
import ActivityIcon from "./ActivityIcon.svelte";
export let amount = 0
export let subActivity = null
</script>
{#if subActivity != null}
<span>{amount} {pluralize(subActivity.unitName, amount)}</span>
{:else}
<span class="unknown">{amount} (unknown unit)</span>
{/if}
<style>
span {
display: inline-block;
}
span.unknown {
color: #555;
}
</style>
@@ -0,0 +1,26 @@
<script>
import ActivityIcon from "./ActivityIcon.svelte";
export let activity = null
export let subActivity = null
</script>
{#if activity != null && !activity.deleted}
<div class="icon"><ActivityIcon name={activity.icon} /></div>
<span class="name">{activity.name}{(subActivity && !subActivity.deleted) ? " " + subActivity.name : ""}</span>
{:else}
<div class="icon unknown"><ActivityIcon name="question" /></div>
<span class="name unknown">(Unknown)</span>
{/if}
<style>
div.icon {
position: relative;
top: 0.175em;
display: inline-block;
}
div.unknown, span.unknown {
color: #555;
}
</style>
@@ -14,6 +14,8 @@
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";
const icons = {
"plus": faPlus,
@@ -29,6 +31,8 @@
"code": faCode,
"code_branch": faCodeBranch,
"guitar": faGuitar,
"archive": faArchive,
"check": faCheck,
};
export const iconNames = Object.keys(icons);
+7 -3
View File
@@ -2,11 +2,10 @@
import Icon from 'fa-svelte'
import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus'
export let header = "";
export let icon = null;
export let top = false;
</script>
<div on:click class="addboi">
<div on:click class="addboi" class:top>
<Icon class="addboi-icon" icon={faPlus} />
<slot></slot>
</div>
@@ -26,6 +25,11 @@
transition: 250ms;
}
div.addboi.top {
margin-bottom: 0.5em;
padding: 0.25em 0;
font-size: 1.5em;
}
div.addboi:hover {
color: #777;
-51
View File
@@ -1,51 +0,0 @@
<script>
export let size = 1;
</script>
<div class="col col-{size}"><slot></slot></div>
<style>
div.col {
display: inline-block;
box-sizing: border-box;
padding: 0;
margin: 0;
}
div.col-1 {
width: calc((100% / 12) * 1)
}
div.col-2 {
width: calc((100% / 12) * 2)
}
div.col-3 {
width: calc((100% / 12) * 3)
}
div.col-4 {
width: calc((100% / 12) * 4)
}
div.col-5 {
width: calc((100% / 12) * 5)
}
div.col-6 {
width: calc((100% / 12) * 6)
}
div.col-7 {
width: calc((100% / 12) * 7)
}
div.col-8 {
width: calc((100% / 12) * 8)
}
div.col-9 {
width: calc((100% / 12) * 9)
}
div.col-10 {
width: calc((100% / 12) * 10)
}
div.col-11 {
width: calc((100% / 12) * 11)
}
div.col-12 {
width: 100%
}
</style>
@@ -0,0 +1,20 @@
<script>
import ActivityIcon from "./ActivityIcon.svelte";
export let item = null
export let id = ""
</script>
{#if item != null}
<span>{item.name}</span>
{:else if id != ""}
<span class="unknown">(Deleted)</span>
{:else}
<span class="unknown">(None)</span>
{/if}
<style>
span.unknown {
color: #555;
}
</style>
+2
View File
@@ -16,5 +16,7 @@
background-color: #222;
margin: 0;
user-select: none;
display: flex;
flex-direction: row;
}
</style>
+1 -1
View File
@@ -8,7 +8,7 @@
<style>
a.menu-item {
display: inline-block;
display: flex;
background-color: #222;
margin: 0;
padding: 0.25em 1ch;
+3 -2
View File
@@ -21,7 +21,7 @@
v = 0;
}
if (i >= red && v === 0) {
if (i >= gold && v === 0) {
break;
}
@@ -30,6 +30,7 @@
gold: i >= gold && i < red,
red: i >= red,
value: v,
empty: v === 0,
full: v === 1000,
})
}
@@ -64,7 +65,7 @@
</script>
{#each bars as bar}
<div class="bar" class:red={bar.red} class:green={bar.green} class:gold={bar.gold} class:full={bar.full}>
<div class="bar" class:red={bar.red} class:green={bar.green} class:gold={bar.gold} class:empty={bar.empty} class:full={bar.full}>
<div class="content" style={`height: ${bar.value / 10}%; top: ${(1000 - bar.value) / 10}%`}>
</div>
</div>
-8
View File
@@ -1,8 +0,0 @@
<div class="row"><slot></slot></div>
<style>
div.row {
display: flex;
flex-wrap: wrap;
}
</style>
+51
View File
@@ -0,0 +1,51 @@
<script>
export let headers = [];
export let percentages = [];
let zipped = [];
$: zipped = headers.map((h, i) => ({header: h, pct: percentages[i]}))
$: if (percentages.length !== headers.length) {
throw new Error(`Mismatching headers (${headers.join(",")}) and percentages (${percentages.join(",")})`)
}
</script>
<table>
<thead>
<tr>
{#each zipped as h}
<th style={`width: ${h.pct}%;`}>{h.header}</th>
{/each}
</tr>
</thead>
<tbody>
<slot></slot>
</tbody>
</table>
<style>
table {
width: 100%;
padding: 0.5em 0.75ch;
}
table :global(th), table :global(td) {
box-sizing: border-box;
}
table :global(th) {
text-align: left;
font-size: 0.75em;
}
table :global(td.ellipsis) {
max-width: 1px; /* Don't ask me why this works... */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
table :global(td:last-of-type), table :global(th:last-of-type) {
text-align: right;
}
</style>
@@ -0,0 +1,38 @@
<script>
import { createEventDispatcher } from 'svelte';
import pluralize from "pluralize";
import Link from "../Link.svelte";
import Table from "../Table.svelte";
import ActivityIcon from "../ActivityIcon.svelte";
import PointsBar from "../PointsBar.svelte";
import ActivityDisplay from "../ActivityDisplay.svelte";
export let period = {goals: []};
export let activities = [];
const dispatch = createEventDispatcher();
let activityMap = {};
function onOption(name, goalId) {
const goal = period.goals.find(g => g.id === goalId);
const activity = activities.find(a => a.id === goal.activityId)
dispatch("option", {name, period, goal, activity})
}
</script>
<Table headers={["Activity", "Points", "Options"]} percentages={[25, 55, 20]}>
{#each period.goals as goal (goal.id)}
<tr>
<td class="ellipsis">
<ActivityDisplay activity={activities.find(a => a.id === goal.activityId)} />
</td>
<td><PointsBar value={period.scores[goal.id]} goal={goal.pointCount} /></td>
<td class="td-options">
<Link on:click={() => onOption("periodgoal.remove", goal.id)}>Delete</Link>
</td>
</tr>
{/each}
</Table>
@@ -0,0 +1,30 @@
<script>
import { createEventDispatcher } from 'svelte';
import Link from "../Link.svelte";
import Table from "../Table.svelte";
export let items = [];
const dispatch = createEventDispatcher();
function onOption(name, id) {
const item = items.find(i => i.id === id)
dispatch("option", {name, item})
}
</script>
<Table headers={["Name", "Multiplier", "Options"]} percentages={[60, 5, 35]}>
{#each items as item (item.id)}
<tr>
<td class="ellipsis">{item.name}</td>
<td>{item.multiplier.toFixed(2)}</td>
<td>
<Link on:click={() => onOption("item.stats", item.id)}>Stats</Link>,
<Link on:click={() => onOption("item.edit", item.id)}>Edit</Link>,
<Link on:click={() => onOption("item.remove", item.id)}>Delete</Link>
</td>
</tr>
{/each}
</Table>
@@ -0,0 +1,66 @@
<script>
import pluralize from "pluralize";
import Link from "../Link.svelte";
import Table from "../Table.svelte";
import { createEventDispatcher } from 'svelte';
import ActivityIcon from "../ActivityIcon.svelte";
import ActivityDisplay from "../ActivityDisplay.svelte";
import ActivityAmount from "../ActivityAmount.svelte";
import ItemDisplay from "../ItemDisplay.svelte";
import PointsBar from "../PointsBar.svelte";
import dateStr from "../../utils/dateStr";
export let period = {goals: []};
export let activities = [];
export let items = [];
const dispatch = createEventDispatcher();
let subActivityMap = {};
function onOption(name, logId) {
const log = period.logs.find(l => l.id === logId);
const goal = period.goals.find(g => g.id === log.goalId)
const activity = goal != null ? activities.find(a => a.id === goal.activityId) : null;
const subActivity = activity != null ? activity.subActivities.find(s => s.id === log.subActivityId) : null;
dispatch("option", {name, period, log, goal, activity, subActivity})
}
$: subActivityMap = activities.map(a => a.subActivities).flat().reduce((p, v) => ({...p, [v.id]: v}), {});
</script>
<Table
headers={["Date", "Goal", "Item", "Amount", "Points", "Options"]}
percentages={[25, 25, 15, 15, 5, 15]}
>
{#each period.logs as log (log.id)}
<tr>
<td>{dateStr(log.date)}</td>
<td class="ellipsis">
<ActivityDisplay
activity={activities.find(a => a.id === (period.goals.find(g => g.id === log.goalId) || {}).activityId)}
subActivity={subActivityMap[log.subActivityId]}
/>
</td>
<td>
<ItemDisplay id={log.itemId} item={items.find(i => i.id === log.itemId)} />
</td>
<td>
<ActivityAmount
amount={log.amount}
subActivity={subActivityMap[log.subActivityId]}
/>
</td>
<td>
<Link on:click={() => onOption("periodlog.info", log.id)}>{log.score.total}</Link>
</td>
<td class="td-options">
<Link on:click={() => onOption("periodlog.remove", log.id)}>Delete</Link>
</td>
</tr>
{/each}
</Table>
@@ -0,0 +1,30 @@
<script>
import { createEventDispatcher } from 'svelte';
import pluralize from "pluralize";
import Link from "../Link.svelte";
import Table from "../Table.svelte";
export let activity = {subActivities: []}
const dispatch = createEventDispatcher();
function onOption(name, subId) {
const subActivity = activity.subActivities.find(s => s.id === subId);
dispatch("option", {name, activity, subActivity})
}
</script>
<Table headers={["Sub-Activity", "Value", "Options"]} percentages={[50, 25, 25]}>
{#each activity.subActivities as subActivtiy (subActivtiy.id)}
<tr>
<td>{subActivtiy.name}</td>
<td>{subActivtiy.value} per {pluralize(subActivtiy.unitName, 1)}</td>
<td>
<Link on:click={() => onOption("subactivity.edit", subActivtiy.id)}>Edit</Link>,
<Link on:click={() => onOption("subactivity.remove", subActivtiy.id)}>Delete</Link>
</td>
</tr>
{/each}
</Table>
@@ -11,7 +11,6 @@
let error = null;
let activityId = "";
let pointCount = "1000";
let subGoals = [{name:"",multiplier:"1.0"}];
function addPeriodGoal() {
error = null;
@@ -26,28 +25,9 @@
return
}
const parsedSubGoals = [];
for (const subGoal of subGoals) {
if (subGoal.name === "") {
continue;
}
const parsedMultiplier = parseFloat(subGoal.multiplier);
if (Number.isNaN(parsedMultiplier) || parsedMultiplier < 0) {
error = `Sub goal ${subGoal.name} needs a numeric multiplier.`;
return
}
parsedSubGoals.push({
name: subGoal.name,
multiplier: parsedMultiplier,
})
}
const addGoal = {
activityId: activityId,
pointCount: parsedPointCount,
subGoals: parsedSubGoals,
}
stufflog.updatePeriod(period.id, {addGoal}).then(() => {
@@ -58,24 +38,6 @@
});
}
function updateSubGoalForm(subGoals) {
let last = -1;
for (let i = 0; i < subGoals.length; ++i) {
if (subGoals[i].name != "") {
last = i;
}
}
if (last == subGoals.length - 1) {
return [...subGoals, {name: "", value: "1.0"}];
} else if (last + 2 < subGoals.length && subGoals.length > 1) {
return subGoals.slice(0, last + 2);
} else {
return subGoals;
}
}
$: subGoals = updateSubGoalForm(subGoals);
$: if (activityId === "" && $stufflog.activities.length > 0) {
activityId = $stufflog.activities[0].id;
}
@@ -91,10 +53,6 @@
</select>
<label>Points</label>
<input class="nolast" type="string" bind:value={pointCount} />
<label>Subgoals</label>
{#each subGoals as subGoal}
<SubGoalInput bind:name={subGoal.name} bind:value={subGoal.multiplier} />
{/each}
<p>
The amount of points must be in an increment of 1000, which should be
+14 -16
View File
@@ -16,12 +16,13 @@
let goal = null;
let activity = null;
let subActivity = null;
let dateInput = dateStr(period.from);
let dateInput = dateStr(new Date);
let goalId = "";
let subGoalId = "";
let subActivityId = "";
let amount = "200";
let description = "";
let dateInPeriod = false;
function addPeriodLog() {
error = null;
@@ -56,9 +57,8 @@
subActivityId = "";
}
}
$: if (goal != null && !goal.subGoals.find(s => s.id === subGoalId)) {
subGoalId = "";
}
$: dateInPeriod = Date.parse(dateInput) > Date.parse(period.from) && Date.parse(dateInput) < Date.parse(period.to)
</script>
<ModalFrame title={`Add ${period.name} Log`} error={error} closable on:close={() => modal.close()}>
@@ -66,6 +66,10 @@
<label>Date</label>
<input type="date" bind:value={dateInput} />
{#if (!dateInPeriod)}
<p class="warning">Date is outside period!</p>
{/if}
<label>Goal</label>
<select bind:value={goalId}>
{#each period.goals as goal (goal.id)}
@@ -82,17 +86,6 @@
</select>
{/if}
{#if (goal != null && goal.subGoals.length > 0)}
<label>Sub-Goal</label>
<select bind:value={subGoalId}>
<option value="">None</option>
{#each goal.subGoals as subGoal (subGoal.id)}
<option value={subGoal.id}>{subGoal.name} ({subGoal.multiplier.toFixed(2)})</option>
{/each}
</select>
{/if}
{#if (subActivity != null)}
<label>{pluralize(capitalize(subActivity.unitName))}</label>
<input type="number" bind:value={amount} />
@@ -103,9 +96,14 @@
<hr />
<button type="submit">Add Goal</button>
<button type="submit">Add Log</button>
</form>
</ModalFrame>
<style>
p.warning {
color: #FB1;
margin: 0;
text-align: center;
}
</style>
+15 -9
View File
@@ -14,18 +14,20 @@
export let log = {};
export let activity = {};
export let subActivity = {};
export let subGoal = null;
export let item = null;
let activityPoints = 0;
let subGoalBonus = 0;
let itemBonus = 0;
let roundingError = 0;
let title = ""
$: activityPoints = log.score.amount * log.score.activityScore;
$: subGoalBonus = subGoal.multiplier ? activityPoints * (subGoal.multiplier - 1) : 0;
$: roundingError = (log.score.total) - (Math.floor(activityPoints) + Math.floor(subGoalBonus) + log.score.dailyBonus);
$: itemBonus = item ? activityPoints * (item.multiplier - 1) : 0;
$: roundingError = (log.score.total) - (Math.floor(activityPoints) + Math.floor(itemBonus) + log.score.dailyBonus);
$: title = `${dateStr(log.date)} - ${(activity||{name:"(Unknown)"}).name} ${(subActivity||{name:"(Unknown)"}).name}`
</script>
<ModalFrame title={`${dateStr(log.date)} - ${activity.name} ${subActivity.name}`} closable on:close={() => modal.close()}>
<ModalFrame title={title} closable on:close={() => modal.close()}>
<form on:submit|preventDefault={() => modal.close()}>
{#if log.description !== ""}
<Property label="Description">
@@ -35,12 +37,16 @@
<Property label="Activity Points">
{Math.floor(activityPoints + roundingError)} points
({log.score.amount} {pluralize(subActivity.unitName, log.score.amount)})
{#if (subActivity != null)}
<span>
({log.score.amount} {pluralize(subActivity.unitName, log.score.amount)})
</span>
{/if}
</Property>
{#if subGoal.multiplier != null}
{#if item != null}
<Property label="Sub-Goal">
{Math.floor(subGoalBonus)} points
({subGoal.name})
{Math.floor(itemBonus)} points
({item.name})
</Property>
{/if}
{#if log.score.dailyBonus != 0}
@@ -27,7 +27,7 @@ let error = null;
<form on:submit|preventDefault={() => removePeriodLog()}>
<p>
Are you sure you want to remove log <b>{log.description}</b>
(<b>{dateStr(log.date)}</b>, <b>{activity.name}</b>) from
(<b>{dateStr(log.date)}</b>, <b>{activity ? activity.name : "(Unknown)"}</b>) from
period <b>{period.name}</b>?
</p>
@@ -6,12 +6,12 @@
import stufflog from "../stores/stufflog";
export let activity = {};
export let subActivtiy = {};
export let subActivity = {};
let error = null;
function removeSubActivity() {
stufflog.updateActivity(activity.id, {removeSub: subActivtiy.id}).then(() => {
stufflog.updateActivity(activity.id, {removeSub: subActivity.id}).then(() => {
modal.close();
}).catch(err => {
error = err.message || err;
@@ -23,7 +23,7 @@
<ModalFrame title={`Remove ${activity.name} Sub-Activity`} error={error} closable on:close={() => modal.close()}>
<form on:submit|preventDefault={() => removeSubActivity()}>
<p>
Are you sure you want to remove sub-acitvity <b>{subActivtiy.name || "(unnamed)"}</b> from
Are you sure you want to remove sub-acitvity <b>{subActivity.name || "(Unnamed)"}</b> from
activity <b>{activity.name}</b>?
</p>
+12
View File
@@ -0,0 +1,12 @@
export default interface Item {
id: string
userId: string
active: boolean
name: string
multiplier: number
}
export interface ItemUpdate {
setName: string
setActive: boolean
}
+1
View File
@@ -0,0 +1 @@
module.exports = {}
+20 -54
View File
@@ -10,55 +10,42 @@
import Boi from "../components/Boi.svelte";
import AddBoi from "../components/AddBoi.svelte";
import Property from "../components/Property.svelte";
import Row from "../components/Row.svelte";
import Col from "../components/Col.svelte";
import Link from "../components/Link.svelte";
import Table from "../components/Table.svelte";
import SubActivityTable from "../components/tables/SubActivityTable";
function openModal(modalName, id, subId) {
function onActivityOption(modalName, id) {
const activity = get(stufflog).activities.find(a => a.id === id);
const subActivity = (activity || {subActivities:[]}).subActivities.find(s => s.id === subId);
modal.open(modalName, {activity, subActivity})
modal.open(modalName, {activity})
}
</script>
<div class="page">
<AddBoi top on:click={() => modal.open("activity.create")}>Activity</AddBoi>
{#each $stufflog.activities as activity (activity.id)}
<Boi header={activity.name} icon={activity.icon}>
<Row>
<Col size=3><Property label="ID" value={activity.id} /></Col>
<Col size=3><Property label="Daily Bonus" value={activity.dailyBonus} /></Col>
<Col size=6>
<Property label="Options">
<Link on:click={() => openModal("activity.edit", activity.id)}>Edit Activity</Link>,
<Link on:click={() => openModal("subactivity.add", activity.id)}>Add Sub-Activity</Link>,
<Link on:click={() => openModal("activity.delete", activity.id)}>Delete Activity</Link>
</Property>
</Col>
</Row>
<table>
<Table headers={["ID", "Daily Bonus", "Options"]} percentages={[25, 25, 50]}>
<tr>
<th class="th-name">Sub-Activity</th>
<th class="th-value">Value</th>
<th class="th-options">Options</th>
<td>{activity.id}</td>
<td>{activity.dailyBonus}</td>
<td>
<Link on:click={() => onActivityOption("activity.edit", activity.id)}>Edit Activity</Link>,
<Link on:click={() => onActivityOption("subactivity.add", activity.id)}>Add Sub-Activity</Link>,
<Link on:click={() => onActivityOption("activity.delete", activity.id)}>Delete Activity</Link>
</td>
</tr>
{#each activity.subActivities as subActivtiy (subActivtiy.id)}
<tr>
<td>{subActivtiy.name}</td>
<td>{subActivtiy.value} per {pluralize(subActivtiy.unitName, 1)}</td>
<td>
<Link on:click={() => openModal("subactivity.edit", activity.id, subActivtiy.id)}>Edit</Link>,
<Link on:click={() => openModal("subactivity.remove", activity.id, subActivtiy.id)}>Delete</Link>
</td>
</tr>
{/each}
</table>
</Table>
<SubActivityTable
activity={activity}
on:option={(e) => modal.open(e.detail.name, e.detail)}
/>
</Boi>
{:else}
<div class="empty">No data.</div>
{/each}
<AddBoi on:click={() => modal.open("activity.create")}>Activity</AddBoi>
</div>
<style>
@@ -67,25 +54,4 @@
max-width: 90%;
margin: auto;
}
table {
width: 100%;
padding: 0.5em 0;
}
th, td {
padding: 0em 1ch;
}
th {
text-align: left;
font-size: 0.75em;
}
th.th-name {
width: 50%;
}
th.th-value {
width: 25%;
}
th.th-options {
width: 25%;
}
</style>
+49
View File
@@ -0,0 +1,49 @@
<script>
import {onMount} from "svelte";
import {get} from "svelte/store";
import items from "../stores/items";
import modal from "../stores/modal";
import Boi from "../components/Boi.svelte";
import AddBoi from "../components/AddBoi.svelte";
import Link from "../components/Link.svelte";
import ItemTable from "../components/tables/ItemTable.svelte";
let activeItems = [];
let inactiveItems = [];
onMount(() => {
items.listItems().catch(err => {
console.warn("Item fetch failed:", err)
})
})
function openModal(modalName, id) {
const list = get(items)
const item = list.find(i => i.id === id)
modal.open(modalName, {item})
}
$: activeItems = $items.filter(i => i.active)
$: inactiveItems = $items.filter(i => !i.active)
</script>
<div class="page">
<AddBoi top on:click={() => modal.open("item.create")}>Item</AddBoi>
<Boi header="Active Items" icon="cubes">
<ItemTable items={activeItems} />
</Boi>
<Boi header="Archived Items" icon="archive">
<ItemTable items={inactiveItems} />
</Boi>
</div>
<style>
div.page {
width: 100ch;
max-width: 90%;
margin: auto;
}
</style>
+30 -136
View File
@@ -1,117 +1,56 @@
<script context="module">
const UNKNOWN = (Object.freeze||(o => o))({
name: "(Unknown)",
subGoals: [],
subActivities: [],
});
</script>
<script>
import { get } from "svelte/store";
import pluralize from "pluralize";
import Boi from "../components/Boi.svelte";
import AddBoi from "../components/AddBoi.svelte";
import Property from "../components/Property.svelte";
import Row from "../components/Row.svelte";
import Col from "../components/Col.svelte";
import Link from "../components/Link.svelte";
import ActivityIcon from "../components/ActivityIcon.svelte";
import PointsBar from "../components/PointsBar.svelte";
import Table from "../components/Table.svelte";
import GoalTable from "../components/tables/GoalTable.svelte";
import LogTable from "../components/tables/LogTable.svelte";
import stufflog from "../stores/stufflog";
import modal from "../stores/modal";
import items from "../stores/items";
import dateStr from "../utils/dateStr";
let activityMap = {};
let scores = {};
function openGoalModal(modalName, id, goalId, subGoalId) {
const period = get(stufflog).periods.find(a => a.id === id);
const goal = period ? period.goals.find(g => g.id === goalId) : null;
const subGoal = goal ? goal.subGoals.find(s => s.id === subGoalId) : null;
const activity = goal ? get(stufflog).activities.find(a => a.id === goal.activityId) : null
modal.open(modalName, {period, goal, subGoal, activity})
function getPeriod(id) {
return get(stufflog).periods.find(a => a.id === id)
}
function openLogModal(modalName, id, logId) {
const period = get(stufflog).periods.find(a => a.id === id);
const row = period ? period.table.find(r => r.log.id === logId) : {};
modal.open(modalName, {period, ...row})
}
$: activityMap = $stufflog.activities.reduce((p, v) => ({...p, [v.id]: v}), {});
</script>
<div class="page">
<AddBoi top on:click={() => modal.open("period.create")}>Period</AddBoi>
{#each $stufflog.periods as period (period.id)}
<Boi header={period.name}>
<Row>
<Col size=3><Property label="From" value={dateStr(period.from)} /></Col>
<Col size=3><Property label="To" value={dateStr(period.to)} /></Col>
<Col size=6>
<Property label="Options">
<Link on:click={() => openGoalModal("period.edit", period.id)}>Edit Period</Link>,
<Link on:click={() => openGoalModal("periodgoal.add", period.id)}>Add Goal</Link>,
<Table headers={["From", "To", "Options"]} percentages={[25, 25, 50]}>
<td>{dateStr(period.from)}</td>
<td>{dateStr(period.to)}</td>
<td>
<Link on:click={() => modal.open("period.edit", {period: getPeriod(period.id)})}>Edit Period</Link>,
<Link on:click={() => modal.open("periodgoal.add", {period: getPeriod(period.id)})}>Add Goal</Link>,
{#if period.goals.length > 0}
<Link on:click={() => openGoalModal("periodlog.add", period.id)}>Add Log</Link>,
<Link on:click={() => modal.open("periodlog.add", {period: getPeriod(period.id)})}>Add Log</Link>,
{/if}
<Link on:click={() => openGoalModal("period.delete", period.id)}>Delete Period</Link>
</Property>
</Col>
</Row>
<table>
<tr>
<th class="th-name">Activity</th>
<th class="th-points">Points</th>
<th class="th-options">Options</th>
</tr>
{#each period.goals as goal (goal.id)}
<tr>
<td>
<div class="icon"><ActivityIcon name={(activityMap[goal.activityId] || {name: "(Unknown)"}).icon} /></div>
<div class="name">{(activityMap[goal.activityId] || {name: "(Unknown)"}).name}</div>
</td>
<td><PointsBar value={period.scores[goal.id]} goal={goal.pointCount} /></td>
<td class="td-options">
<Link on:click={() => openGoalModal("periodgoal.remove", period.id, goal.id)}>Delete</Link>
</td>
</tr>
{/each}
</table>
<table>
<tr>
<th class="th-date">Date</th>
<th class="th-log">Goal</th>
<th class="th-log">Sub-Activity</th>
<th class="th-points2">Amount</th>
<th class="th-points2">Points</th>
<th class="th-options">Options</th>
</tr>
{#each period.table as row (row.log.id)}
<tr>
<td>{dateStr(row.log.date)}</td>
<td class:dark={row.activity.name.startsWith("(")}>
<div class="icon"><ActivityIcon name={row.activity.icon} /></div>
<div class="name">{row.activity.name} {row.subActivity.name}</div>
</td>
<td class:dark={row.subGoal.name.startsWith("(")}>{row.subGoal.name}</td>
<td>{row.log.amount} {pluralize(row.subActivity.unitName, row.log.amount)}</td>
<td>
<Link on:click={() => openLogModal("periodlog.info", period.id, row.log.id)}>{row.log.score.total}</Link>
</td>
<td class="td-options">
<Link on:click={() => openLogModal("periodlog.remove", period.id, row.log.id)}>Delete</Link>
</td>
</tr>
{/each}
</table>
<Link on:click={() => modal.open("period.delete", {period: getPeriod(period.id)})}>Delete Period</Link>
</td>
</Table>
<GoalTable
activities={$stufflog.activities}
period={period}
on:option={(e) => modal.open(e.detail.name, e.detail)}
/>
<LogTable
activities={$stufflog.activities}
items={$items}
period={period}
on:option={(e) => modal.open(e.detail.name, e.detail)}
/>
</Boi>
{/each}
<AddBoi on:click={() => modal.open("period.create")}>Period</AddBoi>
</div>
<style>
@@ -120,49 +59,4 @@
max-width: 90%;
margin: auto;
}
table {
width: 100%;
padding: 0.5em 0;
}
table th, table td {
padding: 0em 1ch;
}
table th {
text-align: left;
font-size: 0.75em;
}
table th.th-name {
width: 25%;
}
table th.th-date {
width: 25%;
}
table th.th-points {
width: 62.5%;
}
table th.th-options, table td.td-options {
width: 12.5%;
text-align: right;
}
table th.th-log {
width: calc(37.5%/2);
}
table th.th-points2 {
width: 12.5%;
}
table td.dark {
color: #666;
}
table td div.icon {
position: relative;
top: 0.175em;
display: inline-block;
}
table td div.name {
display: inline-block;
}
</style>
+58
View File
@@ -0,0 +1,58 @@
import { writable } from "svelte/store";
import slApi from "../api/stufflog";
import stufflogStore from "./stufflog";
function createItemStore() {
const {set, update, subscribe} = writable([])
return {
subscribe,
listItems() {
return slApi.listItems().then(items => {
set(items)
console.log(items)
return items
})
},
findItem(id) {
return slApi.getItem(id).then(item => {
update(s => replaceItem(s, item))
return item
})
},
createItem(item) {
return slApi.postItem(item).then(item => {
update(s => replaceItem(s, item))
return item
})
},
updateItem(id, ...updates) {
return slApi.updateItem(id, ...updates).then(item => {
update(s => replaceItem(s, item))
return item
})
},
deleteItem(id) {
return slApi.deleteItem(id).then(item => {
update(s => s.items.filter(i => i.id !== item.id))
return item
})
},
}
}
function replaceItem(s, item) {
return ([
...s.items.filter(i => i.id !== item.id),
item
].sort((a, b) => a.name.localeCompare(b.name)))
}
export default createItemStore();
+1
View File
@@ -7,6 +7,7 @@ function createModalStore() {
subscribe,
open(name, data = {}) {
console.log("Opening modal", name, data)
set({name, data});
},
-23
View File
@@ -18,25 +18,6 @@ function calculateTotalScore(period) {
)}), {})
}
function generateLogTable(activities, period) {
const newTable = [];
for (const log of period.logs) {
const goal = period.goals.find(g => g.id === log.goalId) || UNKNOWN_GOAL;
const subGoal = log.subGoalId
? goal.subGoals.find(s => s.id === log.subGoalId) || UNKNOWN_SUB_GOAL
: NO_SUB_GOAL;
const activity = activities.find(a => a.id === goal.activityId) || UNKNOWN_ACTIVITY;
const subActivity = activity.subActivities.find(s => s.id === log.subActivityId) || UNKNOWN_SUB_ACTIVITY;
newTable.push({log, goal, subGoal, activity, subActivity})
}
newTable.sort((a, b) => a.log.date - b.log.date);
return newTable;
}
function replaceActivity(d, activity) {
const data = {
...d,
@@ -49,7 +30,6 @@ function replaceActivity(d, activity) {
data.periods = data.periods.map(p => ({
...p,
scores: calculateTotalScore(p),
table: generateLogTable(data.activities, p),
}));
return data;
@@ -63,7 +43,6 @@ function replacePeriod(d, period) {
{
...period,
scores: calculateTotalScore(period),
table: generateLogTable(d.activities, period),
},
].sort((a,b) => b.from - a.from),
};
@@ -100,7 +79,6 @@ function createStufflogStore() {
periods: d.periods.map(p => ({
...p,
scores: calculateTotalScore(p),
table: generateLogTable(activities, p),
})),
}));
},
@@ -117,7 +95,6 @@ function createStufflogStore() {
periods: periods.sort((a,b) => b.from - a.from).map(p => ({
...p,
scores: calculateTotalScore(p),
table: generateLogTable(d.activities, p),
})),
}));
},