commit 190e1eb01d311159d119bc90ea0fb5c14693e02f Author: Gisle Aune Date: Tue Feb 4 18:46:10 2020 +0100 First commit diff --git a/.idea/dictionaries/gisle.xml b/.idea/dictionaries/gisle.xml new file mode 100644 index 0000000..4b5f22d --- /dev/null +++ b/.idea/dictionaries/gisle.xml @@ -0,0 +1,14 @@ + + + + gctx + msgpack + roleplay + serr + sessid + slerr + slerror + stufflog + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..28a804d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..818ba46 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/stufflog.iml b/.idea/stufflog.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/stufflog.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 0000000..97ad6d2 --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..290e987 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,768 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + User + bnAct + WithContext + Activity + an Period + bn + key required + duplicate value + PeriodLog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/svelte-ui/src/hooks/Modal.svelte b/svelte-ui/src/hooks/Modal.svelte new file mode 100644 index 0000000..f22e29e --- /dev/null +++ b/svelte-ui/src/hooks/Modal.svelte @@ -0,0 +1,10 @@ + + +{#if (name === $modal.name)} + +{/if} \ No newline at end of file diff --git a/svelte-ui/src/main.js b/svelte-ui/src/main.js new file mode 100644 index 0000000..2ca6295 --- /dev/null +++ b/svelte-ui/src/main.js @@ -0,0 +1,10 @@ +import App from './App.svelte'; + +const app = new App({ + target: document.body, + props: {}, +}); + +window.app = app; + +export default app; \ No newline at end of file diff --git a/svelte-ui/src/modals/AddPeriodGoalModal.svelte b/svelte-ui/src/modals/AddPeriodGoalModal.svelte new file mode 100644 index 0000000..24c3ab7 --- /dev/null +++ b/svelte-ui/src/modals/AddPeriodGoalModal.svelte @@ -0,0 +1,116 @@ + + + modal.close()}> +
addPeriodGoal()}> + + + + + + {#each subGoals as subGoal} + + {/each} + +

+ The amount of points must be in an increment of 1000, which should be + equivalent to about an hour of baseline activity. Subgoal values are + multipliers. 1.05 means that the 5% more points are added. 0.5 means + the points are halved. +

+ +
+ + + +
+ + \ No newline at end of file diff --git a/svelte-ui/src/modals/AddPeriodLogModal.svelte b/svelte-ui/src/modals/AddPeriodLogModal.svelte new file mode 100644 index 0000000..7312960 --- /dev/null +++ b/svelte-ui/src/modals/AddPeriodLogModal.svelte @@ -0,0 +1,111 @@ + + + modal.close()}> +
addPeriodLog()}> + + + + + + + {#if (activity != null)} + + + {/if} + + {#if (goal != null && goal.subGoals.length > 0)} + + + {/if} + + {#if (subActivity != null)} + + + {/if} + + + + +
+ + +
+
+ + \ No newline at end of file diff --git a/svelte-ui/src/modals/AddSubActivityModal.svelte b/svelte-ui/src/modals/AddSubActivityModal.svelte new file mode 100644 index 0000000..f734856 --- /dev/null +++ b/svelte-ui/src/modals/AddSubActivityModal.svelte @@ -0,0 +1,55 @@ + + + modal.close()}> +
addSubActivity()}> + + + + + + + +

+ 1000 points should be equivalent to about an hour of baseline activity. Beyond that, it's + about incentivitzing the right thing. +

+ +
+ + +
+
\ No newline at end of file diff --git a/svelte-ui/src/modals/CreateActivityModal.svelte b/svelte-ui/src/modals/CreateActivityModal.svelte new file mode 100644 index 0000000..8d3294d --- /dev/null +++ b/svelte-ui/src/modals/CreateActivityModal.svelte @@ -0,0 +1,35 @@ + + + modal.close()}> +
createActivity()}> + + + + + + +
+ + + +
\ No newline at end of file diff --git a/svelte-ui/src/modals/CreatePeriodModal.svelte b/svelte-ui/src/modals/CreatePeriodModal.svelte new file mode 100644 index 0000000..4559780 --- /dev/null +++ b/svelte-ui/src/modals/CreatePeriodModal.svelte @@ -0,0 +1,53 @@ + + + modal.close()}> +
createPeriod()}> + + + + + + + + + +
+
\ No newline at end of file diff --git a/svelte-ui/src/modals/DeleteActivityModal.svelte b/svelte-ui/src/modals/DeleteActivityModal.svelte new file mode 100644 index 0000000..7edbc83 --- /dev/null +++ b/svelte-ui/src/modals/DeleteActivityModal.svelte @@ -0,0 +1,32 @@ + + + modal.close()}> +
removeSubActivity()}> +

