Gisle Aune
3 years ago
commit
f75f342a31
31 changed files with 9557 additions and 0 deletions
-
20.eslintrc.cjs
-
8.gitignore
-
1.npmrc
-
40README.md
-
8304package-lock.json
-
29package.json
-
10src/app.d.ts
-
22src/app.html
-
40src/lib/components/frontpage/ItemLink.svelte
-
34src/lib/components/frontpage/RequirementLink.svelte
-
20src/lib/components/frontpage/ScopeLink.svelte
-
121src/lib/components/frontpage/SprintLink.svelte
-
34src/lib/components/layout/Entry.svelte
-
40src/lib/components/layout/EntryAmounts.svelte
-
14src/lib/components/layout/EntryDescription.svelte
-
28src/lib/components/layout/EntryName.svelte
-
200src/lib/components/layout/EntryProgress.svelte
-
16src/lib/components/layout/EntryProgressRow.svelte
-
16src/lib/models/item.ts
-
36src/lib/models/project.ts
-
11src/lib/models/scope.ts
-
24src/lib/models/sprint.ts
-
52src/lib/models/stat.ts
-
25src/lib/models/status.ts
-
15src/lib/stores/currentTime.ts
-
10src/lib/utils/aggregate.ts
-
146src/routes/index.svelte
-
190src/routes/indexdata.json.ts
-
BINstatic/favicon.png
-
15svelte.config.js
-
36tsconfig.json
@ -0,0 +1,20 @@ |
|||
module.exports = { |
|||
root: true, |
|||
parser: '@typescript-eslint/parser', |
|||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], |
|||
plugins: ['svelte3', '@typescript-eslint'], |
|||
ignorePatterns: ['*.cjs'], |
|||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], |
|||
settings: { |
|||
'svelte3/typescript': () => require('typescript') |
|||
}, |
|||
parserOptions: { |
|||
sourceType: 'module', |
|||
ecmaVersion: 2020 |
|||
}, |
|||
env: { |
|||
browser: true, |
|||
es2017: true, |
|||
node: true |
|||
} |
|||
}; |
@ -0,0 +1,8 @@ |
|||
.DS_Store |
|||
node_modules |
|||
/build |
|||
/.svelte-kit |
|||
/package |
|||
.env |
|||
.env.* |
|||
!.env.example |
@ -0,0 +1 @@ |
|||
engine-strict=true |
@ -0,0 +1,40 @@ |
|||
# create-svelte |
|||
|
|||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). |
|||
|
|||
## Creating a project |
|||
|
|||
If you're seeing this, you've probably already done this step. Congrats! |
|||
|
|||
```bash |
|||
# create a new project in the current directory |
|||
npm init svelte@next |
|||
|
|||
# create a new project in my-app |
|||
npm init svelte@next my-app |
|||
``` |
|||
|
|||
> Note: the `@next` is temporary |
|||
|
|||
## Developing |
|||
|
|||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: |
|||
|
|||
```bash |
|||
npm run dev |
|||
|
|||
# or start the server and open the app in a new browser tab |
|||
npm run dev -- --open |
|||
``` |
|||
|
|||
## Building |
|||
|
|||
To create a production version of your app: |
|||
|
|||
```bash |
|||
npm run build |
|||
``` |
|||
|
|||
You can preview the production build with `npm run preview`. |
|||
|
|||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. |
8304
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "stufflog3", |
|||
"version": "0.0.1", |
|||
"scripts": { |
|||
"dev": "svelte-kit dev", |
|||
"build": "svelte-kit build", |
|||
"package": "svelte-kit package", |
|||
"preview": "svelte-kit preview", |
|||
"check": "svelte-check --tsconfig ./tsconfig.json", |
|||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", |
|||
"lint": "eslint --ignore-path .gitignore ." |
|||
}, |
|||
"devDependencies": { |
|||
"@sveltejs/adapter-auto": "next", |
|||
"@sveltejs/kit": "next", |
|||
"@typescript-eslint/eslint-plugin": "^5.10.1", |
|||
"@typescript-eslint/parser": "^5.10.1", |
|||
"eslint": "^7.32.0", |
|||
"eslint-plugin-svelte3": "^3.2.1", |
|||
"node-sass": "^7.0.1", |
|||
"sass": "^1.49.9", |
|||
"svelte": "^3.44.0", |
|||
"svelte-check": "^2.2.6", |
|||
"svelte-preprocess": "^4.10.1", |
|||
"tslib": "^2.3.1", |
|||
"typescript": "~4.5.4" |
|||
}, |
|||
"type": "module" |
|||
} |
@ -0,0 +1,10 @@ |
|||
/// <reference types="@sveltejs/kit" />
|
|||
|
|||
// See https://kit.svelte.dev/docs/types#the-app-namespace
|
|||
// for information about these interfaces
|
|||
declare namespace App { |
|||
// interface Locals {}
|
|||
// interface Platform {}
|
|||
// interface Session {}
|
|||
// interface Stuff {}
|
|||
} |
@ -0,0 +1,22 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<meta name="description" content="" /> |
|||
<link rel="icon" href="%svelte.assets%/favicon.png" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
|||
<style> |
|||
body { |
|||
background-color: #141415; |
|||
color: #aaa; |
|||
padding: 0; |
|||
margin: 0; |
|||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; |
|||
} |
|||
</style> |
|||
%svelte.head% |
|||
</head> |
|||
<body> |
|||
<div id="root">%svelte.body%</div> |
|||
</body> |
|||
</html> |
@ -0,0 +1,40 @@ |
|||
<script lang="ts"> |
|||
import type { StandaloneItem } from "$lib/models/item"; |
|||
import Entry from "../layout/Entry.svelte"; |
|||
import EntryAmounts from "../layout/EntryAmounts.svelte"; |
|||
import EntryDescription from "../layout/EntryDescription.svelte"; |
|||
import EntryName from "../layout/EntryName.svelte"; |
|||
|
|||
export let item: StandaloneItem |
|||
export let compact: boolean = false; |
|||
|
|||
let link = ""; |
|||
let amounts = []; |
|||
let subtitle = ""; |
|||
|
|||
$: if (item.project != null) { |
|||
link = `/${item.scope.id}/project/${item.project.id}?focusitem=${item.id}` |
|||
} else { |
|||
link = `/${item.scope.id}/item/${item.id}` |
|||
} |
|||
|
|||
$: amounts = item.stats.map(s => ({amount: s.acquired || s.required, label: s.name})) |
|||
$: subtitle = item.project != null ? item.project.name : item.scope.abbreviation |
|||
</script> |
|||
|
|||
<a href={link}> |
|||
<Entry> |
|||
<EntryName subtitle={subtitle}>{item.name}</EntryName> |
|||
{#if !compact} |
|||
<EntryDescription>{item.description}</EntryDescription> |
|||
{/if} |
|||
<EntryAmounts values={amounts} /> |
|||
</Entry> |
|||
</a> |
|||
|
|||
<style> |
|||
a { |
|||
text-decoration: none; |
|||
color: inherit; |
|||
} |
|||
</style> |
@ -0,0 +1,34 @@ |
|||
<script lang="ts"> |
|||
import type { StandaloneRequirement } from "$lib/models/project"; |
|||
|
|||
import Entry from "../layout/Entry.svelte"; |
|||
import EntryDescription from "../layout/EntryDescription.svelte"; |
|||
import EntryName from "../layout/EntryName.svelte"; |
|||
import EntryProgress from "../layout/EntryProgress.svelte"; |
|||
import EntryProgressRow from "../layout/EntryProgressRow.svelte"; |
|||
|
|||
export let requirement: StandaloneRequirement |
|||
export let compact: boolean = false; |
|||
export let boat: number = null; |
|||
</script> |
|||
|
|||
<a href="/{requirement.scope.id}/{requirement.project.id}?requirement={requirement.id}"> |
|||
<Entry> |
|||
<EntryName subtitle={requirement.project.name}>{requirement.name}</EntryName> |
|||
{#if !compact} |
|||
<EntryDescription>{requirement.description}</EntryDescription> |
|||
{/if} |
|||
<EntryProgressRow> |
|||
{#each requirement.stats as stat (stat.id)} |
|||
<EntryProgress boat={boat} name={stat.name} acquired={stat.acquired} required={stat.required} /> |
|||
{/each} |
|||
</EntryProgressRow> |
|||
</Entry> |
|||
</a> |
|||
|
|||
<style> |
|||
a { |
|||
text-decoration: none; |
|||
color: inherit; |
|||
} |
|||
</style> |
@ -0,0 +1,20 @@ |
|||
<script lang="ts"> |
|||
import type { ScopeEntry } from "$lib/models/scope"; |
|||
import Entry from "../layout/Entry.svelte"; |
|||
import EntryName from "../layout/EntryName.svelte"; |
|||
|
|||
export let scope: ScopeEntry |
|||
</script> |
|||
|
|||
<a href="/{scope.id}/"> |
|||
<Entry> |
|||
<EntryName subtitle={scope.abbreviation}>{scope.name}</EntryName> |
|||
</Entry> |
|||
</a> |
|||
|
|||
<style> |
|||
a { |
|||
text-decoration: none; |
|||
color: inherit; |
|||
} |
|||
</style> |
@ -0,0 +1,121 @@ |
|||
<script lang="ts"> |
|||
import type Sprint from "$lib/models/sprint"; |
|||
import type { StatAggregate, StatEntry } from "$lib/models/stat"; |
|||
import { calculateAggregate } from "$lib/utils/aggregate"; |
|||
|
|||
import currentTime from "$lib/stores/currentTime"; |
|||
|
|||
import Entry from "../layout/Entry.svelte"; |
|||
import EntryDescription from "../layout/EntryDescription.svelte"; |
|||
import EntryName from "../layout/EntryName.svelte"; |
|||
import EntryProgress from "../layout/EntryProgress.svelte"; |
|||
import EntryProgressRow from "../layout/EntryProgressRow.svelte"; |
|||
import ItemLink from "./ItemLink.svelte"; |
|||
import RequirementLink from "./RequirementLink.svelte"; |
|||
|
|||
export let sprint: Sprint |
|||
|
|||
let fromDate: Date; |
|||
let toDate: Date; |
|||
let boat: number; |
|||
let itemsAcquired: number; |
|||
let itemsRequired: number; |
|||
let aggregate: StatAggregate; |
|||
let reqAggregate: Record<string, StatAggregate>; |
|||
|
|||
$: { |
|||
fromDate = new Date(sprint.from); |
|||
toDate = new Date(sprint.to); |
|||
boat = ($currentTime.getTime() - fromDate.getTime()) / (toDate.getTime() - fromDate.getTime()); |
|||
boat = Math.max(0, Math.min(1, boat)); |
|||
} |
|||
|
|||
$: { |
|||
if (sprint.items != null) { |
|||
itemsAcquired = sprint.items.filter(i => !!i.acquireDate).length; |
|||
itemsRequired = sprint.items.length; |
|||
} else { |
|||
itemsAcquired = 0; |
|||
itemsRequired = 0; |
|||
} |
|||
} |
|||
|
|||
$: { |
|||
switch (sprint.kind) { |
|||
case "item": { |
|||
aggregate = calculateAggregate(sprint.items.map(i => i.stats).flat()); |
|||
break; |
|||
} |
|||
case "requirements": { |
|||
aggregate = calculateAggregate(sprint.requirements.map(r => r.stats).flat()); |
|||
break; |
|||
} |
|||
case "stats": { |
|||
aggregate = calculateAggregate(sprint.stats); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (sprint.aggregateRequired > 0) { |
|||
aggregate.required = sprint.aggregateRequired; |
|||
} |
|||
|
|||
reqAggregate = {}; |
|||
for (const requirement of (sprint.requirements || [])) { |
|||
reqAggregate[requirement.id] = calculateAggregate(requirement.stats); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<a href="/{sprint.scope.id}/sprints?sprint={sprint.id}"> |
|||
<Entry> |
|||
<EntryName subtitle="{sprint.scope.abbreviation} {sprint.kind} sprint">{sprint.name}</EntryName> |
|||
<EntryDescription>{sprint.description}</EntryDescription> |
|||
<EntryProgressRow> |
|||
{#if sprint.kind === "item"} |
|||
<EntryProgress |
|||
fullwidth green |
|||
percentage={sprint.aggregateRequired == 0} |
|||
name="Items" |
|||
acquired={itemsAcquired} |
|||
required={itemsRequired} |
|||
boat={boat} |
|||
/> |
|||
{:else} |
|||
<EntryProgress |
|||
fullwidth |
|||
percentage={sprint.aggregateRequired == 0} |
|||
name="Total" |
|||
acquired={aggregate.acquired} |
|||
required={aggregate.required} |
|||
boat={boat} |
|||
/> |
|||
{/if} |
|||
{#if !sprint.coarse} |
|||
{#each sprint.stats as stat (stat.id)} |
|||
<EntryProgress |
|||
name={stat.name} |
|||
acquired={stat.acquired} |
|||
required={stat.required} |
|||
boat={boat} |
|||
/> |
|||
{/each} |
|||
{/if} |
|||
</EntryProgressRow> |
|||
{#each sprint.items as item (item.id)} |
|||
{#if !item.acquireDate} |
|||
<ItemLink compact item={item} /> |
|||
{/if} |
|||
{/each} |
|||
{#each sprint.requirements as requirement (requirement.id)} |
|||
<RequirementLink compact boat={boat} requirement={requirement} /> |
|||
{/each} |
|||
</Entry> |
|||
</a> |
|||
|
|||
<style lang="scss"> |
|||
a { |
|||
text-decoration: none; |
|||
color: inherit; |
|||
} |
|||
</style> |
@ -0,0 +1,34 @@ |
|||
<div class="entry"> |
|||
<slot></slot> |
|||
</div> |
|||
|
|||
<style> |
|||
div.entry { |
|||
display: block; |
|||
text-decoration: none; |
|||
color: #aaa; |
|||
background-color: #222; |
|||
border-bottom-right-radius: 0.75em; |
|||
margin: 0.5em 0; |
|||
padding: 0.25em 0.5ch; |
|||
transition: 250ms; |
|||
} |
|||
div.entry:hover { |
|||
background-color: #333; |
|||
color: #eee; |
|||
} |
|||
|
|||
:global(div.entry div.entry) { |
|||
margin: 0.25em 0; |
|||
} |
|||
:global(div.entry div.entry) { |
|||
border-bottom-right-radius: 0.5em; |
|||
} |
|||
:global(div.entry div.entry:hover) { |
|||
background-color: #444; |
|||
} |
|||
|
|||
div.entry:last-of-type { |
|||
margin-bottom: 0.1em; |
|||
} |
|||
</style> |
@ -0,0 +1,40 @@ |
|||
<script lang="ts" context="module"> |
|||
export interface Amount { |
|||
label: string |
|||
amount: 0 |
|||
} |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
export let values: Amount[]; |
|||
</script> |
|||
|
|||
<div class="amounts"> |
|||
{#each values as value} |
|||
<div class="amount"> |
|||
<span class="value">{value.amount}×</span> |
|||
<span class="label">{value.label}</span> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
div.amounts { |
|||
display: flex; |
|||
flex-direction: row; |
|||
flex-wrap: wrap; |
|||
margin-top: 0.5em; |
|||
font-size: 0.75em; |
|||
} |
|||
div.amounts:empty { |
|||
margin-top: 0; |
|||
} |
|||
|
|||
div.amount { |
|||
padding-right: 1ch; |
|||
} |
|||
|
|||
span.label { |
|||
color: #777; |
|||
} |
|||
</style> |
@ -0,0 +1,14 @@ |
|||
<script lang="ts"> |
|||
</script> |
|||
|
|||
<div class="description"> |
|||
<span><slot></slot></span> |
|||
</div> |
|||
|
|||
<style> |
|||
div.description { |
|||
margin-top: 0.25em; |
|||
font-size: 1em; |
|||
color: inherit; |
|||
} |
|||
</style> |
@ -0,0 +1,28 @@ |
|||
<script lang="ts"> |
|||
export let subtitle: string = "" |
|||
</script> |
|||
|
|||
<div class="name"> |
|||
<span><slot></slot></span> |
|||
<span class="abbreviation">{subtitle}</span> |
|||
</div> |
|||
|
|||
<style> |
|||
div.name { |
|||
font-size: 1em; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
span.abbreviation { |
|||
font-size: 1em; |
|||
opacity: 0.333; |
|||
} |
|||
|
|||
@media screen and (max-width: 1500px) { |
|||
span.abbreviation { |
|||
display: block; |
|||
font-size: 0.75em; |
|||
margin-bottom: 0.5em; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,200 @@ |
|||
<script lang="ts" context="module"> |
|||
export const PROGRESS_COLORS = [ |
|||
"none", |
|||
"bronze", |
|||
"silver", |
|||
"gold", |
|||
] |
|||
|
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
export let name: string; |
|||
export let acquired: number; |
|||
export let required: number; |
|||
export let boat: number | null = null; |
|||
export let big: boolean = false; |
|||
export let bare: boolean = false; |
|||
export let fullwidth: boolean = false; |
|||
export let percentage: boolean = false; |
|||
export let green: boolean = false; |
|||
|
|||
let offClass = PROGRESS_COLORS[0]; |
|||
let onClass = PROGRESS_COLORS[1]; |
|||
let ons = 0; |
|||
let offs = 1; |
|||
let segmented = false; |
|||
let boatClass = "gray"; |
|||
let boatDone = false; |
|||
|
|||
$: { |
|||
if (required > 0) { |
|||
let level = Math.floor(acquired / required); |
|||
if (level >= PROGRESS_COLORS.length - 1) { |
|||
offs = 0; |
|||
ons = required; |
|||
offClass = "gold"; |
|||
onClass = "gold"; |
|||
} else { |
|||
ons = acquired - (level * required); |
|||
offs = required - ons; |
|||
console.log(name, ons, offs); |
|||
offClass = PROGRESS_COLORS[level]; |
|||
onClass = PROGRESS_COLORS[level + 1]; |
|||
} |
|||
|
|||
// Mark it non-segmented if the required is too high. This prevents a clunky progress bar, |
|||
// or a browser freeze if you set the required very high. |
|||
segmented = !percentage && (required <= (fullwidth ? 60 : 15)); |
|||
} else { |
|||
offClass = "none"; |
|||
segmented = true; |
|||
ons = 0; |
|||
offs = 1; |
|||
} |
|||
|
|||
if (green) { |
|||
onClass = "green"; |
|||
offClass = "none"; |
|||
|
|||
if (acquired >= required) { |
|||
ons = required; |
|||
offs = 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
$: { |
|||
if (boat != null && required > 0) { |
|||
const diff = (acquired / required) - boat; |
|||
if (diff < -0.05) { |
|||
boatClass = "red"; |
|||
} else if (diff > 0.05) { |
|||
boatClass = "green"; |
|||
} else { |
|||
boatClass = "gray"; |
|||
} |
|||
|
|||
boatDone = acquired >= required; |
|||
} else { |
|||
boatClass = "gray"; |
|||
boatDone = false; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div class="progress" class:fullwidth> |
|||
<!-- Header --> |
|||
{#if !bare} |
|||
<div class="header"> |
|||
<span>{name}</span> |
|||
{#if percentage} |
|||
<span class="ackreq">({((acquired / required) * 100).toFixed(0)}%)</span> |
|||
{:else} |
|||
<span class="ackreq">({Math.round(acquired)} / {Math.round(required)})</span> |
|||
{/if} |
|||
</div> |
|||
{/if} |
|||
|
|||
<!-- Progress Bar --> |
|||
<div class="bar" class:big> |
|||
{#if segmented} |
|||
{#each {length: Math.max(ons, 0)} as _} |
|||
<div class="segmented on {onClass}"></div> |
|||
{/each} |
|||
{#each {length: Math.max(offs, 0)} as _} |
|||
<div class="segmented off {offClass}"></div> |
|||
{/each} |
|||
{:else} |
|||
{#if ons > 0} |
|||
<div style="flex: {ons}" class="non-segmented on {onClass}"></div> |
|||
{/if} |
|||
{#if offs > 0} |
|||
<div style="flex: {offs}" class="non-segmented off {offClass}"></div> |
|||
{/if} |
|||
{/if} |
|||
</div> |
|||
|
|||
<!-- "Boat" --> |
|||
{#if boat != null && !boatDone} |
|||
<div class="bar boat"> |
|||
<div style="flex: {Math.min(boat, 1)}" class="non-segmented on {boatClass}"></div> |
|||
<div style="flex: {Math.max(1 - boat, 0)}" class="non-segmented off none"></div> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style lang="scss"> |
|||
div.progress { |
|||
padding: 0.25em 0.5ch; |
|||
font-size: 0.9em; |
|||
flex-basis: calc(33.33% - 1ch); |
|||
|
|||
@media screen and (max-width: 1300px) { |
|||
flex-basis: calc(50% - 1ch); |
|||
} |
|||
|
|||
@media screen and (max-width: 600px) { |
|||
font-size: 1.2em; |
|||
flex-basis: calc(100% - 1ch); |
|||
} |
|||
|
|||
&.fullwidth { |
|||
flex-basis: calc(100% - 1ch); |
|||
} |
|||
} |
|||
|
|||
div.bar { |
|||
display: flex; |
|||
flex-direction: row; |
|||
margin: 0; |
|||
box-sizing: border-box; |
|||
width: 100%; |
|||
height: 8px; |
|||
margin: 0.5px 0; |
|||
transform: skew(-11.25deg, 0); |
|||
|
|||
margin-top: 0.2em; |
|||
|
|||
@media screen and (max-width: 600px) { |
|||
height: 6px; |
|||
} |
|||
|
|||
&.big { |
|||
height: 12px; |
|||
} |
|||
&.boat { |
|||
transform: skew(-11.25deg, 0) translateX(-1.1px); |
|||
margin-top: 0em; |
|||
height: 2.5px; |
|||
} |
|||
|
|||
> div { |
|||
flex-grow: 1; |
|||
flex-basis: 0; |
|||
display: inline-block; |
|||
box-sizing: border-box; |
|||
margin: 0 0.5px; |
|||
border: 0.25px solid #000; |
|||
|
|||
div.non-segmented { |
|||
flex-grow: inherit; |
|||
flex-basis: inherit; |
|||
} |
|||
|
|||
&.on {opacity: 0.75; } |
|||
&.none { background-color: #000; } |
|||
&.gray { background-color: #999; opacity: 1; } |
|||
&.bronze { background-color: #f4b083; } |
|||
&.silver { background-color: #d8dce4; opacity: 0.8; } |
|||
&.gold { background-color: #ffd966; opacity: 0.9; } |
|||
&.green { background-color: #8ef88e; opacity: 0.8; } |
|||
&.red { background-color: hsl(0, 95%, 70%); opacity: 0.8; } |
|||
&.off {opacity: 0.30; } |
|||
} |
|||
} |
|||
|
|||
span.ackreq { |
|||
opacity: 0.333; |
|||
} |
|||
</style> |
@ -0,0 +1,16 @@ |
|||
<div class="progress-row"> |
|||
<slot></slot> |
|||
</div> |
|||
|
|||
<style> |
|||
div.progress-row { |
|||
display: flex; |
|||
flex-direction: row; |
|||
flex-wrap: wrap; |
|||
margin-top: 0.5em; |
|||
font-size: 0.75em; |
|||
} |
|||
div.progress-row:empty { |
|||
margin-top: 0; |
|||
} |
|||
</style> |
@ -0,0 +1,16 @@ |
|||
import type { ProjectEntry } from "./project"; |
|||
import type { ScopeEntry } from "./scope"; |
|||
import type { StatProgressEntry } from "./stat"; |
|||
|
|||
export default interface Item { |
|||
id: number |
|||
name: string |
|||
description: string |
|||
acquireDate?: string |
|||
stats: StatProgressEntry[] |
|||
} |
|||
|
|||
export interface StandaloneItem extends Item { |
|||
scope: ScopeEntry |
|||
project?: ProjectEntry |
|||
} |
@ -0,0 +1,36 @@ |
|||
import type Item from "./item"; |
|||
import type Scope from "./scope"; |
|||
import type { ScopeEntry } from "./scope"; |
|||
import type { StatAggregate, StatEntry, StatProgressEntry } from "./stat"; |
|||
import type Stat from "./stat"; |
|||
import type Status from "./status"; |
|||
|
|||
export default interface Project { |
|||
id: number |
|||
name: string |
|||
description: string |
|||
status: Status |
|||
|
|||
scope: ScopeEntry |
|||
requirements: Requirement[] |
|||
} |
|||
|
|||
export interface ProjectEntry { |
|||
id: number |
|||
name: string |
|||
status: Status |
|||
} |
|||
|
|||
export interface Requirement { |
|||
id: number |
|||
name: string |
|||
description: string |
|||
status: Status |
|||
|
|||
stats: StatProgressEntry[] |
|||
} |
|||
|
|||
export interface StandaloneRequirement extends Requirement { |
|||
scope: ScopeEntry |
|||
project: ProjectEntry |
|||
} |
@ -0,0 +1,11 @@ |
|||
import type { ProjectEntry } from "./project"; |
|||
|
|||
export default interface Scope extends ScopeEntry { |
|||
activeProjects: ProjectEntry[] |
|||
} |
|||
|
|||
export interface ScopeEntry { |
|||
id: number |
|||
name: string |
|||
abbreviation: string |
|||
} |
@ -0,0 +1,24 @@ |
|||
import type { StandaloneItem } from "./item"; |
|||
import type { StandaloneRequirement } from "./project"; |
|||
import type { ScopeEntry } from "./scope"; |
|||
import type { StatAggregate, StatProgressEntry } from "./stat"; |
|||
|
|||
export default interface Sprint { |
|||
id: number |
|||
name: string |
|||
description: string |
|||
from: string |
|||
to: string |
|||
timed: boolean |
|||
coarse: boolean |
|||
kind: "item" | "requirements" | "stats" |
|||
scope: ScopeEntry |
|||
items: StandaloneItem[] |
|||
requirements: StandaloneRequirement[] |
|||
aggregateRequired: number, |
|||
stats: StatProgressEntry[] |
|||
} |
|||
|
|||
export interface StandaloneSprint extends Sprint { |
|||
scope: ScopeEntry |
|||
} |
@ -0,0 +1,52 @@ |
|||
export default interface Stat extends StatEntry { |
|||
description: string |
|||
allowedAmounts?: Record<string, number> |
|||
} |
|||
|
|||
export interface StatEntry { |
|||
id: number |
|||
weight: number |
|||
name: string |
|||
} |
|||
|
|||
export interface StatProgressEntry extends StatEntry { |
|||
acquired: number |
|||
required: number |
|||
} |
|||
|
|||
export interface StatAggregate { |
|||
acquired: number |
|||
required: number |
|||
} |
|||
|
|||
const EXAMPLE2: Stat = { |
|||
id: 12, |
|||
name: "Assets", |
|||
description: "The amount of 'assets' produced in the end. This does not contribute towards the weight", |
|||
weight: 0, |
|||
} |
|||
|
|||
const EXAMPLE3: Stat = { |
|||
id: 12, |
|||
name: "Hard Surface Practice", |
|||
description: "A value of the asset complexity, indicating how much general modeling practice is involved.", |
|||
weight: 0.5, |
|||
allowedAmounts: { |
|||
"Rote": 1, |
|||
"Novel": 2, |
|||
"Tutorial": 4, |
|||
"Difficult": 7, |
|||
} |
|||
} |
|||
|
|||
const EXAMPLE: Stat = { |
|||
id: 12, |
|||
name: "Complexity Points", |
|||
description: "A value of the asset complexity, indicating how much general modeling practice is involved.", |
|||
weight: 1, |
|||
allowedAmounts: { |
|||
"Small": 1, |
|||
"Moderate": 3, |
|||
"Complex": 7, |
|||
} |
|||
} |
@ -0,0 +1,25 @@ |
|||
enum Status { |
|||
Blocked = 0, |
|||
Available = 1, |
|||
Inactive = 2, |
|||
Active = 3, |
|||
Completed = 4, |
|||
Failed = 5, |
|||
Dropped = 6, |
|||
} |
|||
|
|||
// Blocked -> Available -> Active -> Completed
|
|||
// -> Failed
|
|||
// -> Dropped
|
|||
// -> Inactive -> Active
|
|||
|
|||
//
|
|||
// (X) Create basic furniture [S v] [/] [X]
|
|||
// ¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨
|
|||
// There needs to be a servicable library of
|
|||
// furntirue for this project
|
|||
//
|
|||
// Asset Complexity HS Practice
|
|||
// [ ] [ ] 0
|
|||
|
|||
export default Status |
@ -0,0 +1,15 @@ |
|||
import { writable, type Readable } from "svelte/store"; |
|||
|
|||
const currentTimeWritable = writable(new Date()); |
|||
|
|||
setInterval(() => { |
|||
currentTimeWritable.set(new Date()); |
|||
}, 15000); |
|||
|
|||
/** |
|||
* A readable store that is updated twice every minute with the current time. Mainly for the progress |
|||
* bar timers, but other coarse-grained timers could use it as well. |
|||
*/ |
|||
const currentTime: Readable<Date> = { subscribe: currentTimeWritable.subscribe } |
|||
|
|||
export default currentTime; |
@ -0,0 +1,10 @@ |
|||
import type { StatAggregate, StatProgressEntry } from "$lib/models/stat"; |
|||
|
|||
export function calculateAggregate(entries: StatProgressEntry[]): StatAggregate { |
|||
return entries.reduce<StatAggregate>((acc, entry) => { |
|||
return { |
|||
acquired: acc.acquired + (entry.acquired * entry.weight), |
|||
required: acc.required + (entry.required * entry.weight), |
|||
} |
|||
}, {acquired: 0, required: 0}); |
|||
} |
@ -0,0 +1,146 @@ |
|||
<script lang="ts" context="module"> |
|||
import type { LoadInput } from "@sveltejs/kit/types/internal"; |
|||
|
|||
const TIPS = [ |
|||
"Ctrl-click items and requirements in your suggestions to log it.", |
|||
"Suggestions are incomplete, go into scopes or sprints to view all you need to do.", |
|||
] |
|||
|
|||
export async function load({ url, fetch }: LoadInput ) { |
|||
const res = await fetch("indexdata.json"); |
|||
|
|||
if (res.ok) { |
|||
return { |
|||
props: { |
|||
...await res.json(), |
|||
tip: TIPS[Math.floor(Math.random() * TIPS.length)] |
|||
} |
|||
}; |
|||
} |
|||
|
|||
return { |
|||
status: res.status, |
|||
error: `Failed: ${res.status}` |
|||
}; |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import ItemLink from "$lib/components/frontpage/ItemLink.svelte"; |
|||
import ScopeLink from "$lib/components/frontpage/ScopeLink.svelte"; |
|||
import RequirementLink from "$lib/components/frontpage/RequirementLink.svelte"; |
|||
import type { ScopeEntry } from "$lib/models/scope"; |
|||
import type { StandaloneItem } from "$lib/models/item"; |
|||
import type { StandaloneRequirement } from "$lib/models/project"; |
|||
import type { StandaloneSprint } from "$lib/models/sprint"; |
|||
import SprintLink from "$lib/components/frontpage/SprintLink.svelte"; |
|||
|
|||
export let scopes: ScopeEntry[] = []; |
|||
export let items: StandaloneItem[] = []; |
|||
export let requirements: StandaloneRequirement[] = []; |
|||
export let sprints: StandaloneSprint[] = []; |
|||
export let tip: string; |
|||
</script> |
|||
|
|||
<div class="header-wrapper"> |
|||
<h1>Stufflog</h1> |
|||
<h2>Logging your stuff.</h2> |
|||
</div> |
|||
|
|||
<main> |
|||
<div class="column"> |
|||
<div class="row"> |
|||
<h2>Scopes</h2> |
|||
{#each scopes as scope (scope.id)} |
|||
<ScopeLink scope={scope} /> |
|||
{/each} |
|||
</div> |
|||
<div class="row"> |
|||
<h2>Today</h2> |
|||
<p>When you mark an item as aquired today, it will appear here.</p> |
|||
<p>TIP: {tip}</p> |
|||
</div> |
|||
</div> |
|||
<div class="column"> |
|||
<div class="row"> |
|||
<h2>Suggestions</h2> |
|||
{#each items as item (item.id)} |
|||
<ItemLink item={item} /> |
|||
{/each} |
|||
{#each requirements as requirement (requirement.id)} |
|||
<RequirementLink requirement={requirement} /> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
<div class="column"> |
|||
<div class="row"> |
|||
<h2>Sprints</h2> |
|||
{#each sprints as sprint (sprint.id)} |
|||
<SprintLink sprint={sprint} /> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
</main> |
|||
|
|||
<style lang="scss"> |
|||
div.header-wrapper { |
|||
width: 180ch; |
|||
max-width: 95%; |
|||
margin: auto; |
|||
margin-top: 2em; |
|||
|
|||
h1 { |
|||
margin: 0.333em 0.333ch; |
|||
margin-bottom: 0; |
|||
font-size: 3em; |
|||
font-weight: 100; |
|||
} |
|||
|
|||
h2 { |
|||
font-size: 1em; |
|||
margin: 1em 1ch; |
|||
margin-top: 0; |
|||
font-style: italic; |
|||
font-weight: 100; |
|||
} |
|||
} |
|||
|
|||
p { |
|||
margin-top: 0.25em; |
|||
} |
|||
|
|||
main { |
|||
display: flex; |
|||
flex-direction: row; |
|||
flex-basis: 100%; |
|||
flex: 1; |
|||
|
|||
width: 180ch; |
|||
max-width: 95%; |
|||
margin: auto; |
|||
|
|||
@media screen and (max-width: 1000px) { |
|||
width: 100%; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
> div.column { |
|||
display: flex; |
|||
flex-direction: column; |
|||
flex-basis: 50; |
|||
flex: 1; |
|||
|
|||
> div.row { |
|||
margin: 0.5em 1ch; |
|||
padding: 0.5em 1ch; |
|||
|
|||
> h2 { |
|||
font-size: 1.25em; |
|||
margin: 0; |
|||
margin-bottom: 0.25em; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,190 @@ |
|||
import type { RequestHandler } from "@sveltejs/kit" |
|||
|
|||
import type { StandaloneItem } from "$lib/models/item" |
|||
import type { StandaloneRequirement } from "$lib/models/project" |
|||
import type { ScopeEntry } from "$lib/models/scope" |
|||
import Status from "$lib/models/status" |
|||
import type { StandaloneSprint } from "$lib/models/sprint" |
|||
|
|||
export const get: RequestHandler = async({}) => { |
|||
const scopes: ScopeEntry[] = [ |
|||
{id: 1, name: "3D Modeling", abbreviation: "3D"}, |
|||
{id: 2, name: "Roleplay", abbreviation: "RP"}, |
|||
{id: 3, name: "Minecraft", abbreviation: "MC"}, |
|||
{id: 4, name: "Coding", abbreviation: "CODE"}, |
|||
{id: 5, name: "System Administration", abbreviation: "SA"}, |
|||
] |
|||
|
|||
const items: StandaloneItem[] = [ |
|||
{ |
|||
id: 1, scope: scopes[0], |
|||
name: "Table", |
|||
description: "A table for the Redrock rec-room.", |
|||
acquireDate: "2022-03-14T00:00:00Z", |
|||
stats: [ |
|||
{id: 1, name: "Asset", weight: 0.2, required: 1, acquired: 1}, |
|||
{id: 2, name: "Complexity", weight: 1, required: 3, acquired: 5}, |
|||
], |
|||
project: { |
|||
id: 443, |
|||
name: "3D Maps: Proving the concept", |
|||
status: Status.Active |
|||
} |
|||
}, |
|||
{ |
|||
id: 2, scope: scopes[0], |
|||
name: "Datapad Set", |
|||
description: "I need one, at least, but more is better.", |
|||
stats: [ |
|||
{id: 1, name: "Asset", weight: 0.2, required: 1, acquired: 0}, |
|||
{id: 2, name: "Complexity", weight: 1, required: 3, acquired: 0}, |
|||
{id: 3, name: "Hard Surface", weight: 0.333, required: 1, acquired: 0}, |
|||
], |
|||
project: { |
|||
id: 443, |
|||
name: "3D Maps: Proving the concept", |
|||
status: Status.Active |
|||
} |
|||
}, |
|||
{ |
|||
id: 3, scope: scopes[1], |
|||
name: "Enila/Renala: Technicalities", |
|||
description: "Renala is answering Leah's concern and takes the shift at the Respite.", |
|||
stats: [ |
|||
{id: 1, name: "Story", weight: 3, required: 1, acquired: 0}, |
|||
{id: 2, name: "Story Word", weight: 0.002, required: 500, acquired: 0}, |
|||
], |
|||
project: { |
|||
id: 1, |
|||
name: "Background Stories", |
|||
status: Status.Active |
|||
} |
|||
}, |
|||
] |
|||
|
|||
const requirements: StandaloneRequirement[] = [ |
|||
{ |
|||
id: 1, |
|||
name: "Basic Furniture", |
|||
description: "I need a lot of it.", |
|||
project: { |
|||
id: 443, |
|||
name: "3D Maps: Proving the concept", |
|||
status: Status.Active |
|||
}, |
|||
status: Status.Active, |
|||
stats: [ |
|||
{id: 1, name: "Asset", weight: 0.2, acquired: 8, required: 15}, |
|||
{id: 2, name: "Complexity", weight: 1, acquired: 69, required: 30}, |
|||
{id: 3, name: "Hard Surface", weight: 0.333, acquired: 20, required: 5}, |
|||
], |
|||
scope: scopes[0], |
|||
}, |
|||
{ |
|||
id: 2, |
|||
name: "Catching Up", |
|||
description: "Write a few stories to get back up to speed with less written characters.", |
|||
project: { |
|||
id: 1, |
|||
name: "Background Stories", |
|||
status: Status.Active |
|||
}, |
|||
status: Status.Active, |
|||
stats: [ |
|||
{id: 1, name: "Story", weight: 3, required: 10, acquired: 3}, |
|||
{id: 2, name: "Story Word", weight: 0.002, required: 5000, acquired: 2173}, |
|||
{id: 3, name: "Worldbuilding", weight: 0.5, required: 5, acquired: 1}, |
|||
], |
|||
scope: scopes[1], |
|||
}, |
|||
{ |
|||
id: 3, |
|||
name: "Chapter 3", |
|||
description: "it's go time", |
|||
project: { |
|||
id: 1, |
|||
name: "Wenera's Job", |
|||
status: Status.Active |
|||
}, |
|||
status: Status.Active, |
|||
stats: [ |
|||
{id: 1, name: "Story", weight: 3, required: 5, acquired: 1}, |
|||
{id: 2, name: "Story Word", weight: 0.002, required: 4000, acquired: 759}, |
|||
], |
|||
scope: scopes[1], |
|||
} |
|||
] |
|||
|
|||
const sprints: StandaloneSprint[] = [ |
|||
{ |
|||
id: 553, |
|||
name: "March Sprint", |
|||
description: "Hey kid, wanna model some trees?", |
|||
from: "2022-03-01T00:00:00+0200", |
|||
to: "2022-03-30T23:59:59+0200", |
|||
coarse: false, |
|||
kind: "stats", |
|||
scope: scopes[0], |
|||
timed: true, |
|||
stats: [ |
|||
{id: 1, name: "Asset", weight: 0, acquired: 13, required: 15}, |
|||
{id: 2, name: "Complexity", weight: 1, acquired: 37, required: 30}, |
|||
{id: 3, name: "Hard Surface", weight: 0.25, acquired: 8, required: 5}, |
|||
{id: 4, name: "UV Mapping", weight: 0.25, acquired: 3, required: 5}, |
|||
], |
|||
items: [], |
|||
requirements: [], |
|||
aggregateRequired: 40, |
|||
}, |
|||
{ |
|||
id: 643, |
|||
name: "March Sprint", |
|||
description: "Crank out stories", |
|||
from: "2022-03-01T00:00:00+0200", |
|||
to: "2022-03-30T23:59:59+0200", |
|||
coarse: false, |
|||
kind: "requirements", |
|||
scope: scopes[1], |
|||
timed: true, |
|||
stats: [], |
|||
items: [], |
|||
requirements: [ |
|||
requirements[1], |
|||
requirements[2], |
|||
], |
|||
aggregateRequired: 0, |
|||
}, |
|||
{ |
|||
id: 771, |
|||
name: "March Sprint", |
|||
description: "Do these", |
|||
from: "2022-03-01T00:00:00+0200", |
|||
to: "2022-03-30T23:59:59+0200", |
|||
coarse: false, |
|||
kind: "item", |
|||
scope: scopes[0], |
|||
timed: true, |
|||
stats: [ |
|||
{id: 1, name: "Asset", weight: 0.2, required: 2, acquired: 1}, |
|||
{id: 2, name: "Complexity", weight: 1, required: 8, acquired: 5}, |
|||
{id: 3, name: "Hard Surface", weight: 0.333, required: 1, acquired: 0}, |
|||
], |
|||
items: [ |
|||
items[0], |
|||
items[1], |
|||
], |
|||
requirements: [], |
|||
aggregateRequired: 0, |
|||
} |
|||
] |
|||
|
|||
return { |
|||
status: 200, |
|||
headers: { |
|||
"Content-Type": "application/json" |
|||
}, |
|||
body: JSON.stringify({ |
|||
scopes, items, requirements, sprints |
|||
}), |
|||
} |
|||
} |
After Width: 128 | Height: 128 | Size: 1.5 KiB |
@ -0,0 +1,15 @@ |
|||
import adapter from '@sveltejs/adapter-auto'; |
|||
import preprocess from 'svelte-preprocess'; |
|||
|
|||
/** @type {import('@sveltejs/kit').Config} */ |
|||
const config = { |
|||
// Consult https://github.com/sveltejs/svelte-preprocess
|
|||
// for more information about preprocessors
|
|||
preprocess: preprocess(), |
|||
|
|||
kit: { |
|||
adapter: adapter() |
|||
} |
|||
}; |
|||
|
|||
export default config; |
@ -0,0 +1,36 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"moduleResolution": "node", |
|||
"module": "es2020", |
|||
"lib": ["es2020", "DOM"], |
|||
"target": "es2020", |
|||
/** |
|||
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript |
|||
to enforce using \`import type\` instead of \`import\` for Types. |
|||
*/ |
|||
"importsNotUsedAsValues": "error", |
|||
/** |
|||
TypeScript doesn't know about import usages in the template because it only sees the |
|||
script of a Svelte file. Therefore preserve all value imports. Requires TS 4.5 or higher. |
|||
*/ |
|||
"preserveValueImports": true, |
|||
"isolatedModules": true, |
|||
"resolveJsonModule": true, |
|||
/** |
|||
To have warnings/errors of the Svelte compiler at the correct position, |
|||
enable source maps by default. |
|||
*/ |
|||
"sourceMap": true, |
|||
"esModuleInterop": true, |
|||
"skipLibCheck": true, |
|||
"forceConsistentCasingInFileNames": true, |
|||
"baseUrl": ".", |
|||
"allowJs": true, |
|||
"checkJs": true, |
|||
"paths": { |
|||
"$lib": ["src/lib"], |
|||
"$lib/*": ["src/lib/*"] |
|||
} |
|||
}, |
|||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"] |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue