Feat/add bulma (#1)

* remove fixture for task and steps

* install bulma

* convert to scss files

* reset and implement first style classes

* design task form

* design task view

* design step record

* 💄 (home) new task and reset styling

* fix step title height

* add a record progress

* ♻️ (record) extract record controls

* ️ (record) add text for progress

* 💄 (step record) fix blob size

* ♻️ (record) no more getters who do an action too
This commit is contained in:
Julien Calixte
2023-04-15 16:54:29 +02:00
committed by GitHub
parent 5bf3d248dd
commit 7d6523067c
21 changed files with 1901 additions and 386 deletions

View File

@@ -6,7 +6,7 @@
"dev": "vite", "dev": "vite",
"build": "run-p type-check build-only", "build": "run-p type-check build-only",
"preview": "vite preview", "preview": "vite preview",
"test": "pnpm lint && pnpm type-check && pnpm test:unit", "test": "pnpm lint && pnpm type-check && pnpm test:unit --ui",
"test:unit": "vitest", "test:unit": "vitest",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
@@ -15,6 +15,7 @@
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^9.13.0", "@vueuse/core": "^9.13.0",
"bulma": "^0.9.4",
"nanoid": "^4.0.2", "nanoid": "^4.0.2",
"pinia": "^2.0.34", "pinia": "^2.0.34",
"pinia-plugin-persistedstate": "^3.1.0", "pinia-plugin-persistedstate": "^3.1.0",
@@ -29,6 +30,8 @@
"@types/jsdom": "^21.1.1", "@types/jsdom": "^21.1.1",
"@types/node": "^18.14.2", "@types/node": "^18.14.2",
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.1.0",
"@vitest/browser": "^0.30.1",
"@vitest/ui": "^0.30.1",
"@vue/eslint-config-prettier": "^7.1.0", "@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.2", "@vue/eslint-config-typescript": "^11.0.2",
"@vue/test-utils": "^2.3.2", "@vue/test-utils": "^2.3.2",
@@ -42,6 +45,7 @@
"typescript": "~4.8.4", "typescript": "~4.8.4",
"vite": "^4.2.1", "vite": "^4.2.1",
"vitest": "^0.29.8", "vitest": "^0.29.8",
"vue-tsc": "^1.2.0" "vue-tsc": "^1.2.0",
"webdriverio": "^8.8.2"
} }
} }

1564
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,76 +4,12 @@ import { RouterLink, RouterView } from 'vue-router'
<template> <template>
<header> <header>
<div class="wrapper">
<nav> <nav>
<router-link to="/">Home</router-link> <router-link class="title is-3" to="/">Loopycode</router-link>
</nav> </nav>
</div>
</header> </header>
<RouterView /> <RouterView />
</template> </template>
<style scoped> <style scoped></style>
header {
line-height: 1;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style>

View File

@@ -1,78 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Oxygen+Mono&display=swap');
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(251, 251, 251, 0.798);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
}
body {
color: var(--color-text);
background: var(--color-background);
transition: color 0.5s, background-color 0.5s;
line-height: 1.6;
font-family: 'Oxygen Mono', monospace;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
font-family: 'Oxygen Mono', monospace;
}

View File

@@ -1,29 +0,0 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 0 1rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
}

26
src/assets/main.scss Normal file
View File

@@ -0,0 +1,26 @@
@charset "utf-8";
@import url('https://fonts.googleapis.com/css2?family=Oxygen+Mono&display=swap');
$family-monospace: 'Oxygen Mono',
monospace;
$family-primary: $family-monospace;
@import '../../node_modules/bulma/bulma.sass';
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
transition: color 0.5s, background-color 0.5s;
font-size: 16px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0 1rem;
}

View File

@@ -4,7 +4,7 @@ import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import { router } from './router' import { router } from './router'
import './assets/main.css' import './assets/main.scss'
const app = createApp(App) const app = createApp(App)

View File