+ Are you sure you want to remove activity {activity.name}? +

+ +
+ + +
+
\ No newline at end of file diff --git a/svelte-ui/src/modals/DeletePeriodModal.svelte b/svelte-ui/src/modals/DeletePeriodModal.svelte new file mode 100644 index 0000000..506c6c3 --- /dev/null +++ b/svelte-ui/src/modals/DeletePeriodModal.svelte @@ -0,0 +1,32 @@ + + + modal.close()}> +
deletePeriod()}> +

+ Are you sure you want to remove period {period.name}? +

+ +
+ + +
+
\ No newline at end of file diff --git a/svelte-ui/src/modals/EditActivityModal.svelte b/svelte-ui/src/modals/EditActivityModal.svelte new file mode 100644 index 0000000..18c658c --- /dev/null +++ b/svelte-ui/src/modals/EditActivityModal.svelte @@ -0,0 +1,37 @@ + + + modal.close()}> +
editActivity()}> + + + + + + +
+ + + +
\ No newline at end of file diff --git a/svelte-ui/src/modals/EditPeriodModal.svelte b/svelte-ui/src/modals/EditPeriodModal.svelte new file mode 100644 index 0000000..cf0465f --- /dev/null +++ b/svelte-ui/src/modals/EditPeriodModal.svelte @@ -0,0 +1,51 @@ + + + modal.close()}> +
editPeriod()}> + + + + + + + +
+
\ No newline at end of file diff --git a/svelte-ui/src/modals/EditSubActivityModal.svelte b/svelte-ui/src/modals/EditSubActivityModal.svelte new file mode 100644 index 0000000..76a1163 --- /dev/null +++ b/svelte-ui/src/modals/EditSubActivityModal.svelte @@ -0,0 +1,57 @@ + + + modal.close()}> +
editSubActivity()}> + + + + + + + +

+ 1000 points should be equivalent to about an hour of baseline activity. Beyond that, it's + about incentivitzing the right thing. +

+ +
+ + +
+
\ No newline at end of file diff --git a/svelte-ui/src/modals/InfoPeriodLogModal.svelte b/svelte-ui/src/modals/InfoPeriodLogModal.svelte new file mode 100644 index 0000000..4f34c1e --- /dev/null +++ b/svelte-ui/src/modals/InfoPeriodLogModal.svelte @@ -0,0 +1,62 @@ + + + modal.close()}> +
modal.close()}> + {#if log.description !== ""} + + {log.description} + + {/if} + + + {Math.floor(activityPoints + roundingError)} points + ({log.score.amount} {pluralize(subActivity.unitName, log.score.amount)}) + + {#if subGoal.multiplier != null} + + {Math.floor(subGoalBonus)} points + ({subGoal.name}) + + {/if} + {#if log.score.dailyBonus != 0} + + {log.score.dailyBonus} points + + {/if} + + {log.score.total} points + + +
+ + +
+
+ + \ No newline at end of file diff --git a/svelte-ui/src/modals/LoginModal.svelte b/svelte-ui/src/modals/LoginModal.svelte new file mode 100644 index 0000000..a54be1a --- /dev/null +++ b/svelte-ui/src/modals/LoginModal.svelte @@ -0,0 +1,40 @@ + + + +
{}}> + + + + + +
+ + + +
+
\ No newline at end of file diff --git a/svelte-ui/src/modals/RemovePeriodGoalModal.svelte b/svelte-ui/src/modals/RemovePeriodGoalModal.svelte new file mode 100644 index 0000000..80dd160 --- /dev/null +++ b/svelte-ui/src/modals/RemovePeriodGoalModal.svelte @@ -0,0 +1,34 @@ + + + modal.close()}> +
removePeriodLog()}> +

+ Are you sure you want to remove {activity.name} goal from + period {period.name}? +

+ +
+ + +
+
\ No newline at end of file diff --git a/svelte-ui/src/modals/RemovePeriodLogModal.svelte b/svelte-ui/src/modals/RemovePeriodLogModal.svelte new file mode 100644 index 0000000..b507252 --- /dev/null +++ b/svelte-ui/src/modals/RemovePeriodLogModal.svelte @@ -0,0 +1,38 @@ + + + modal.close()}> +
removePeriodLog()}> +

+ Are you sure you want to remove log {log.description} + ({dateStr(log.date)}, {activity.name}) from + period {period.name}? +

+ +
+ + +
+
\ No newline at end of file diff --git a/svelte-ui/src/modals/RemoveSubActivityModal.svelte b/svelte-ui/src/modals/RemoveSubActivityModal.svelte new file mode 100644 index 0000000..63ef350 --- /dev/null +++ b/svelte-ui/src/modals/RemoveSubActivityModal.svelte @@ -0,0 +1,34 @@ + + + modal.close()}> +
removeSubActivity()}> +

+ Are you sure you want to remove sub-acitvity {subActivtiy.name || "(unnamed)"} from + activity {activity.name}? +

+ +
+ + +
+
\ No newline at end of file diff --git a/svelte-ui/src/models/activity.d.ts b/svelte-ui/src/models/activity.d.ts new file mode 100644 index 0000000..58ee895 --- /dev/null +++ b/svelte-ui/src/models/activity.d.ts @@ -0,0 +1,61 @@ +/** + * Data about an activity. + */ +export default class Activity { + /** ID of the activity, ignored on input */ + public id : string; + + /** The user ID the activity is associated with. */ + public userId : string; + + /** A name for the activity */ + public name : string; + + /** Icon to use for it */ + public icon : string; + + /** Bonus awarded once daily. This is to reward consistency. */ + public dailyBonus : number; + + /** Sub activities */ + public subActivites : SubActivtiy[]; +} + +/** + * An activity udpate for activities + */ +export class ActivityUpdate { + /** Add a sub activity */ + addSub? : SubActivtiy; + + /** Replace a sub activity */ + editSub? : SubActivtiy; + + /** Remove a sub activity by ID */ + removeSub? : string; + + /** Set activity name */ + setName? : string; + + /** Set activity icon */ + setIcon? : string; +} + +/** + * Sub-Activity of an activity. While an activity can be as general as "3D modeling", this + * is where the specific activiteis under that umbrella is. Practice, tutorial, or whatever fits + * the parent activity. + */ +export class SubActivtiy { + /** ID of the sub-activtiy, ignored on input */ + public id : string; + + /** Sub-activity name */ + public name : string; + + /** Measured unit, always plural (e.g. minutes) */ + public unitName : string; + + /** Points awarded per unit. */ + public value : number; +} \ No newline at end of file diff --git a/svelte-ui/src/models/activity.js b/svelte-ui/src/models/activity.js new file mode 100644 index 0000000..4669f13 --- /dev/null +++ b/svelte-ui/src/models/activity.js @@ -0,0 +1,25 @@ +export default class Activity { + constructor(data) { + this.id = data.id || null; + this.userId = data.userId || null; + this.name = data.name || ""; + this.icon = data.icon || ""; + this.dailyBonus = data.dailyBonus || 0; + this.subActivities = (data.subActivities || []).map(s => new SubActivtiy(s)); + } +} + +export class SubActivtiy { + constructor(data) { + this.id = data.id || null; + this.name = data.name || ""; + this.unitName = data.unitName || "minutes"; + this.value = data.value || null; + } +} + +export const ActivityUpdate = class ActivityUpdateDummyForTyping{ + constructor() { + console.warn(new Error("ActivityUpdate is a dummy, don't instantiate.")); + } +}; \ No newline at end of file diff --git a/svelte-ui/src/models/period.d.ts b/svelte-ui/src/models/period.d.ts new file mode 100644 index 0000000..9c6f669 --- /dev/null +++ b/svelte-ui/src/models/period.d.ts @@ -0,0 +1,128 @@ +/** + * Data about an time period. + */ +export default class Period { + /** The ID of the Period. */ + id : string; + + /** The ID of the owning user. */ + userId : string; + + /** The inclusive start time. */ + from : Date; + + /** The exclusive end date. */ + to : Date; + + /** The name of the time period, usually generated.*/ + name : string; + + /** + * Tags used to explain the goals being out of the ordinary. For example, + * if leaving for a vacation where hobbies are best put on hold. + */ + tags : string[]; + + /** The goals set for this time period. */ + goals : PeriodGoal[]; + + /** Logged activities during this time period. */ + logs : PeriodLog[]; +} + +/** + * A goal set during the period. + */ +export class PeriodGoal { + /** The ID of the PeriodGoal. */ + id : string + + /** The activity ID associated with the goal. */ + activityId : string + + /** The point count for the goal. 1000 is usually considered an hour's work. */ + pointCount : number + + /** Optional sub-goals. */ + subGoals : PeriodSubGoal[]; +} + +/** + * Optional sub-goals so that the user can award more points on important + * activities and not use up all the points on time-sinking guilty pleasures. + */ +export class PeriodSubGoal { + /** The ID of the PeriodSubGoal. */ + id : string; + + /** A short name of the sub goal. */ + name : string; + + /** The multiplier applied to the score. 1.10 = 10% better. */ + multiplier : number; +} + +/** + * A logged activity during this time period. + */ +export class PeriodLog { + /** The ID of the PeriodLog. */ + id : string; + + /** The ID of the PeriodLog. */ + date : Date; + + /** The ID of the sub-activity performed (see goal for activity id) */ + subActivityId : string; + + /** The ID of the goal. */ + goalId : string; + + /** The applicable sub goal, or `null` if none. */ + subGoalId : string; + + /** A description of the activity performed. */ + description : string; + + /** The amount of units done. */ + amount : number + + /** The calculated score and its breakdown. */ + score : PeriodLogScore +} + +/** + * The PeriodLogScore is a score for the log. This may not reflect the + * latest numbers. + */ +export class PeriodLogScore { + /** The amount of activity done (e.g number of words or minutes). */ + amount : number; + + /** The score that's worth. */ + activityScore : number; + + /** The subgoal multiplier in effect. */ + subGoalMultiplier : number; + + /** The daily bonus, or 0 if not applicable. */ + dailyBonus : number; + + /** The total score. */ + total : number; +} + +export interface PeriodUpdate { + setFrom?: Date + setTo?: Date + setName?: string + + addLog?: PeriodLog + removeLog?: string + addGoal?: PeriodGoal + replaceGoal?: PeriodGoal + removeGoal?: string + + addTag?: string + removeTag?: String +} \ No newline at end of file diff --git a/svelte-ui/src/models/period.js b/svelte-ui/src/models/period.js new file mode 100644 index 0000000..48a78ce --- /dev/null +++ b/svelte-ui/src/models/period.js @@ -0,0 +1,77 @@ +export default class Period { + constructor(data) { + this.id = data.id || null; + this.userId = data.userId || null; + this.from = new Date(data.from); + this.to = new Date(data.to); + this.name = data.name || ""; + + this.tags = data.tags || []; + this.goals = (data.goals || []).map(d => new PeriodGoal(d)); + this.logs = (data.logs || []).map(d => new PeriodLog(d)); + } + + goal(id) { + return this.goals.find(g => g.id === id); + } + + subGoal(goalId, id) { + const goal = this.goal(goalId); + if (goal == null) { + return null; + } + + return goal.subGoal(id); + } +} + +export class PeriodGoal { + constructor(data) { + this.id = data.id || null; + this.activityId = data.activityId || null; + this.pointCount = data.pointCount || 0; + + this.subGoals = (data.subGoals || []).map(d => new PeriodSubGoal(d)); + } + + subGoal(id) { + return this.subGoals.find(s => s.id === id); + } +} + +export class PeriodSubGoal { + constructor(data) { + this.id = data.id || null; + this.name = data.name || ""; + this.multiplier = data.multiplier || 1; + } +} + +export class PeriodLog { + constructor(data) { + this.id = data.id || null; + this.date = new Date(data.date || "0000-00-00T00:00:00Z"); + this.subActivityId = data.subActivityId || null; + this.goalId = data.goalId || null; + this.subGoalId = data.subGoalId || null; + this.description = data.description || ""; + this.amount = data.amount || 0; + this.score = (data.score ? new PeriodLogScore(data.score) : null); + } +} + +export class PeriodLogScore { + constructor(data) { + this.amount = data.amount; + this.activityScore = data.activityScore; + this.subGoalMultiplier = data.subGoalMultiplier; + this.dailyBonus = data.dailyBonus; + this.total = data.total; + } +} + +export const PeriodUpdate = class PeriodUpdateDummyForTyping{ + constructor() { + console.warn(new Error("PeriodUpdate is a dummy, don't instantiate.")); + } +}; \ No newline at end of file diff --git a/svelte-ui/src/routes/ActivitiesPage.svelte b/svelte-ui/src/routes/ActivitiesPage.svelte new file mode 100644 index 0000000..53f5f88 --- /dev/null +++ b/svelte-ui/src/routes/ActivitiesPage.svelte @@ -0,0 +1,91 @@ + + +
+ {#each $stufflog.activities as activity (activity.id)} + + + + + + + openModal("activity.edit", activity.id)}>Edit Activity, + openModal("subactivity.add", activity.id)}>Add Sub-Activity, + openModal("activity.delete", activity.id)}>Delete Activity + + + + + + + + + + {#each activity.subActivities as subActivtiy (subActivtiy.id)} + + + + + + {/each} +
Sub-ActivityValueOptions
{subActivtiy.name}{subActivtiy.value} per {pluralize(subActivtiy.unitName, 1)} + openModal("subactivity.edit", activity.id, subActivtiy.id)}>Edit, + openModal("subactivity.remove", activity.id, subActivtiy.id)}>Delete +
+
+ {:else} +
No data.
+ {/each} + modal.open("activity.create")}>Activity +
+ + \ No newline at end of file diff --git a/svelte-ui/src/routes/LogPage.svelte b/svelte-ui/src/routes/LogPage.svelte new file mode 100644 index 0000000..d8bfaa7 --- /dev/null +++ b/svelte-ui/src/routes/LogPage.svelte @@ -0,0 +1,168 @@ + + + + +
+ {#each $stufflog.periods as period (period.id)} + + + + + + + openGoalModal("period.edit", period.id)}>Edit Period, + openGoalModal("periodgoal.add", period.id)}>Add Goal, + {#if period.goals.length > 0} + openGoalModal("periodlog.add", period.id)}>Add Log, + {/if} + openGoalModal("period.delete", period.id)}>Delete Period + + + + + + + + + + {#each period.goals as goal (goal.id)} + + + + + + {/each} +
ActivityPointsOptions
+
+
{(activityMap[goal.activityId] || {name: "(Unknown)"}).name}
+
+ openGoalModal("periodgoal.remove", period.id, goal.id)}>Delete +
+ + + + + + + + + + {#each period.table as row (row.log.id)} + + + + + + + + + {/each} +
DateGoalSub-ActivityAmountPointsOptions
{dateStr(row.log.date)} +
+
{row.activity.name} {row.subActivity.name}
+
{row.subGoal.name}{row.log.amount} {pluralize(row.subActivity.unitName, row.log.amount)} + openLogModal("periodlog.info", period.id, row.log.id)}>{row.log.score.total} + + openLogModal("periodlog.remove", period.id, row.log.id)}>Delete +
+
+ {/each} + modal.open("period.create")}>Period +
+ + \ No newline at end of file diff --git a/svelte-ui/src/stores/auth.js b/svelte-ui/src/stores/auth.js new file mode 100644 index 0000000..aa5d9fd --- /dev/null +++ b/svelte-ui/src/stores/auth.js @@ -0,0 +1,49 @@ +import { writable } from "svelte/store"; + +import slApi from "../api/stufflog"; + +import stufflogStore from "./stufflog"; + +function createAuthStore() { + const {set, update, subscribe} = writable({checked: false, user: null}) + + return { + subscribe, + + async check() { + const data = await slApi.checkSession(); + + set({checked: true, user: data.user || null}); + return data; + }, + + async login(username, password) { + const data = await slApi.login(username, password); + + set({checked: true, user: data.user}); + return data; + }, + + async logout() { + const data = await slApi.logout(); + + stufflogStore.clearAll(); + + set({checked: true, user: data.user || null}); + return data; + }, + + async register(username, password) { + const data = await slApi.register(username, password); + + stufflogStore.clearAll(); + + set({checked: true, user: data.user}); + return data; + }, + } +} + +const session = createAuthStore(); + +export default session; \ No newline at end of file diff --git a/svelte-ui/src/stores/modal.js b/svelte-ui/src/stores/modal.js new file mode 100644 index 0000000..9795c70 --- /dev/null +++ b/svelte-ui/src/stores/modal.js @@ -0,0 +1,19 @@ +import { writable } from "svelte/store"; + +function createModalStore() { + const {set, subscribe} = writable({name: null, data: null}); + + return { + subscribe, + + open(name, data = {}) { + set({name, data}); + }, + + close() { + set({name: null, data: null}) + }, + } +} + +export default createModalStore(); \ No newline at end of file diff --git a/svelte-ui/src/stores/stufflog.js b/svelte-ui/src/stores/stufflog.js new file mode 100644 index 0000000..93c78dc --- /dev/null +++ b/svelte-ui/src/stores/stufflog.js @@ -0,0 +1,203 @@ +import { writable, derived } from "svelte/store"; + +import slApi from "../api/stufflog"; +import Activity, { ActivityUpdate } from "../models/activity"; +import Period, { PeriodUpdate } from "../models/period"; + +const UNKNOWN_GOAL = Object.freeze({name: "(Unknown)", subGoals: []}); +const NO_SUB_GOAL = Object.freeze({name: "(None)"}); +const UNKNOWN_SUB_GOAL = Object.freeze({name: "(Unknown)"}); +const UNKNOWN_ACTIVITY = Object.freeze({name: "(Unknown)", subActivities: []}); +const UNKNOWN_SUB_ACTIVITY = Object.freeze({name: "(Unknown)", unitName: "unit"}); + +function calculateTotalScore(period) { + return period.goals.reduce((o, g) => o = ({...o, [g.id]: ( + period.logs.filter(l => l.goalId === g.id) + .map(l => l.score.total) + .reduce((n, m) => n + m, 0) + )}), {}) +} + +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, + activities: [ + ...d.activities.filter(a => a.id !== activity.id), + activity, + ].sort((a,b) => a.name.localeCompare(b.name)), + }; + + data.periods = data.periods.map(p => ({ + ...p, + scores: calculateTotalScore(p), + table: generateLogTable(data.activities, p), + })); + + return data; +} + +function replacePeriod(d, period) { + return { + ...d, + periods: [ + ...d.periods.filter(a => a.id !== period.id), + { + ...period, + scores: calculateTotalScore(period), + table: generateLogTable(d.activities, period), + }, + ].sort((a,b) => b.from - a.from), + }; +} + +function deleteActivity(d, id) { + return { + ...d, + activities: d.activities.filter(a => a.id !== id), + }; +} + +function deletePeriod(d, id) { + return { + ...d, + periods: d.periods.filter(a => a.id !== id), + }; +} + +function createStufflogStore() { + /** @type {{activities: Activity[], periods: Period[], logTables: {}}} */ + const initialData = {activities: [], periods: [], logTables: {}}; + + const {set, update, subscribe} = writable(initialData); + + return { + subscribe, + + async listActivities() { + const activities = await slApi.listActivities(); + update(d => ({ + ...d, + activities: activities.sort((a,b) => a.name.localeCompare(b.name)), + periods: d.periods.map(p => ({ + ...p, + scores: calculateTotalScore(p), + table: generateLogTable(activities, p), + })), + })); + }, + + async loadActivity(id) { + const activity = await slApi.findActivity(id); + update(d => replaceActivity(d, activity)); + }, + + async listPeriods() { + const periods = await slApi.listPeriods(); + update(d => ({ + ...d, + periods: periods.sort((a,b) => b.from - a.from).map(p => ({ + ...p, + scores: calculateTotalScore(p), + table: generateLogTable(d.activities, p), + })), + })); + }, + + async loadPeriods(id) { + const period = await slApi.findPeriod(id); + update(d => replacePeriod(d, period)); + }, + + /** + * Create period + * + * @param {Period} input + */ + async createPeriod(input) { + const period = await slApi.postPeriod(input); + update(d => replacePeriod(d, period)); + }, + + /** + * Create an activity + * + * @param {Activity} input + */ + async createActivity(input) { + const activity = await slApi.postActivity(input); + update(d => replaceActivity(d, activity)) + }, + + /** + * Update activities + * + * @param {string} id + * @param {...ActivityUpdate} updates + */ + async updateActivity(id, ...updates) { + const activity = await slApi.patchActivity(id, ...updates); + update(d => replaceActivity(d, activity)); + }, + + /** + * Update periods + * + * @param {string} id + * @param {...PeriodUpdate} updates + */ + async updatePeriod(id, ...updates) { + const period = await slApi.patchPeriod(id, ...updates); + update(d => replacePeriod(d, period)); + }, + + /** + * Delete an activity + * + * @param {string} id + */ + async deleteActivity(id) { + const activity = await slApi.deleteActivity(id); + update(d => deleteActivity(d, activity.id)); + }, + + /** + * Delete a period + * + * @param {string} id + */ + async deletePeriod(id) { + const period = await slApi.deletePeriod(id); + update(d => deletePeriod(d, period.id)); + }, + + /** + * Clear all data. This does not do any API call. + */ + clearAll() { + set({activities: [], periods: []}); + }, + } +}; + +const stufflog = createStufflogStore(); + +export default stufflog; \ No newline at end of file diff --git a/svelte-ui/src/utils/dateStr.js b/svelte-ui/src/utils/dateStr.js new file mode 100644 index 0000000..d9856e7 --- /dev/null +++ b/svelte-ui/src/utils/dateStr.js @@ -0,0 +1,16 @@ +/** + * Get a `YYYY-MM-DD` string. + * + * @param {Date} date + */ +export default function dateStr(date) { + function pad(n) { + return n < 10 ? "0" + n : n.toString(); + } + + if (!(date instanceof Date)) { + date = new Date(date); + } + + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; +} \ No newline at end of file diff --git a/svelte-ui/webpack.config.js b/svelte-ui/webpack.config.js new file mode 100644 index 0000000..4c15ea9 --- /dev/null +++ b/svelte-ui/webpack.config.js @@ -0,0 +1,66 @@ +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); + +const path = require("path"); + +const mode = process.env.NODE_ENV || "development"; +const prod = mode === "production"; + +module.exports = { + entry: { + bundle: ["./src/main.js"] + }, + resolve: { + alias: { + svelte: path.resolve("node_modules", "svelte") + }, + extensions: [".mjs", ".js", ".svelte"], + mainFields: ["svelte", "browser", "module", "main"] + }, + output: { + path: __dirname + "/public", + filename: "[name].js", + chunkFilename: "[name].[id].js" + }, + module: { + rules: [ + { + test: /\.svelte$/, + use: { + loader: "svelte-loader", + options: { + emitCss: true, + hotReload: true + } + } + }, + { + test: /\.css$/, + use: [ + /** + * MiniCssExtractPlugin doesn"t support HMR. + * For developing, use "style-loader" instead. + * */ + prod ? MiniCssExtractPlugin.loader : "style-loader", + "css-loader" + ] + } + ] + }, + mode, + plugins: [ + new MiniCssExtractPlugin({ + filename: "[name].css" + }), + ], + devtool: prod ? false: "source-map", + devServer: { + historyApiFallback: true, + proxy: { + "/api": { + "changeOrigin": true, + "cookieDomainRewrite": "localhost", + "target": "http://localhost:8001", + }, + }, + }, +};