First commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
public/bundle.*
|
||||
yarn.lock
|
||||
@@ -0,0 +1,64 @@
|
||||
# svelte app
|
||||
|
||||
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template-webpack.
|
||||
|
||||
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
|
||||
|
||||
```bash
|
||||
npx degit sveltejs/template-webpack svelte-app
|
||||
cd svelte-app
|
||||
```
|
||||
|
||||
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
|
||||
|
||||
|
||||
## Get started
|
||||
|
||||
Install the dependencies...
|
||||
|
||||
```bash
|
||||
cd svelte-app
|
||||
npm install
|
||||
```
|
||||
|
||||
...then start webpack:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Navigate to [localhost:8080](http://localhost:8080). You should see your app running. Edit a component file in `src`, save it, and the page should reload with your changes.
|
||||
|
||||
|
||||
## Deploying to the web
|
||||
|
||||
### With [now](https://zeit.co/now)
|
||||
|
||||
Install `now` if you haven't already:
|
||||
|
||||
```bash
|
||||
npm install -g now
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
now
|
||||
```
|
||||
|
||||
As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon.
|
||||
|
||||
### With [surge](https://surge.sh/)
|
||||
|
||||
Install `surge` if you haven't already:
|
||||
|
||||
```bash
|
||||
npm install -g surge
|
||||
```
|
||||
|
||||
Then, from within your project folder:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
surge public
|
||||
```
|
||||
Generated
+6465
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "stufflog-svelte-ui",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
||||
"capitalize": "^2.0.1",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^2.1.1",
|
||||
"fa-svelte": "^3.1.0",
|
||||
"html-minifier": "^4.0.0",
|
||||
"ky": "^0.17.0",
|
||||
"mini-css-extract-plugin": "^0.6.0",
|
||||
"minify-html-webpack-plugin": "0.0.5",
|
||||
"pluralize": "^8.0.0",
|
||||
"serve": "^11.0.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"svelte": "^3.0.0",
|
||||
"svelte-loader": "2.13.3",
|
||||
"svelte-routing": "^1.4.0",
|
||||
"webpack": "^4.30.0",
|
||||
"webpack-cli": "^3.3.0",
|
||||
"webpack-dev-server": "^3.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"dev": "webpack-dev-server --content-base public"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,63 @@
|
||||
html, body {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(0,100,200);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: rgb(0,80,160);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input, button, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
padding: 0.4em;
|
||||
margin: 0 0 0.5em 0;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f4f4f4;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
border-color: #666;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<title>Stufflog</title>
|
||||
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
<link rel="stylesheet" href="/global.css">
|
||||
<link rel="stylesheet" href="/bundle.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script>
|
||||
import { Router, Route } from "svelte-routing";
|
||||
|
||||
import LogPage from "./routes/LogPage";
|
||||
import ActivitiesPage from "./routes/ActivitiesPage";
|
||||
|
||||
import LoginModal from "./modals/LoginModal";
|
||||
import CreateActivityModal from "./modals/CreateActivityModal";
|
||||
import CreatePeriodModal from "./modals/CreatePeriodModal";
|
||||
import AddPeriodGoalModal from "./modals/AddPeriodGoalModal"
|
||||
import AddPeriodLogModal from "./modals/AddPeriodLogModal"
|
||||
import AddSubActivityModal from "./modals/AddSubActivityModal";
|
||||
import EditPeriodModal from "./modals/EditPeriodModal";
|
||||
import DeletePeriodModal from "./modals/DeletePeriodModal";
|
||||
import EditActivityModal from "./modals/EditActivityModal";
|
||||
import EditSubActivityModal from "./modals/EditSubActivityModal";
|
||||
import RemoveSubActivityModal from "./modals/RemoveSubActivityModal";
|
||||
import RemovePeriodGoalModal from "./modals/RemovePeriodGoalModal";
|
||||
import RemovePeriodLogModal from "./modals/RemovePeriodLogModal";
|
||||
import InfoPeriodLogModal from "./modals/InfoPeriodLogModal";
|
||||
import DeleteActivityModal from "./modals/DeleteActivityModal";
|
||||
|
||||
import Modal from "./hooks/Modal";
|
||||
|
||||
import Menu from "./components/Menu";
|
||||
import MenuItem from "./components/MenuItem";
|
||||
|
||||
import LoginCheck from "./LoginCheck";
|
||||
|
||||
import auth from "./stores/auth";
|
||||
</script>
|
||||
|
||||
<Menu>
|
||||
<MenuItem href="/">Stufflog</MenuItem><MenuItem href="/activities/">Activities</MenuItem>
|
||||
|
||||
<div slot="right">
|
||||
{#if ($auth.checked && $auth.user !== null)}
|
||||
<MenuItem on:click={() => auth.logout()}>Logout [{$auth.user.name}]</MenuItem>
|
||||
{/if}
|
||||
</div>
|
||||
</Menu>
|
||||
|
||||
<Router>
|
||||
<Route path="/" component={LogPage} />
|
||||
<Route path="/activities/" component={ActivitiesPage} />
|
||||
</Router>
|
||||
|
||||
<Modal name="login" component={LoginModal} />
|
||||
<Modal name="period.create" component={CreatePeriodModal} />
|
||||
<Modal name="period.edit" component={EditPeriodModal} />
|
||||
<Modal name="period.delete" component={DeletePeriodModal} />
|
||||
<Modal name="activity.create" component={CreateActivityModal} />
|
||||
<Modal name="activity.edit" component={EditActivityModal} />
|
||||
<Modal name="activity.delete" component={DeleteActivityModal} />
|
||||
<Modal name="subactivity.add" component={AddSubActivityModal} />
|
||||
<Modal name="subactivity.edit" component={EditSubActivityModal} />
|
||||
<Modal name="subactivity.remove" component={RemoveSubActivityModal} />
|
||||
<Modal name="periodgoal.add" component={AddPeriodGoalModal} />
|
||||
<Modal name="periodgoal.remove" component={RemovePeriodGoalModal} />
|
||||
<Modal name="periodlog.add" component={AddPeriodLogModal} />
|
||||
<Modal name="periodlog.remove" component={RemovePeriodLogModal} />
|
||||
<Modal name="periodlog.info" component={InfoPeriodLogModal} />
|
||||
|
||||
<LoginCheck />
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import auth from "./stores/auth";
|
||||
import stufflog from "./stores/stufflog";
|
||||
import modal from "./stores/modal";
|
||||
|
||||
export let url = "";
|
||||
|
||||
let initialLoadDone = false;
|
||||
|
||||
auth.check();
|
||||
|
||||
$: {
|
||||
if ($auth.user !== null && !initialLoadDone) {
|
||||
initialLoadDone = true;
|
||||
stufflog.listActivities();
|
||||
stufflog.listPeriods();
|
||||
} else if ($auth.user === null && $auth.checked) {
|
||||
initialLoadDone = false;
|
||||
modal.open("login");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,139 @@
|
||||
import ky from "ky";
|
||||
import Activity, {ActivityUpdate} from "../models/activity";
|
||||
import Period, {PeriodUpdate} from "../models/period";
|
||||
|
||||
function json(data) {
|
||||
return {
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
}
|
||||
}
|
||||
|
||||
const k = ky.create({
|
||||
credentials: "include",
|
||||
})
|
||||
|
||||
export class StuffLogAPI {
|
||||
async checkSession() {
|
||||
return await k.get("/api/user/").json();
|
||||
}
|
||||
async login(username, password) {
|
||||
return await k.post("/api/user/login", json({username,password})).json();
|
||||
}
|
||||
async register(username, password) {
|
||||
return await k.post("/api/user/register", json({username,password})).json();
|
||||
}
|
||||
async logout() {
|
||||
return await k.post("/api/user/logout").json();
|
||||
}
|
||||
|
||||
/**
|
||||
* List activities
|
||||
*
|
||||
* @returns {Activity[]}
|
||||
*/
|
||||
async listActivities() {
|
||||
const data = await k.get("/api/activity/").json();
|
||||
return data.activities.map(d => new Activity(d));
|
||||
}
|
||||
/**
|
||||
* List periods
|
||||
*
|
||||
* @returns {{periods: Period[], activities: Activity[]}}
|
||||
*/
|
||||
async listPeriods() {
|
||||
const data = await k.get("/api/period/").json();
|
||||
|
||||
return data.periods.map(d => new Period(d));
|
||||
}
|
||||
/**
|
||||
* Find activity
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Activity}
|
||||
*/
|
||||
async findActivity(id) {
|
||||
const data = await k.get(`/api/activity/${id}`).json();
|
||||
return new Activity(data.activity);
|
||||
}
|
||||
/**
|
||||
* Find period
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Period}
|
||||
*/
|
||||
async findPeriod(id) {
|
||||
const data = await k.get(`/api/period/${id}`).json();
|
||||
return new Period(data.period);
|
||||
}
|
||||
/**
|
||||
* Create a new activity
|
||||
*
|
||||
* @param {Activity} activity
|
||||
* @returns {Activity}
|
||||
*/
|
||||
async postActivity(activity) {
|
||||
const data = await k.post(`/api/activity/`, json(activity)).json();
|
||||
return new Activity(data.activity);
|
||||
}
|
||||
/**
|
||||
* Create a new period
|
||||
*
|
||||
* @param {Period} period
|
||||
* @returns {Period}
|
||||
*/
|
||||
async postPeriod(period) {
|
||||
const data = await k.post(`/api/period/`, json(period)).json();
|
||||
return new Period(data.period);
|
||||
}
|
||||
/**
|
||||
* Update an activity
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {ActivityUpdate[]} updates
|
||||
* @returns {Activity}
|
||||
*/
|
||||
async patchActivity(id, ...updates) {
|
||||
const data = await k.patch(`/api/activity/${id}`, json(updates)).json();
|
||||
return new Activity(data.activity);
|
||||
}
|
||||
/**
|
||||
* Update an period
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {PeriodUpdate[]} updates
|
||||
* @returns {Period}
|
||||
*/
|
||||
async patchPeriod(id, ...updates) {
|
||||
const data = await k.patch(`/api/period/${id}`, json(updates)).json();
|
||||
return new Period(data.period);
|
||||
}
|
||||
/**
|
||||
* Update an activity
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {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}
|
||||
*/
|
||||
async deletePeriod(id) {
|
||||
const data = await k.delete(`/api/period/${id}`).json();
|
||||
return new Period(data.period);
|
||||
}
|
||||
}
|
||||
|
||||
const slApi = new StuffLogAPI();
|
||||
|
||||
export default slApi;
|
||||
@@ -0,0 +1,45 @@
|
||||
<script context="module">
|
||||
import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus";
|
||||
import { faCubes } from "@fortawesome/free-solid-svg-icons/faCubes";
|
||||
import { faBook } from "@fortawesome/free-solid-svg-icons/faBook";
|
||||
import { faBookDead } from "@fortawesome/free-solid-svg-icons/faBookDead";
|
||||
import { faPen } from "@fortawesome/free-solid-svg-icons/faPen";
|
||||
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 { 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";
|
||||
|
||||
const icons = {
|
||||
"plus": faPlus,
|
||||
"cubes": faCubes,
|
||||
"book": faBook,
|
||||
"book_dead": faBookDead,
|
||||
"pen": faPen,
|
||||
"dice_d20": faDiceD20,
|
||||
"dice_d6": faDiceD6,
|
||||
"dungeon": faDungeon,
|
||||
"gamepad": faGamepad,
|
||||
"language": faLanguage,
|
||||
"code": faCode,
|
||||
"code_branch": faCodeBranch,
|
||||
"guitar": faGuitar,
|
||||
};
|
||||
|
||||
export const iconNames = Object.keys(icons);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Icon from "fa-svelte"
|
||||
|
||||
export let name = "";
|
||||
</script>
|
||||
|
||||
<Icon class="activity-icon" icon={icons[name] || faQuestion} />
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import ActivityIcon, {iconNames} from "./ActivityIcon.svelte";
|
||||
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="icon-select">
|
||||
{#each iconNames as iconName (iconName)}
|
||||
<div class="icon-item" class:selected={value===iconName} on:click={() => value = iconName}>
|
||||
<ActivityIcon 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-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);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script>
|
||||
import Icon from 'fa-svelte'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus'
|
||||
|
||||
export let header = "";
|
||||
export let icon = null;
|
||||
</script>
|
||||
|
||||
<div on:click class="addboi">
|
||||
<Icon class="addboi-icon" icon={faPlus} />
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.addboi {
|
||||
margin: 0.5em 0 4em 0;
|
||||
padding: 1em 0;
|
||||
text-align: center;
|
||||
border: 4px dotted;
|
||||
|
||||
color: #333;
|
||||
font-size: 2em;
|
||||
font-weight: 100;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
transition: 250ms;
|
||||
}
|
||||
|
||||
div.addboi:hover {
|
||||
color: #777;
|
||||
|
||||
transition: 250ms;
|
||||
}
|
||||
|
||||
div.addboi > :global(.addboi-icon) {
|
||||
position: relative;
|
||||
top: 0.125em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script>
|
||||
import ActivityIcon from "./ActivityIcon"
|
||||
|
||||
export let header = "";
|
||||
export let icon = null;
|
||||
</script>
|
||||
|
||||
<div class="boi">
|
||||
<div class="boi-header">
|
||||
{#if (icon !== null)}<ActivityIcon name={icon} />{/if}{header}
|
||||
</div>
|
||||
<div class="boi-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.boi {
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
|
||||
background-color: #222;
|
||||
margin: 1em 0;
|
||||
|
||||
box-shadow: 0.1em 0.1em 0.05em #080808;
|
||||
}
|
||||
|
||||
div.boi > div.boi-header {
|
||||
background-color: #292929;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
font-size: 1.25em;
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
|
||||
div.boi > div.boi-header > :global(.activity-icon) {
|
||||
position: relative;
|
||||
top: 0.125em;
|
||||
margin-right: 0.5ch;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<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,16 @@
|
||||
<span on:click><slot></slot></span>
|
||||
|
||||
<style>
|
||||
span {
|
||||
color: #777;
|
||||
cursor: pointer;
|
||||
|
||||
transition: 250ms;
|
||||
}
|
||||
span:hover {
|
||||
color: #FFF;
|
||||
text-decoration: underline;
|
||||
|
||||
transition: 250ms;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="menu">
|
||||
<div class="right">
|
||||
<slot name="right" />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.menu {
|
||||
background-color: #222;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script>
|
||||
import { link } from "svelte-routing";
|
||||
|
||||
export let href = "/";
|
||||
</script>
|
||||
|
||||
<a class="menu-item" href={href} on:click use:link><slot/></a>
|
||||
|
||||
<style>
|
||||
a.menu-item {
|
||||
display: inline-block;
|
||||
background-color: #222;
|
||||
margin: 0;
|
||||
padding: 0.25em 1ch;
|
||||
|
||||
color: #777;
|
||||
|
||||
transition: 250ms;
|
||||
}
|
||||
|
||||
a.menu-item:hover {
|
||||
background-color: #111;
|
||||
color: #ccc;
|
||||
text-decoration: none;
|
||||
|
||||
transition: 250ms;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let title = "";
|
||||
export let wide = false;
|
||||
export let error = null;
|
||||
export let closable = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<div class="modal-background" on:click|self={() => closable ? dispatch("close") : null}>
|
||||
<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|self={() => dispatch("close")}>X</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<hr />
|
||||
{#if (error != null)}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
<div class="body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 {
|
||||
background: #222;
|
||||
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;
|
||||
}
|
||||
|
||||
div.modal :global(input), div.modal :global(select) {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
background: #222;
|
||||
color: #777;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
div.modal :global(select) {
|
||||
padding-left: 0.5ch;
|
||||
}
|
||||
|
||||
div.modal :global(input:last-of-type) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
div.modal :global(input.nolast) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
div.modal :global(input:focus), div.modal :global(select: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,117 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let value;
|
||||
export let goal;
|
||||
|
||||
let smoothValue = value;
|
||||
let bars = [];
|
||||
let timeout = null;
|
||||
|
||||
function update(value, goal) {
|
||||
const bars = [];
|
||||
let gold = (goal / 1000);
|
||||
let red = gold + 3;
|
||||
|
||||
for (let i = 0;; ++i) {
|
||||
let v = value - (i * 1000);
|
||||
if (v > 1000) {
|
||||
v = 1000;
|
||||
} else if (v < 0) {
|
||||
v = 0;
|
||||
}
|
||||
|
||||
if (i >= red && v === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
bars.push({
|
||||
green: i < gold,
|
||||
gold: i >= gold && i < red,
|
||||
red: i >= red,
|
||||
value: v,
|
||||
full: v === 1000,
|
||||
})
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
return () => clearTimeout(timeout);
|
||||
})
|
||||
|
||||
$: bars = update(smoothValue, goal);
|
||||
|
||||
$: if (smoothValue !== value && timeout === null) {
|
||||
timeout = setInterval(() => {
|
||||
if (smoothValue < value) {
|
||||
smoothValue += 100;
|
||||
if (smoothValue > value) {
|
||||
smoothValue = value;
|
||||
}
|
||||
} else if (smoothValue > value) {
|
||||
smoothValue -= 100;
|
||||
if (smoothValue < value) {
|
||||
smoothValue = value;
|
||||
}
|
||||
} else {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
}, 1000/30);
|
||||
}
|
||||
</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="content" style={`height: ${bar.value / 10}%; top: ${(1000 - bar.value) / 10}%`}>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
div.bar {
|
||||
display: inline-block;
|
||||
background: #111;
|
||||
width: 1.5ch;
|
||||
height: 1em;
|
||||
margin-right: 0.5ch;
|
||||
}
|
||||
|
||||
div.bar > div.content {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.bar.green {
|
||||
border: 0.1px solid #4da069;
|
||||
}
|
||||
div.bar.green > div.content {
|
||||
background-color: #4da069;
|
||||
}
|
||||
div.bar.green.full > div.content {
|
||||
background-color: #45fc82;
|
||||
box-shadow: 0px 0px 2px #45fc82;
|
||||
}
|
||||
div.bar.gold {
|
||||
border: 0.1px solid #9b7a1f;
|
||||
}
|
||||
div.bar.gold > div.content {
|
||||
background-color: #9b7a1f;
|
||||
}
|
||||
div.bar.gold.full > div.content {
|
||||
background-color: #ffc936;
|
||||
box-shadow: 0px 0px 2px #ffc936;
|
||||
}
|
||||
div.bar.red {
|
||||
border: 0.1px solid #880000;
|
||||
}
|
||||
div.bar.red > div.content {
|
||||
background-color: #880000;
|
||||
}
|
||||
div.bar.red.full > div.content {
|
||||
background-color: #bb0000;
|
||||
box-shadow: 0px 0px 2px #ff0000;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script>
|
||||
export let label = "Label";
|
||||
export let value = "";
|
||||
</script>
|
||||
|
||||
<div class="property">
|
||||
<div class="property-label">{label}:</div>
|
||||
<div class="property-value"><slot>{value}</slot></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.property {
|
||||
padding: 0.5em 1ch;
|
||||
}
|
||||
|
||||
div.property-label {
|
||||
font-size: 0.75em;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
div.property-value {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
:global(form) div.property:last-of-type {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="row"><slot></slot></div>
|
||||
|
||||
<style>
|
||||
div.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script>
|
||||
export let name;
|
||||
export let value;
|
||||
</script>
|
||||
|
||||
<div class="subgoal-input">
|
||||
<div><input type="text" bind:value={name} /></div>
|
||||
<div><input type="text" bind:value={value} /></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.subgoal-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
div.subgoal-input div {
|
||||
line-height: 1em;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
div.subgoal div {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
|
||||
padding: 0.5em;
|
||||
}
|
||||
div.subgoal-input div:first-of-type {
|
||||
width: 75%;
|
||||
}
|
||||
div.subgoal-input div:last-of-type {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
div.subgoal-input input {
|
||||
margin-bottom: 0 !important;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
div.subgoal-input div:first-of-type input {
|
||||
border-right: 1px solid #333;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script>
|
||||
import modal from "../stores/modal";
|
||||
|
||||
export let component;
|
||||
export let name;
|
||||
</script>
|
||||
|
||||
{#if (name === $modal.name)}
|
||||
<svelte:component this={component} {...$modal.data} />
|
||||
{/if}
|
||||
@@ -0,0 +1,10 @@
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
props: {},
|
||||
});
|
||||
|
||||
window.app = app;
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,116 @@
|
||||
<script>
|
||||
import pluralize from "pluralize";
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
import SubGoalInput from "../components/SubGoalInput";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
export let period = {};
|
||||
|
||||
let error = null;
|
||||
let activityId = "";
|
||||
let pointCount = "1000";
|
||||
let subGoals = [{name:"",multiplier:"1.0"}];
|
||||
|
||||
function addPeriodGoal() {
|
||||
error = null;
|
||||
|
||||
const parsedPointCount = parseInt(pointCount)
|
||||
if (Number.isNaN(parsedPointCount)) {
|
||||
error = "Point count must be a number.";
|
||||
return
|
||||
}
|
||||
if (parsedPointCount <= 0 || (parsedPointCount % 1000) !== 0) {
|
||||
error = "Point must be a positive non-zero multiple of 1000.";
|
||||
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(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`Add ${period.name} Goal`} error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => addPeriodGoal()}>
|
||||
<label>Activity</label>
|
||||
<select bind:value={activityId}>
|
||||
{#each $stufflog.activities as activity (activity.id)}
|
||||
<option value={activity.id}>{activity.name}</option>
|
||||
{/each}
|
||||
</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
|
||||
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.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Add Goal</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
|
||||
<style>
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script>
|
||||
import pluralize from "pluralize";
|
||||
import capitalize from "capitalize";
|
||||
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
import SubGoalInput from "../components/SubGoalInput";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
import dateStr from "../utils/dateStr";
|
||||
|
||||
export let period = {};
|
||||
|
||||
let error = null;
|
||||
let goal = null;
|
||||
let activity = null;
|
||||
let subActivity = null;
|
||||
let dateInput = dateStr(period.from);
|
||||
let goalId = "";
|
||||
let subGoalId = "";
|
||||
let subActivityId = "";
|
||||
let amount = "200";
|
||||
let description = "";
|
||||
|
||||
function addPeriodLog() {
|
||||
error = null;
|
||||
const addLog = {
|
||||
date: new Date(`${dateInput} 12:00:00.000`),
|
||||
goalId: goalId,
|
||||
subGoalId: subGoalId,
|
||||
subActivityId: subActivityId,
|
||||
amount: parseInt(amount),
|
||||
description: description,
|
||||
}
|
||||
|
||||
stufflog.updatePeriod(period.id, {addLog}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
|
||||
$: goal = period.goals.find(g => g.id === goalId) || null;
|
||||
$: activity = $stufflog.activities.find(a => a.id === goal.activityId) || null;
|
||||
$: subActivity = activity.subActivities.find(sg => sg.id === subActivityId) || null;
|
||||
|
||||
$: if (period.goals.length > 0 && goalId === "") {
|
||||
goalId = period.goals[0].id;
|
||||
}
|
||||
$: if (activity != null && !activity.subActivities.find(s => s.id === subActivityId)) {
|
||||
if (activity.subActivities.length > 0) {
|
||||
subActivityId = activity.subActivities[0].id;
|
||||
} else {
|
||||
subActivityId = "";
|
||||
}
|
||||
}
|
||||
$: if (goal != null && !goal.subGoals.find(s => s.id === subGoalId)) {
|
||||
subGoalId = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`Add ${period.name} Log`} error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => addPeriodLog()}>
|
||||
<label>Date</label>
|
||||
<input type="date" bind:value={dateInput} />
|
||||
|
||||
<label>Goal</label>
|
||||
<select bind:value={goalId}>
|
||||
{#each period.goals as goal (goal.id)}
|
||||
<option value={goal.id}>{$stufflog.activities.find(a => a.id === goal.activityId).name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
{#if (activity != null)}
|
||||
<label>Sub-Activity</label>
|
||||
<select bind:value={subActivityId}>
|
||||
{#each activity.subActivities as subActivity (subActivity.id)}
|
||||
<option value={subActivity.id}>{subActivity.name}</option>
|
||||
{/each}
|
||||
</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} />
|
||||
{/if}
|
||||
|
||||
<label>Description</label>
|
||||
<input type="text" bind:value={description} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Add Goal</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script>
|
||||
import pluralize from "pluralize";
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
export let activity = {};
|
||||
|
||||
let error = null;
|
||||
let name = "";
|
||||
let unitName = "minute";
|
||||
let value = "1";
|
||||
|
||||
function addSubActivity() {
|
||||
value = parseFloat(value)
|
||||
if (Number.isNaN(value)) {
|
||||
error = "Invalid value, it should be a number."
|
||||
value = "1";
|
||||
return
|
||||
}
|
||||
|
||||
stufflog.updateActivity(activity.id, {addSub: {name, unitName, value}}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`Add ${activity.name} Sub-Activity`} error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => addSubActivity()}>
|
||||
<label>Name</label>
|
||||
<input type="text" bind:value={name} />
|
||||
<label>Unit</label>
|
||||
<select bind:value={unitName}>
|
||||
<option value="minute">minute</option>
|
||||
<option value="word">word</option>
|
||||
<option value="hour">hour</option>
|
||||
<option value="item">item</option>
|
||||
</select>
|
||||
<label>Value per {pluralize(unitName, 1)}</label>
|
||||
<input type="string" bind:value={value} />
|
||||
|
||||
<p>
|
||||
1000 points should be equivalent to about an hour of baseline activity. Beyond that, it's
|
||||
about incentivitzing the right thing.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Add Sub-Activity</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script>
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
import ActivityIconSelect from "../components/ActivityIconSelect";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
let error = null;
|
||||
let name = "";
|
||||
let icon = "cubes";
|
||||
let dailyBonus = 100;
|
||||
|
||||
function createActivity() {
|
||||
stufflog.createActivity({name, icon, dailyBonus}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title="Create Activity" error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => createActivity()}>
|
||||
<label>Name</label>
|
||||
<input type="text" bind:value={name} />
|
||||
<label>Icon</label>
|
||||
<ActivityIconSelect bind:value={icon} />
|
||||
<label>Daily Bonus</label>
|
||||
<input type="number" bind:value={dailyBonus} />
|
||||
<hr />
|
||||
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script>
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
import ActivityIconSelect from "../components/ActivityIconSelect";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
import dateStr from "../utils/dateStr";
|
||||
|
||||
let error = null;
|
||||
let name = "";
|
||||
let fromStr = dateStr(new Date())
|
||||
let toStr = dateStr(new Date(Date.now() + (86400000 * 6)))
|
||||
let tagsStr = "";
|
||||
|
||||
function createPeriod() {
|
||||
const from = new Date(`${fromStr} 00:00:00.000`);
|
||||
const to = new Date(`${toStr} 23:59:59.999`);
|
||||
const tags = tagsStr.split(",").map(t => t.trim()).filter(t => t.length > 0);
|
||||
|
||||
error = null;
|
||||
|
||||
if (Number.isNaN(from.getTime())) {
|
||||
error = "You must enter a from date.";
|
||||
return;
|
||||
}
|
||||
if (Number.isNaN(from.getTime())) {
|
||||
error = "You must enter a to date.";
|
||||
return;
|
||||
}
|
||||
|
||||
stufflog.createPeriod({name, from, to, tags}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title="Create Period" error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => createPeriod()}>
|
||||
<label>Name</label>
|
||||
<input type="text" bind:value={name} />
|
||||
<label>From</label>
|
||||
<input type="date" bind:value={fromStr} />
|
||||
<label>To</label>
|
||||
<input type="date" bind:value={toStr} />
|
||||
<label>Tags (Comma separated)</label>
|
||||
<input type="text" bind:value={tagsStr} />
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script>
|
||||
import pluralize from "pluralize";
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
export let activity = {};
|
||||
|
||||
let error = null;
|
||||
|
||||
function removeSubActivity() {
|
||||
stufflog.deleteActivity(activity.id).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`Remove ${activity.name}`} error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => removeSubActivity()}>
|
||||
<p>
|
||||
Are you sure you want to remove activity <b>{activity.name}</b>?
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Yes, do as I say</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script>
|
||||
import pluralize from "pluralize";
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
export let period = {};
|
||||
|
||||
let error = null;
|
||||
|
||||
function deletePeriod() {
|
||||
stufflog.deletePeriod(period.id).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`Remove ${period.name}`} error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => deletePeriod()}>
|
||||
<p>
|
||||
Are you sure you want to remove period <b>{period.name}</b>?
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Yes, do as I say</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
import ActivityIconSelect from "../components/ActivityIconSelect";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
export let activity = {}
|
||||
|
||||
let name = activity.name;
|
||||
let icon = activity.icon;
|
||||
let dailyBonus = activity.dailyBonus;
|
||||
let error = null;
|
||||
|
||||
function editActivity() {
|
||||
stufflog.updateActivity(activity.id, {setName: name, setIcon: icon, setDailyBonus: dailyBonus}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`Update ${activity.name}`} error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => editActivity()}>
|
||||
<label>Name</label>
|
||||
<input type="text" bind:value={name} />
|
||||
<label>Icon</label>
|
||||
<ActivityIconSelect bind:value={icon} />
|
||||
<label>Daily Bonus</label>
|
||||
<input type="number" bind:value={dailyBonus} />
|
||||
<hr />
|
||||
|
||||
<button type="submit">Edit Activity</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script>
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
import ActivityIconSelect from "../components/ActivityIconSelect";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
import dateStr from "../utils/dateStr";
|
||||
|
||||
export let period = {};
|
||||
|
||||
let error = null;
|
||||
let name = period.name;
|
||||
let fromStr = dateStr(period.from)
|
||||
let toStr = dateStr(period.to)
|
||||
|
||||
function editPeriod() {
|
||||
const from = new Date(`${fromStr} 00:00:00.000`);
|
||||
const to = new Date(`${toStr} 23:59:59.999`);
|
||||
|
||||
error = null;
|
||||
|
||||
if (Number.isNaN(from.getTime())) {
|
||||
error = "You must enter a from date.";
|
||||
return;
|
||||
}
|
||||
if (Number.isNaN(from.getTime())) {
|
||||
error = "You must enter a to date.";
|
||||
return;
|
||||
}
|
||||
|
||||
stufflog.updatePeriod(period.id, {setName: name, setFrom: from, setTo: to}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title="Edit Period" error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => editPeriod()}>
|
||||
<label>Name</label>
|
||||
<input type="text" bind:value={name} />
|
||||
<label>From</label>
|
||||
<input type="date" bind:value={fromStr} />
|
||||
<label>To</label>
|
||||
<input type="date" bind:value={toStr} />
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script>
|
||||
import pluralize from "pluralize";
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
export let activity = {};
|
||||
export let subActivity = {};
|
||||
|
||||
let error = null;
|
||||
let id = subActivity.id;
|
||||
let name = subActivity.name;
|
||||
let unitName = subActivity.unitName;
|
||||
let value = subActivity.value;
|
||||
|
||||
function editSubActivity() {
|
||||
value = parseFloat(value)
|
||||
if (Number.isNaN(value)) {
|
||||
error = "Value should be a number."
|
||||
value = subActivity.value;
|
||||
return
|
||||
}
|
||||
|
||||
stufflog.updateActivity(activity.id, {editSub: {id, name, unitName, value}}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`Edit ${activity.name} Sub-Activity`} error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => editSubActivity()}>
|
||||
<label>Name</label>
|
||||
<input type="text" bind:value={name} />
|
||||
<label>Unit</label>
|
||||
<select bind:value={unitName}>
|
||||
<option value="minute">minute</option>
|
||||
<option value="word">word</option>
|
||||
<option value="hour">hour</option>
|
||||
<option value="item">item</option>
|
||||
</select>
|
||||
<label>Value per {pluralize(unitName, 1)}</label>
|
||||
<input type="string" bind:value={value} />
|
||||
|
||||
<p>
|
||||
1000 points should be equivalent to about an hour of baseline activity. Beyond that, it's
|
||||
about incentivitzing the right thing.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Edit Sub-Activity</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import pluralize from "pluralize";
|
||||
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
import SubGoalInput from "../components/SubGoalInput";
|
||||
import Property from "../components/Property";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
import dateStr from "../utils/dateStr";
|
||||
|
||||
export let period = {};
|
||||
export let log = {};
|
||||
export let activity = {};
|
||||
export let subActivity = {};
|
||||
export let subGoal = null;
|
||||
|
||||
let activityPoints = 0;
|
||||
let subGoalBonus = 0;
|
||||
let roundingError = 0;
|
||||
|
||||
$: 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);
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`${dateStr(log.date)} - ${activity.name} ${subActivity.name}`} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => modal.close()}>
|
||||
{#if log.description !== ""}
|
||||
<Property label="Description">
|
||||
{log.description}
|
||||
</Property>
|
||||
{/if}
|
||||
|
||||
<Property label="Activity Points">
|
||||
{Math.floor(activityPoints + roundingError)} points
|
||||
({log.score.amount} {pluralize(subActivity.unitName, log.score.amount)})
|
||||
</Property>
|
||||
{#if subGoal.multiplier != null}
|
||||
<Property label="Sub-Goal">
|
||||
{Math.floor(subGoalBonus)} points
|
||||
({subGoal.name})
|
||||
</Property>
|
||||
{/if}
|
||||
{#if log.score.dailyBonus != 0}
|
||||
<Property label="Daily Bonus">
|
||||
{log.score.dailyBonus} points
|
||||
</Property>
|
||||
{/if}
|
||||
<Property label="Total">
|
||||
{log.score.total} points
|
||||
</Property>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Add Goal</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script>
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
|
||||
import auth from "../stores/auth";
|
||||
import modal from "../stores/modal";
|
||||
|
||||
function login(username, password) {
|
||||
auth.login(username, password).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = "Incorrect username or password."
|
||||
});
|
||||
}
|
||||
|
||||
function register(username, password) {
|
||||
auth.register(username, password).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = "Incorrect username or password."
|
||||
});
|
||||
}
|
||||
|
||||
let username = "";
|
||||
let password = "";
|
||||
let error = null;
|
||||
</script>
|
||||
|
||||
<ModalFrame title="Login" error={error}>
|
||||
<form on:submit|preventDefault={() => {}}>
|
||||
<label>Username</label>
|
||||
<input type="text" bind:value={username} />
|
||||
<label>Password</label>
|
||||
<input type="password" bind:value={password} />
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit" on:click={() => login(username, password)}>Login</button>
|
||||
<button type="submit" on:click={() => register(username, password)}>Register</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import pluralize from "pluralize";
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
export let period = {};
|
||||
export let goal = {};
|
||||
export let activity = {};
|
||||
let error = null;
|
||||
|
||||
function removePeriodLog() {
|
||||
stufflog.updatePeriod(period.id, {removeGoal: goal.id}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`Remove ${period.name} Goal`} error={error} closable on:close={() => modal.close()}>
|
||||
<form on:submit|preventDefault={() => removePeriodLog()}>
|
||||
<p>
|
||||
Are you sure you want to remove <b>{activity.name}</b> goal from
|
||||
period <b>{period.name}</b>?
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Yes, do as I say</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script>
|
||||
import pluralize from "pluralize";
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
import dateStr from "../utils/dateStr";
|
||||
|
||||
export let period = {};
|
||||
export let log = {};
|
||||
export let activity = {};
|
||||
|
||||
let error = null;
|
||||
|
||||
function removePeriodLog() {
|
||||
stufflog.updatePeriod(period.id, {removeLog: log.id}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalFrame title={`Remove ${period.name} Log`} error={error} closable on:close={() => modal.close()}>
|
||||
<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
|
||||
period <b>{period.name}</b>?
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Yes, do as I say</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import pluralize from "pluralize";
|
||||
import ModalFrame from "../components/ModalFrame";
|
||||
|
||||
import modal from "../stores/modal";
|
||||
import stufflog from "../stores/stufflog";
|
||||
|
||||
export let activity = {};
|
||||
export let subActivtiy = {};
|
||||
|
||||
let error = null;
|
||||
|
||||
function removeSubActivity() {
|
||||
stufflog.updateActivity(activity.id, {removeSub: subActivtiy.id}).then(() => {
|
||||
modal.close();
|
||||
}).catch(err => {
|
||||
error = err.message || err;
|
||||
console.warn(err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<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
|
||||
activity <b>{activity.name}</b>?
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
<button type="submit">Yes, do as I say</button>
|
||||
</form>
|
||||
</ModalFrame>
|
||||
Vendored
+61
@@ -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;
|
||||
}
|
||||
@@ -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."));
|
||||
}
|
||||
};
|
||||
Vendored
+128
@@ -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
|
||||
}
|
||||
@@ -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."));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
<script>
|
||||
import {get} from "svelte/store";
|
||||
|
||||
import pluralize from "pluralize";
|
||||
import Icon from 'fa-svelte'
|
||||
import { faCircle } from '@fortawesome/free-solid-svg-icons/faCircle'
|
||||
|
||||
import stufflog from "../stores/stufflog";
|
||||
import modal from "../stores/modal";
|
||||
|
||||
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";
|
||||
|
||||
function openModal(modalName, id, subId) {
|
||||
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})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
{#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>
|
||||
<tr>
|
||||
<th class="th-name">Sub-Activity</th>
|
||||
<th class="th-value">Value</th>
|
||||
<th class="th-options">Options</th>
|
||||
</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>
|
||||
</Boi>
|
||||
{:else}
|
||||
<div class="empty">No data.</div>
|
||||
{/each}
|
||||
<AddBoi on:click={() => modal.open("activity.create")}>Activity</AddBoi>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.page {
|
||||
width: 100ch;
|
||||
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>
|
||||
@@ -0,0 +1,168 @@
|
||||
<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 stufflog from "../stores/stufflog";
|
||||
import modal from "../stores/modal";
|
||||
|
||||
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 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">
|
||||
{#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>,
|
||||
{#if period.goals.length > 0}
|
||||
<Link on:click={() => openGoalModal("periodlog.add", 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>
|
||||
</Boi>
|
||||
{/each}
|
||||
<AddBoi on:click={() => modal.open("period.create")}>Period</AddBoi>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.page {
|
||||
width: 100ch;
|
||||
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>
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
@@ -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())}`;
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user