@@ -0,0 +1,94 @@
<script setup lang="ts">
import { useTaskStore } from '@/modules/task/stores/useTask.store'
import { toISODate } from '@/shared/types/date'
import { useMagicKeys, whenever } from '@vueuse/core'
import { computed } from 'vue'
import { useTaskRecordStore } from '../stores/useTaskRecordStore'
const props = defineProps<{
taskId: string
recordId: string
}>()
const taskStore = useTaskStore()
const recordStore = useTaskRecordStore()
const task = computed(() => taskStore.getTask(props.taskId))
const record = computed(() => recordStore.getTaskRecord(props.recordId))
const getNextStepId = () => {
if (!task.value) {
return null
}
if (!recordStore.currentStepId) {
const [firstStep] = task.value.steps
return firstStep.id
}
const currentStepIndex = task.value.steps.findIndex(
(step) => step.id === recordStore.currentStepId
)
const canHaveNextIndex =
currentStepIndex >= 0 && currentStepIndex < task.value.steps.length - 1
if (canHaveNextIndex) {
return task.value.steps[currentStepIndex + 1].id
}
return null
}
const canStart = computed(() => !recordStore.currentStepId)
const startRecording = () => {
if (!canStart.value || !task.value) {
return
}
recordStore.startStepRecord({
recordId: props.recordId,
stepId: task.value.steps[0].id,
start: toISODate(new Date())
})
}
const nextStep = () => {
if (!task.value || !recordStore.currentStepId || !record.value) {
return
}
recordStore.nextStepRecord({
recordId: record.value.id,
currentStepId: recordStore.currentStepId,
nextStepId: getNextStepId(),
tick: toISODate(new Date())
})
}
const { n, s } = useMagicKeys()
whenever(n, () => {
nextStep()
})
whenever(s, () => {
startRecording()
})
</script>
<template>
<div class="record-controls buttons">
<template v-if="!record || !record.end">
<button v-if="canStart" @click="startRecording" class="button is-primary">
start
</button>
<button class="button" v-else @click="nextStep">next</button>
</template>
<button class="button is-warning" @click="recordStore.reset(recordId)">
reset
</button>
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { useTaskStore } from '@/modules/task/stores/useTask.store'
import { computed } from 'vue'
import { useTaskRecordStore } from '../stores/useTaskRecordStore'
const props = defineProps<{
taskId: string
recordId: string
}>()
const taskStore = useTaskStore()
const taskRecordStore = useTaskRecordStore()
const task = computed(() => taskStore.getTask(props.taskId))
const record = computed(() => taskRecordStore.getRecord(props.recordId))
const numberOfFinishedSteps = computed(
() =>
Object.values(record.value?.stepRecords ?? {}).filter((step) => !!step.end)
.length
)
</script>
<template>
<progress
v-if="task && record"
class="progress is-primary"
:value="numberOfFinishedSteps"
:max="task.steps.length ?? 0"
>
{{ numberOfFinishedSteps }}/{{ task.steps.length }}
</progress>
</template>

View File

@@ -59,17 +59,21 @@ const isSuperiorToEstimation = computed(() => {
<span v-else-if="isSuperiorToEstimation"> </span> <span v-else-if="isSuperiorToEstimation"> </span>
<span v-else></span> <span v-else></span>
</td> </td>
<td class="title"> <td>
<div class="step-title">
<div v-if="isCurrentStep" class="blob green"></div> <div v-if="isCurrentStep" class="blob green"></div>
{{ step.title }} {{ step.title }}
</div>
</td> </td>
<td class="estimation">{{ step.estimation }} minutes</td> <td class="estimation minutes">{{ step.estimation }} min</td>
<td v-if="stepRecord">{{ duration }} minutes</td> <td class="minutes" v-if="stepRecord">{{ duration }} min</td>
<td v-else></td> <td v-else></td>
</tr> </tr>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
$blob-size: 15px;
.step-record { .step-record {
.status { .status {
text-align: center; text-align: center;
@@ -85,19 +89,23 @@ const isSuperiorToEstimation = computed(() => {
.blob { .blob {
border-radius: 50%; border-radius: 50%;
height: 10px; min-height: $blob-size;
width: 10px; min-width: $blob-size;
background: rgba(51, 217, 178, 1); background: rgba(51, 217, 178, 1);
box-shadow: 0 0 0 0 rgba(51, 217, 178, 1); box-shadow: 0 0 0 0 rgba(51, 217, 178, 1);
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
.title { .step-title {
display: flex; display: flex;
align-items: center; align-items: center;
padding-right: 1rem; padding-right: 1rem;
gap: 1rem; gap: 1rem;
} }
.minutes {
text-align: right;
}
} }
@keyframes pulse { @keyframes pulse {

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTaskStore } from '@/modules/task/stores/useTask.store' import { useTaskStore } from '@/modules/task/stores/useTask.store'
import { formatLongDate } from '@/shared/format-date' import { formatLongDate } from '@/shared/format-date'
import { toISODate } from '@/shared/types/date'
import { useMagicKeys, whenever } from '@vueuse/core'
import { computed } from 'vue' import { computed } from 'vue'
import { useTaskRecordMetadata } from '../hooks/useTaskRecordMetadata' import { useTaskRecordMetadata } from '../hooks/useTaskRecordMetadata'
import { useTaskRecordStore } from '../stores/useTaskRecordStore' import { useTaskRecordStore } from '../stores/useTaskRecordStore'
import RecordControls from './RecordControls.vue'
import RecordProgress from './RecordProgress.vue'
import StepRecord from './StepRecord.vue' import StepRecord from './StepRecord.vue'
const props = defineProps<{ const props = defineProps<{
@@ -16,69 +16,14 @@ const props = defineProps<{
const taskStore = useTaskStore() const taskStore = useTaskStore()
const recordStore = useTaskRecordStore() const recordStore = useTaskRecordStore()
recordStore.addRecord(props.taskId, props.recordId)
const task = computed(() => taskStore.getTask(props.taskId)) const task = computed(() => taskStore.getTask(props.taskId))
const record = computed(() => const record = computed(() => recordStore.getTaskRecord(props.recordId))
recordStore.createAndRetrieveTaskRecord(props.taskId, props.recordId)
)
const recordNotes = computed(() => recordStore.getRecordNotes(props.recordId)) const recordNotes = computed(() => recordStore.getRecordNotes(props.recordId))
const { duration } = useTaskRecordMetadata(record) const { duration } = useTaskRecordMetadata(record)
const getNextStepId = () => {
if (!task.value) {
return null
}
if (!recordStore.currentStepId) {
const [firstStep] = task.value.steps
return firstStep.id
}
const currentStepIndex = task.value.steps.findIndex(
(step) => step.id === recordStore.currentStepId
)
const canHaveNextIndex =
currentStepIndex >= 0 && currentStepIndex < task.value.steps.length - 1
if (canHaveNextIndex) {
return task.value.steps[currentStepIndex + 1].id
}
return null
}
const startRecording = () => {
if (!task.value) {
return
}
recordStore.startStepRecord({
recordId: props.recordId,
stepId: task.value.steps[0].id,
start: toISODate(new Date())
})
}
const nextStep = () => {
if (!task.value || !recordStore.currentStepId) {
return
}
recordStore.nextStepRecord({
recordId: record.value.id,
currentStepId: recordStore.currentStepId,
nextStepId: getNextStepId(),
tick: toISODate(new Date())
})
}
const { n } = useMagicKeys()
whenever(n, () => {
nextStep()
})
const isSuperiorToEstimation = computed(() => { const isSuperiorToEstimation = computed(() => {
if (!task.value || !record.value || !duration.value) { if (!task.value || !record.value || !duration.value) {
return false return false
@@ -90,25 +35,18 @@ const isSuperiorToEstimation = computed(() => {
<template> <template>
<main class="task-record" v-if="task"> <main class="task-record" v-if="task">
<h1> <record-progress :task-id="taskId" :record-id="recordId" />
Task: <h1 class="title">
<router-link :to="{ name: 'task-view', params: { id: task.id } }"> <router-link
:to="{ name: 'task-view', params: { id: task.id } }"
class="button is-link is-light"
>
{{ task.title }} {{ task.title }}
</router-link> </router-link>
</h1> </h1>
<h2>{{ formatLongDate(record.start) }}</h2> <h2 class="subtitle" v-if="record">{{ formatLongDate(record.start) }}</h2>
<template v-if="!record.end"> <record-controls :task-id="taskId" :record-id="recordId" />
<button <table class="table is-striped is-narrow is-hoverable is-fullwidth">
v-if="!recordStore.currentStepId && !record.hasStepRecords"
@click="startRecording"
>
start
</button>
<button v-else @click="nextStep">next</button>
</template>
<button @click="recordStore.$reset">reset</button>
<table>
<thead> <thead>
<tr> <tr>
<th>#</th> <th>#</th>
@@ -119,44 +57,42 @@ const isSuperiorToEstimation = computed(() => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<StepRecord <step-record
v-for="(step, key) in task.steps" v-for="(step, key) in task.steps"
:task-id="taskId" :task-id="taskId"
:record-id="recordId" :record-id="recordId"
:key="step.id" :key="step.id"
:step-id="step.id" :step-id="step.id"
:step-number="key" :step-number="key + 1"
/> />
</tbody> </tbody>
</table> </table>
<div v-if="record.end"> <div v-show="record && record.end" class="content">
<hr /> <p
:class="{
'has-text-primary-dark': !isSuperiorToEstimation,
'has-text-warning-dark': isSuperiorToEstimation
}"
>
The task took {{ duration }} minutes instead of The task took {{ duration }} minutes instead of
{{ task.totalEstimation }} minutes. {{ task.totalEstimation }} minutes.
<span> </p>
<span v-if="isSuperiorToEstimation">More</span><span v-else>Less</span>
than expected.
</span>
</div> </div>
<div class="columns is-centered">
<div class="column is-half">
<textarea <textarea
name="record-notes" name="record-notes"
id="record-notes" id="record-notes"
cols="30"
rows="10" rows="10"
:value="recordNotes" :value="recordNotes"
@input=" @input="
//@ts-ignore //@ts-ignore
recordStore.updateRecordNotes(recordId, $event.target?.value) recordStore.updateRecordNotes(recordId, $event.target.value)
" "
placeholder="Take notes while you're doing the task. It can be helpful at the end to retrieve your thought." placeholder="Take notes while you're doing the task. It can be helpful at the end to retrieve your thought."
class="textarea"
></textarea> ></textarea>
</div>
</div>
</main> </main>
</template> </template>
<style scoped lang="scss">
.task-record {
table {
width: 100%;
}
}
</style>

View File

@@ -11,20 +11,23 @@ const { duration } = useTaskRecordMetadata(props.record)
</script> </script>
<template> <template>
<span> <div class="task-record-link-container content">
<router-link <router-link
class="task-record-link" class="task-record-link button is-outlined"
:to="{ :to="{
name: 'record-view', name: 'record-view',
params: { taskId: record.taskId, recordId: record.id } params: { taskId: record.taskId, recordId: record.id }
}" }"
>{{ formatDate(record.start) }}</router-link >{{ formatDate(record.start) }}</router-link
> >
{{ duration }} minutes <span v-if="duration !== null"> {{ duration }} minutes </span>
</span> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.task-record-link { .task-record-link-container {
display: flex;
align-items: center;
gap: 1rem;
} }
</style> </style>

View File

@@ -14,22 +14,20 @@ const newRecordId = createUuid()
</script> </script>
<template> <template>
<div class="content">
<ol v-if="records.length" class="task-record-list"> <ol v-if="records.length" class="task-record-list">
<li v-for="record in records" :key="record.id"> <li v-for="record in records" :key="record.id">
<task-record-link :record="record" /> <task-record-link :record="record" />
</li> </li>
</ol> </ol>
<div v-else>No record yet</div> <div v-else>No record yet</div>
</div>
<router-link <router-link
:to="{ :to="{
name: 'record-view', name: 'record-view',
params: { taskId, recordId: newRecordId } params: { taskId, recordId: newRecordId }
}" }"
class="button is-primary is-light"
>start a new record</router-link >start a new record</router-link
> >
</template> </template>
<style scoped lang="scss">
.task-record-list {
}
</style>

View File

@@ -2,7 +2,9 @@ import { formatDiffInMinutes } from '@/shared/format-date'
import { computed, isRef, type Ref } from 'vue' import { computed, isRef, type Ref } from 'vue'
import type { TaskRecord } from '../models/task-record' import type { TaskRecord } from '../models/task-record'
export const useTaskRecordMetadata = (record: TaskRecord | Ref<TaskRecord>) => { export const useTaskRecordMetadata = (
record: TaskRecord | Ref<TaskRecord | null>
) => {
const duration = computed(() => { const duration = computed(() => {
const recordValue = isRef(record) ? record.value : record const recordValue = isRef(record) ? record.value : record
if (!recordValue?.end) { if (!recordValue?.end) {

View File

@@ -16,8 +16,11 @@ export const useTaskRecordStore = defineStore('task-record-store', {
records: {} records: {}
}), }),
actions: { actions: {
addRecord(taskRecord: TaskRecord) { addRecord(taskId: string, recordId: string) {
this.records[taskRecord.id] = taskRecord if (recordId in this.records) {
return
}
this.records[recordId] = new TaskRecord(recordId, taskId)
}, },
removeRecord(recordId: string) { removeRecord(recordId: string) {
delete this.records[recordId] delete this.records[recordId]
@@ -51,7 +54,7 @@ export const useTaskRecordStore = defineStore('task-record-store', {
}, },
endStepRecord(params: { recordId: string; stepId: string; end: ISODate }) { endStepRecord(params: { recordId: string; stepId: string; end: ISODate }) {
const stepRecord = const stepRecord =
this.records[params.recordId].stepRecords[params.stepId] this.records[params.recordId]?.stepRecords[params.stepId]
if (!stepRecord) { if (!stepRecord) {
return return
@@ -83,6 +86,10 @@ export const useTaskRecordStore = defineStore('task-record-store', {
}) })
}, },
endRecord(recordId: string) { endRecord(recordId: string) {
if (!this.records[recordId]) {
return
}
this.records[recordId].end = toISODate(new Date()) this.records[recordId].end = toISODate(new Date())
this.currentStepId = null this.currentStepId = null
}, },
@@ -100,6 +107,14 @@ export const useTaskRecordStore = defineStore('task-record-store', {
} }
}) })
} }
},
reset(recordId: string) {
this.currentStepId = null
if (!this.records[recordId]) {
return
}
this.records[recordId].stepRecords = {}
this.records[recordId].end = undefined
} }
}, },
getters: { getters: {
@@ -109,22 +124,20 @@ export const useTaskRecordStore = defineStore('task-record-store', {
.filter((record) => record.taskId === taskId) .filter((record) => record.taskId === taskId)
.map((record) => TaskRecord.fromRecordable(record)) .map((record) => TaskRecord.fromRecordable(record))
}, },
createAndRetrieveTaskRecord() { getTaskRecord() {
return (taskId: string, recordId: string): TaskRecord => { return (recordId: string): TaskRecord | null => {
const hasTaskRecord = !!this.records[recordId] const hasTaskRecord = !!this.records[recordId]
if (hasTaskRecord) { if (hasTaskRecord) {
return TaskRecord.fromRecordable(this.records[recordId]) return TaskRecord.fromRecordable(this.records[recordId])
} }
const newTaskRecord = new TaskRecord(recordId, taskId) return null
this.records[recordId] = newTaskRecord
return newTaskRecord
} }
}, },
getRecord() { getRecord() {
return (recordId: string) => this.records[recordId] ?? null return (recordId: string): Recordable | null =>
this.records[recordId] ?? null
}, },
getStepRecord() { getStepRecord() {
return (recordId: string, stepId: string): StepRecordable | null => return (recordId: string, stepId: string): StepRecordable | null =>

View File

@@ -1,9 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { createUuid } from '@/shared/create-uuid' import { createUuid } from '@/shared/create-uuid'
import { faker } from '@faker-js/faker'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { createStepFixture } from '../models/step.fixture' import type { Stepable } from '../interfaces/stepable'
import { Task } from '../models/task' import { Task } from '../models/task'
import { useTaskStore } from '../stores/useTask.store' import { useTaskStore } from '../stores/useTask.store'
import StepInput from './StepInput.vue' import StepInput from './StepInput.vue'
@@ -13,13 +12,11 @@ const router = useRouter()
const id = createUuid() const id = createUuid()
const title = ref(faker.animal.bird()) const title = ref('')
const link = ref(faker.internet.url()) const link = ref('')
const steps = ref(
Array.from({ length: Math.floor(Math.random() * 10) }, () => const steps = ref<Stepable[]>([])
createStepFixture()
)
)
const totalEstimation = computed(() => const totalEstimation = computed(() =>
steps.value.map((step) => step.estimation).reduce((a, b) => a + b, 0) steps.value.map((step) => step.estimation).reduce((a, b) => a + b, 0)
) )
@@ -46,22 +43,30 @@ const saveTask = () => {
</script> </script>
<template> <template>
<div> <div class="columns is-centered">
<h1>Create a task</h1> <div class="column is-half">
<h2>Estimation: {{ totalEstimation }} minutes</h2> <h1 class="title">Create a task</h1>
<h2 class="subtitle">Estimation: {{ totalEstimation }} minutes</h2>
<form @submit.prevent="saveTask"> <form @submit.prevent="saveTask">
<button type="submit">save task</button> <div class="field">
<div> <label class="label" for="title">Title</label>
<label for="title">Title</label> <div class="control">
<input type="text" id="title" v-model="title" /> <input class="input" type="text" id="title" v-model="title" />
</div>
</div>
<div class="field">
<label class="label" for="link">User story link</label>
<div class="control">
<input class="input" type="text" id="link" v-model="link" />
</div> </div>
<div>
<label for="link">User story link</label>
<input type="text" id="link" v-model="link" />
</div> </div>
<StepInput v-model="steps" /> <StepInput v-model="steps" />
<button class="button is-primary is-fullwidth" type="submit">
save
</button>
</form> </form>
</div> </div>
</div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -29,16 +29,19 @@ const stepsTextarea = computed({
</script> </script>
<template> <template>
<div class="step-input"> <div class="step-input field">
<label for="steps">steps</label> <label class="label" for="steps">steps</label>
<div class="control">
<textarea <textarea
id="steps" id="steps"
name="steps" name="steps"
v-model="stepsTextarea" v-model="stepsTextarea"
cols="40" rows="15"
rows="20" class="textarea"
placeholder="- [step] | <minutes you estimate it will take>"
></textarea> ></textarea>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>

View File

@@ -6,18 +6,25 @@ const taskStore = useTaskStore()
</script> </script>
<template> <template>
<div class="content">
<ul class="task-list"> <ul class="task-list">
<li v-for="task in taskStore.recentTasks" :key="task.id"> <li v-for="task in taskStore.recentTasks" :key="task.id">
<router-link :to="{ name: 'task-view', params: { id: task.id } }">{{ <router-link
task.title :to="{ name: 'task-view', params: { id: task.id } }"
}}</router-link> class="button is-link is-outlined"
| {{ task.totalEstimation }} minutes | >{{ task.title }}</router-link
{{ formatDate(task.date) }} >
<span> {{ task.totalEstimation }} minutes </span>
<span>{{ formatDate(task.date) }}</span>
</li> </li>
</ul> </ul>
</div>
</template> </template>
<style scoped> <style scoped>
.task-list { .task-list li {
display: flex;
align-items: center;
gap: 1rem;
} }
</style> </style>

View File

@@ -7,8 +7,12 @@ const taskStore = useTaskStore()
<template> <template>
<main> <main>
<router-link :to="{ name: 'new-task' }">New task</router-link> <router-link :to="{ name: 'new-task' }" class="button is-primary"
>New task</router-link
>
<task-list /> <task-list />
<button @click="() => taskStore.reset()">reset list</button> <button class="button is-danger" @click="() => taskStore.reset()">
reset list
</button>
</main> </main>
</template> </template>

View File

@@ -12,8 +12,3 @@ defineProps<{
<TaskRecord :task-id="taskId" :record-id="recordId" /> <TaskRecord :task-id="taskId" :record-id="recordId" />
</div> </div>
</template> </template>
<style scoped lang="scss">
.record-view {
}
</style>

View File

@@ -14,27 +14,24 @@ const task = computed(() => taskStore.getTask(props.id))
<template> <template>
<div class="task-view" v-if="task"> <div class="task-view" v-if="task">
<h1>{{ task.title }}</h1> <h1 class="title">{{ task.title }}</h1>
<h2>{{ task.totalEstimation }} minutes</h2> <h2 class="subtitle">{{ task.totalEstimation }} minutes</h2>
<a <a
v-if="task.link" v-if="task.link"
:href="task.link" :href="task.link"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
>User Story link</a class="button is-link"
>user story link</a
> >
<ul> <div class="content is-large">
<ol>
<li v-for="step in task.steps" :key="step.id"> <li v-for="step in task.steps" :key="step.id">
<div>{{ step.title }} | {{ step.estimation }}</div> <div>{{ step.title }} | {{ step.estimation }}</div>
</li> </li>
</ul> </ol>
<hr /> </div>
<task-record-list :task-id="id" /> <task-record-list :task-id="id" />
</div> </div>
<div v-else>Task not found</div> <div v-else>Task not found</div>
</template> </template>
<style scoped>
.task-view {
}
</style>