feat(tasking): add projects feature and restructure backend

- Restructure backend code into Tasking module with organized endpoints
  - Add Project and Sprint entities with database migrations
  - Implement CRUD endpoints for projects (create, get, rename, delete)
  - Refactor task endpoints into Tasking namespace
  - Add integration test suite with Testcontainers and Respawn
  - Refactor frontend to use Pinia stores with dedicated API clients
  - Add DueDatePicker and DueTimePicker components for task scheduling
  - Add environment configuration for API base URL
  - Add infrastructure setup scripts for Docker/Postgres
This commit is contained in:
2025-12-16 15:33:35 -05:00
parent c973237053
commit 5c93d81ad5
67 changed files with 5971 additions and 1653 deletions

6
Tasker.Ui/.env Normal file
View File

@@ -0,0 +1,6 @@
# Development environment variables for Vite
# You can override these locally without committing secrets.
# Vite only exposes variables prefixed with VITE_.
# Backend API base URL for development
VITE_API_BASE_URL=http://localhost:5239/

View File

@@ -0,0 +1,5 @@
# Production environment variables for Vite
# Replace the URL below with your production API endpoint.
# Vite only exposes variables prefixed with VITE_.
VITE_API_BASE_URL=https://api.example.com/

View File

@@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to Vuetify 3</title>
<title>Done By Daylight</title>
</head>
<body>

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,8 @@
"dependencies": {
"@mdi/font": "^7.4.47",
"@vueuse/core": "^10.11.0",
"axios": "^1.6.7",
"axios": "^1.7.7",
"date-fns": "^3.6.0",
"pinia": "^2.1.7",
"uuidv7": "^1.0.1",
"vue": "^3.4.29",

View File

@@ -36,6 +36,9 @@
value="users"
></v-list-item>
</v-list>
<project-section></project-section>
</v-navigation-drawer>
<v-app-bar app
@@ -67,31 +70,14 @@
</v-app>
</template>
m
<script setup lang="ts">
import { ref, watch } from 'vue'
import ProjectSection from '@/components/ProjectSection.vue'
const drawer = ref(true)
const rail = ref(false)
const group = ref(null)
const items = ref([
{
title: 'Foo',
value: 'foo'
},
{
title: 'Bar',
value: 'bar'
},
{
title: 'Fizz',
value: 'fizz'
},
{
title: 'Buzz',
value: 'buzz'
}
])
watch(group, () => {
drawer.value = false

View File

@@ -1,120 +0,0 @@
import { ref, onMounted } from 'vue'
export interface Task {
id: string;
name: string; // [MaxLength(255)]
description?: string | null; // [MaxLength(2048)]
dueDate?: string | null;
}
export interface TaskApiClient {
getTasks(): Promise<Task[]>;
addTask(task: Task): Promise<Task>;
}
export class TaskApiClientImpl implements TaskApiClient {
private readonly baseUrl: string
constructor(baseUrl: string) {
this.baseUrl = baseUrl
}
async getTasks(): Promise<Task[]> {
const response = await fetch(`${this.baseUrl}/tasks`)
if (!response.ok) {
throw new Error('Failed to fetch tasks')
}
const data = await response.json()
return data as Task[]
}
async addTask(task: Task): Promise<Task> {
const response = await fetch(`${this.baseUrl}/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(task)
})
if (!response.ok) {
throw new Error('Failed to add task')
}
const data = await response.json()
return data as Task
}
async completeTask(id: string): Promise<Task> {
const response = await fetch(`${this.baseUrl}/tasks/${id}/complete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'CompleteOn': new Date().toISOString()
})
})
if (!response.ok) {
throw new Error('Failed to add task')
}
const data = await response.json()
return data as Task
}
}
export function useTasks() {
const tasks = ref<Task[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
const taskApiClient = new TaskApiClientImpl('https://localhost:7055')
const fetchTasks = async () => {
isLoading.value = true
error.value = null
try {
tasks.value = await taskApiClient.getTasks()
} catch (err) {
error.value = (err as Error).message
} finally {
isLoading.value = false
}
}
const addTask = async (task: Task) => {
isLoading.value = true
error.value = null
try {
const newTask = await taskApiClient.addTask(task)
tasks.value.push(newTask)
} catch (err) {
error.value = (err as Error).message
} finally {
isLoading.value = false
}
}
const completeTask = async (id: string) => {
isLoading.value = true
error.value = null
try {
await taskApiClient.completeTask(id)
} catch (err) {
error.value = (err as Error).message
} finally {
isLoading.value = false
}
}
onMounted(fetchTasks)
return {
tasks,
isLoading,
error,
fetchTasks,
addTask,
completeTask
}
}

View File

@@ -0,0 +1,160 @@
<template>
<div class="due-date-picker">
<v-card>
<!-- Date Search Input -->
<v-text-field
v-model="searchQuery"
label="Type a due date"
variant="underlined"
></v-text-field>
<v-divider></v-divider>
<!-- Quick Date Selection -->
<v-list>
<v-list-item @click="selectQuickDate('today')">
<v-icon left>mdi-calendar-today</v-icon>
<v-list-item-title>Today</v-list-item-title>
<v-list-item-subtitle>{{ formattedToday }}</v-list-item-subtitle>
</v-list-item>
<v-list-item @click="selectQuickDate('tomorrow')">
<v-icon left>mdi-calendar-arrow-right</v-icon>
<v-list-item-title>Tomorrow</v-list-item-title>
<v-list-item-subtitle>{{ formattedTomorrow }}</v-list-item-subtitle>
</v-list-item>
<v-list-item @click="selectQuickDate('weekend')">
<v-icon left>mdi-calendar-weekend</v-icon>
<v-list-item-title>This weekend</v-list-item-title>
<v-list-item-subtitle>{{ formattedWeekend }}</v-list-item-subtitle>
</v-list-item>
<v-list-item @click="selectQuickDate('nextWeek')">
<v-icon left>mdi-calendar-arrow-right-outline</v-icon>
<v-list-item-title>Next week</v-list-item-title>
<v-list-item-subtitle>{{ formattedNextWeek }}</v-list-item-subtitle>
</v-list-item>
</v-list>
<v-divider></v-divider>
<!-- Date Picker -->
<v-date-picker v-model="selectedDate"></v-date-picker>
<v-btn @click="openTimePicker">Time</v-btn>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text="Cancel"
color="secondary"
@click="cancel"
></v-btn>
<v-btn
text="Set Time"
color="primary"
@click="save"
></v-btn>
</v-card-actions>
</v-card>
<!-- DueTimePicker Component -->
<due-time-picker
v-model="isTimeDialogOpen"
:time="selectedTime"
@cancel=""
@save="handleTimeSave"
/>
</div>
</template>
<script setup lang="ts">
import {ref, computed, defineEmits} from 'vue'
import {format, addDays, nextMonday} from 'date-fns'
import DueTimePicker from './DueTimePicker.vue'
import type { Time } from './Time';
// Reactive state
const selectedDate = ref<Date | null>(null)
const searchQuery = ref<string>('')
const selectedTime = ref<Time | null>(null)
interface DueDateChanged {
selectedDate: Date,
selectedTime: Time,
}
const emit = defineEmits<{
change: [value: DueDateChanged],
update: [value: number]
}>()
// Dialog state for time picker
const isTimeDialogOpen = ref<boolean>(false)
// Methods to get quick dates
const today = new Date()
const tomorrow = addDays(today, 1)
const weekend = addDays(today, 6 - today.getDay())
const nextWeek = nextMonday(today)
// Computed formatted dates
const formattedToday = computed(() => format(today, 'PPPP'))
const formattedTomorrow = computed(() => format(tomorrow, 'PPPP'))
const formattedWeekend = computed(() => format(weekend, 'PPPP'))
const formattedNextWeek = computed(() => format(nextWeek, 'PPPP'))
// Select quick date options
function selectQuickDate(option: 'today' | 'tomorrow' | 'weekend' | 'nextWeek') {
switch (option) {
case 'today':
selectedDate.value = today
break
case 'tomorrow':
selectedDate.value = tomorrow
break
case 'weekend':
selectedDate.value = weekend
break
case 'nextWeek':
selectedDate.value = nextWeek
break
}
}
function save() {
emit('update', 1)
}
function cancel() {
}
// Open Time Picker Dialog
function openTimePicker() {
isTimeDialogOpen.value = true
}
// Handle Save Event from DueTimePicker
function handleTimeSave(time: Time) {
console.log('Time:', time.time)
console.log('Duration:', time.duration)
console.log('Time Zone:', time.timeZone)
}
</script>
<style scoped>
.due-date-picker {
width: 350px;
}
.v-card {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<v-dialog v-model="isTimeDialogOpen" width="400px">
<v-card>
<v-card-title>Time Picker</v-card-title>
<v-card-text>
<!-- Time Picker or Manual Input -->
<v-autocomplete
v-model="selectedTime"
:items="timeOptions"
label="Select or Enter Time"
clearable
@blur="validateTime"
></v-autocomplete>
<!-- Duration Picker -->
<v-select
v-model="selectedDuration"
:items="durations"
label="Duration"
></v-select>
<!-- Time Zone Picker -->
<v-select
v-model="selectedTimeZone"
:items="timeZones"
label="Time zone"
></v-select>
</v-card-text>
<v-card-actions>
<v-btn color="secondary" @click="close">Cancel</v-btn>
<v-btn color="primary" @click="saveTimeSettings">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import {ref} from 'vue';
import {type Time} from './Time';
const emit = defineEmits<{
cancel: [],
ok: [value: Time]
}>()
// Reactive state
const selectedTime = ref<string | null>(null);
const selectedDuration = ref<string | null>(null);
const selectedTimeZone = ref<string | null>('Floating time');
// Duration and Time Zones options
const durations = ref(['No duration', '15 mins', '30 mins', '1 hour', '2 hours']);
const timeZones = ref(['Floating time', 'Pacific Time', 'Mountain Time', 'Central Time', 'Eastern Time']);
// Time options every 15 minutes
const timeOptions = ref(generateTimeOptions());
// Generate time options in 15-minute intervals
function generateTimeOptions() {
const times = [];
for (let hour = 0; hour < 24; hour++) {
for (let minute = 0; minute < 60; minute += 15) {
const time = new Date(0, 0, 0, hour, minute).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
times.push(time);
}
}
return times;
}
// Validate manually entered time
function validateTime() {
if (!selectedTime.value) return;
const isValidTime = timeOptions.value.includes(selectedTime.value);
if (!isValidTime) {
// If the time is not valid, you might want to reset it or show an error
console.log('Invalid time entered');
}
}
// Close the dialog
function close() {
emit('cancel', false);
}
// Save Time, Duration, and Time Zone Settings
function saveTimeSettings() {
emit('ok', {
time: selectedTime.value,
duration: selectedDuration.value,
timeZone: selectedTimeZone.value,
});
close();
}
</script>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { ref } from 'vue'
const showForm = ref(false)
const name = ref('')
const description = ref(null)
function createProject() {
console.log(`Create new project: ${name.value} - ${description.value}`)
}
</script>
<template>
<v-btn v-if="!showForm" @click="showForm = true">
<v-icon left>mdi-plus</v-icon>
Add New Project
</v-btn>
<v-form v-else>
<v-card>
<v-text-field
label="Name"
v-model="name"
required
></v-text-field>
<v-text-field
label="Description"
v-model="description"
></v-text-field>
<v-card-actions>
<v-btn color="success" @click="createProject">
Submit
</v-btn>
<v-btn color="error" @click="showForm = false">
Cancel
</v-btn>
</v-card-actions>
</v-card>
</v-form>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useProjectStore } from '@/stores/projectStore'
const projectStore = useProjectStore()
</script>
<template>
<v-list density="compact" nav>
<v-list-item
v-for="(item, index) in projectStore.projects"
:key="index"
>
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import ProjectList from '@/components/ProjectList.vue'
import ProjectCreate from '@/components/ProjectCreate.vue'
</script>
<template>
<h1>Projects</h1>
<project-list></project-list>
<project-create></project-create>
</template>
<style scoped>
</style>

View File

@@ -1,26 +1,36 @@
<script setup lang="ts">
import { ref } from 'vue'
import { type Task, useTasks } from '@/api/taskApi'
import { uuidv7 } from 'uuidv7'
import DueDatePicker from '@/components/DueDatePicker.vue'
import { useTaskStore } from '@/stores/taskStore'
const name = ref('')
const description = ref(null)
const dueDate = ref(null)
const name = ref<string>('')
const description = ref<string | null>(null)
const dueDate = ref<string | null>(null)
const { addTask } = useTasks()
const modalDueDatePicker = ref<boolean>(false)
const doAddTask = () => {
const newTask: Task = {
function changeDueDate(): void {
modalDueDatePicker.value = !modalDueDatePicker.value
}
function onDueDateChanged(value: string): void {
dueDate.value = value
}
const taskStore = useTaskStore()
function doAddTask(): Task {
taskStore.addTask({
id: uuidv7(),
name: name.value,
description: description.value,
dueDate: dueDate.value
}
addTask(newTask)
} as Task)
}
const cancelAddTask = () => {
}
</script>
@@ -38,19 +48,28 @@ const cancelAddTask = () => {
</v-text-field>
<div class="flex flex-row space-x-2">
<span>due-date</span>
<v-btn @click="changeDueDate()">
<span>due-date</span>
</v-btn>
<v-dialog v-model="modalDueDatePicker"
width="400px">
<due-date-picker :value="dueDate"
@value-changed="onDueDateChanged"
></due-date-picker>
</v-dialog>
<span>priority</span>
<span>tags</span>
<span>reminders</span>
</div>
<div class=" bg-teal-400 p-2 flex flex-row space-x-2 justify-end">
<div class="bg-teal-400 p-2 flex flex-row space-x-2 justify-end">
<span class="justify-start">projects</span>
<div class="bg-red space-x-2 justify-end">
<v-btn @click="cancelAddTask()"
variant="outlined">

View File

@@ -1,44 +1,45 @@
<script setup lang="ts">
import { useTaskStore } from '@/stores/taskStore'
import type { Task } from '@/stores/task'
import { useTasks } from '@/api/taskApi'
const taskStore = useTaskStore()
const { completeTask, tasks, isLoading, error } = useTasks()
function itemCompleted(id: string) {
console.log(`completing task: ${id}`)
completeTask(id)
function toggleTaskComplete(task: Task) {
taskStore.completeTask(task) // Call the API to save the change
}
</script>
<template>
<v-list v-if="!isLoading && !error">
<v-list-item v-for="task in tasks" :key="task.id">
<div class="flex p-1 bg-amber">
<v-form v-if="!taskStore.isLoading && !taskStore.error">
<div class="flex p-0 m-0 bg-amber"
v-for="task in taskStore.tasks" :key="task.id">
<div>
<v-checkbox-btn @change="itemCompleted(task.id)"
></v-checkbox-btn>
</div>
<div class="flex-column">
<div class="">
<div class="font-sans">{{ task.name }}</div>
<div class="font-serif font-normal">Description {{ task.description }}</div>
</div>
<div>
<v-checkbox-btn :model-value="task.completedOn !== null"
@change="toggleTaskComplete(task)"
></v-checkbox-btn>
</div>
<div>{{ task.dueDate }}</div>
<!-- <div>{{ task.project.Name }}</div>-->
</div>
<div class="flex-column">
<div class="">
<div class="font-sans font-medium">{{ task.name }}</div>
<div class="font-sans font-normal">{{ task.description }}</div>
</div>
</v-list-item>
</v-list>
<div>{{ task.dueDate }}</div>
</div>
</div>
</v-form>
<v-alert v-else-if="error" type="error">
{{ error }}
</v-alert>
<v-progress-circular v-else indeterminate></v-progress-circular>
</template>
<style scoped>
</style>
</style>

View File

@@ -0,0 +1,5 @@
export interface Time{
time: string,
timeZone: string,
duration: string,
}

View File

@@ -1,12 +0,0 @@
import {ref, computed} from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@@ -0,0 +1,50 @@
// src/api/axiosClient.ts
import axios from 'axios'
// Resolve API base URL from Vite environment variables.
// Configure via `.env` (development) and `.env.production` (production):
// VITE_API_BASE_URL=https://localhost:7055/
// If not set, it will fall back to current origin.
console.log(`BASE URL: ${import.meta.env.VITE_API_BASE_URL}`);
const envBaseUrl = (import.meta as any)?.env?.VITE_API_BASE_URL as string | undefined
const resolvedBaseUrl = envBaseUrl && envBaseUrl.trim().length > 0
? envBaseUrl
: (typeof window !== 'undefined' ? window.location.origin : '')
console.log(`RESOLVED BASE URL: ${resolvedBaseUrl}`);
// Create an instance of axios
const httpClient = axios.create({
baseURL: resolvedBaseUrl, // API base URL from configuration
headers: {
'Content-Type': 'application/json', // Default headers
},
timeout: 5000, // Set a timeout (optional)
})
// Optionally, add request/response interceptors
httpClient.interceptors.request.use(
(config) => {
// You can modify the request here, e.g., attach auth tokens
// config.headers.Authorization = `Bearer ${yourToken}`
return config
},
(error) => {
return Promise.reject(error)
}
)
httpClient.interceptors.response.use(
(response) => {
return response
},
(error) => {
// Handle errors globally, e.g., log out on 401
if (error.response?.status === 401) {
console.error('Unauthorized, redirect to login')
}
return Promise.reject(error)
}
)
export default httpClient

View File

@@ -0,0 +1,17 @@

export interface Project {
id: string;
name: string; // [MaxLength(255)]
description?: string | null; // [MaxLength(2048)]
dueDate?: string | null;
completedOn?: string | null;
}
export interface CreateProjectRequest {
id: string;
}
export interface ChangeProjectDescription {
id: string;
completedOn: Date;
}

View File

@@ -0,0 +1,77 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { projectsApiClient } from '@/stores/projectsApiClient'
import type { Project } from '@/stores/project'
export const useProjectStore = defineStore(
'projectStore',
() => {
const projects = ref<Project[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
isLoading.value = true
error.value = null
projectsApiClient
.getProjects()
.then(t => projects.value = t)
.catch(e => error.value = (e as Error).message)
.finally(isLoading.value = false)
const fetchProjects = async () => {
isLoading.value = true
error.value = null
try {
projects.value = await projectsApiClient.getProjects()
} catch (err) {
error.value = (err as Error).message
} finally {
isLoading.value = false
}
}
const addProject = async (project: project) => {
isLoading.value = true
error.value = null
try {
const newProject = await projectsApiClient.createProject(project)
projects.value.push(newProject)
} catch (err) {
error.value = (err as Error).message
} finally {
isLoading.value = false
}
}
const completeProject = async (project: Project) => {
isLoading.value = true
error.value = null
try {
if (project.completedOn) {
await projectsApiClient.uncompleteProject(project.id)
project.completedOn = null
} else {
project.completedOn = new Date().toISOString()
await projectsApiClient.completeProject(
{
id: project.id,
completedOn: project.completedOn
})
}
} catch (err) {
error.value = (err as Error).message
} finally {
isLoading.value = false
}
}
return {
projects,
isLoading,
error,
fetchProjects,
addProject,
completeProject
}
}
)

View File

@@ -0,0 +1,44 @@
import type { Project, CreateProjectRequest, CompleteProjectRequest } from '@/stores/project'
import httpClient from '@/stores/httpClient'
export const projectsApiClient = {
// Fetch all Projects
async getProjects(): Promise<Project[]> {
const response = await httpClient.get<Project[]>('/projects')
return response.data
},
// Fetch a single Project by ID
async getProjectById(id: string): Promise<Project> {
const response = await httpClient.get<Project>(`/projects/${id}`)
return response.data
},
// Create a new Project
async createProject(request: CreateProjectRequest): Promise<Project> {
const response = await httpClient.post<Project>('/projects', request)
return response.data
},
// Update an existing Project
async completeProject(request: CompleteProjectRequest): Promise<Project> {
const response = await httpClient.put<Project>(
`/projects/complete`,
request)
return response.data
},
async uncompleteProject(id: string): Promise<Project> {
const response = await httpClient.put<Project>(
`/projects/uncomplete`,
{
id: id
})
return response.data
},
// Delete a Project
async deleteProject(id: string): Promise<void> {
await httpClient.delete(`/projects/${id}`)
},
}

View File

@@ -0,0 +1,17 @@

export interface Task {
id: string;
name: string; // [MaxLength(255)]
description?: string | null; // [MaxLength(2048)]
dueDate?: string | null;
completedOn?: string | null;
}
export interface CreateTaskRequest {
id: string;
}
export interface CompleteTaskRequest {
id: string;
completedOn: Date;
}

View File

@@ -0,0 +1,77 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { tasksApiClient } from '@/stores/tasksApiClient'
import type { Task } from '@/stores/task'
export const useTaskStore = defineStore(
'taskStore',
() => {
const tasks = ref<Task[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
isLoading.value = true
error.value = null
tasksApiClient
.getTasks()
.then(t => tasks.value = t)
.catch(e => error.value = (e as Error).message)
.finally(isLoading.value = false)
const fetchTasks = async () => {
isLoading.value = true
error.value = null
try {
tasks.value = await tasksApiClient.getTasks()
} catch (err) {
error.value = (err as Error).message
} finally {
isLoading.value = false
}
}
const addTask = async (task: Task) => {
isLoading.value = true
error.value = null
try {
const newTask = await tasksApiClient.createTask(task)
tasks.value.push(newTask)
} catch (err) {
error.value = (err as Error).message
} finally {
isLoading.value = false
}
}
const completeTask = async (task: Task) => {
isLoading.value = true
error.value = null
try {
if (task.completedOn) {
await tasksApiClient.uncompleteTask(task.id)
task.completedOn = null
} else {
task.completedOn = new Date().toISOString()
await tasksApiClient.completeTask(
{
id: task.id,
completedOn: task.completedOn
})
}
} catch (err) {
error.value = (err as Error).message
} finally {
isLoading.value = false
}
}
return {
tasks,
isLoading,
error,
fetchTasks,
addTask,
completeTask
}
}
)

View File

@@ -0,0 +1,46 @@
import type { Task, CreateTaskRequest, CompleteTaskRequest } from '@/stores/task'
import httpClient from '@/stores/httpClient'
export const tasksApiClient = {
// Fetch all Tasks
async getTasks(): Promise<Task[]> {
const response = await httpClient.get<Task[]>('/tasks')
return response.data
},
// Fetch a single Task by ID
async getTaskById(id: string): Promise<Task> {
const response = await httpClient.get<Task>(`/tasks/${id}`)
return response.data
},
// Create a new Task
async createTask(request: CreateTaskRequest): Promise<Task> {
const response = await httpClient.post<Task>(
'/tasks',
request)
return response.data
},
// Update an existing Task
async completeTask(request: CompleteTaskRequest): Promise<Task> {
const response = await httpClient.put<Task>(
`/tasks/complete`,
request)
return response.data
},
async uncompleteTask(id: string): Promise<Task> {
const response = await httpClient.put<Task>(
`/tasks/uncomplete`,
{
id: id
})
return response.data
},
// Delete a Task
async deleteTask(id: string): Promise<void> {
await httpClient.delete(`/tasks/${id}`)
}
}

View File

@@ -1,14 +1,16 @@
<script setup lang="ts">
import {useCounterStore} from "@/stores/counter";
import TaskInput from "@/components/TaskInput.vue";
import TaskInput from '@/components/TaskInput.vue'
import TaskList from '@/components/TaskList.vue'
const counterStore = useCounterStore()
</script>
<template>
<task-input></task-input>
<task-list></task-list>
<v-card class="m-2 p-2">
<task-input></task-input>
</v-card>
<v-card class="m-2 p-2">
<task-list></task-list>
</v-card>
</template>