kanban style app

This commit is contained in:
2026-05-17 21:08:41 -04:00
parent 1f4f4210b3
commit 8dc99de4a7
10 changed files with 568 additions and 76 deletions
+23 -68
View File
@@ -1,74 +1,29 @@
<template> <template>
<div class="button-group"> <header>
<MyButton size="sm"> <h1>Task Board</h1>
Primary Small </header>
</MyButton> <main class="three-col-layout">
<MyButton> <section id="todo-section" aria-labelledby="todo-header">
Primary Medium <h2 id="todo-header">To-do</h2>
</MyButton> <TaskList id="todo-list" :tasks="todoTasks" />
<MyButton size="lg"> </section>
Primary Large <section id="inprogress-section" aria-labelledby="inprogress-header">
</MyButton> <h2 id="inprogress-header">In-progress</h2>
</div> <TaskList id="inprogress-list" :tasks="inProgressTasks" />
</section>
<div class="button-group"> <section id="done-section" aria-labelledby="done-header">
<MyButton variant="secondary" <h2 id="done-header">Done</h2>
size="sm"> <TaskList id="done-list" :tasks="doneTasks" />
Secondary Small </section>
</MyButton> </main>
<MyButton variant="secondary"> <footer></footer>
Secondary Medium
</MyButton>
<MyButton variant="secondary"
size="lg">
Secondary Large
</MyButton>
</div>
<div class="button-group">
<MyButton variant="danger"
size="sm">
Danger Small
</MyButton>
<MyButton variant="danger">
Danger Medium
</MyButton>
<MyButton variant="danger"
size="lg">
Danger Large
</MyButton>
</div>
<div class="button-group">
<MyButton variant="danger"
size="lg"
:loading="true">
Danger Large
</MyButton>
<MyButton size="lg"
:disabled="true">
Disabled Large
</MyButton>
</div>
<div class="button-group">
<MyButton size="lg"
:fullwidth="true">
Primary Fullwidth
</MyButton>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import MyButton from './components/MyButton.vue'; import { storeToRefs } from "pinia";
import { useTasksStore } from "./stores/tasks";
import TaskList from "./components/TaskList.vue";
const tasksStore = useTasksStore();
const { todoTasks, inProgressTasks, doneTasks } = storeToRefs(tasksStore);
</script> </script>
<style scoped>
.button-group {
display: flex;
align-items: center;
gap: 1rem;
margin-block-start: 1rem;
}
</style>
@@ -0,0 +1,39 @@
<template>
<div v-if="task.assignee">
<dt>Assignee:</dt>
<dd>
<form :id="`${task.id}-assignee-form`">
<select
:id="`${task.id}-assignee-select`"
:value="task.assignee.id"
@change="updateTaskAssignee(task.id, $event)"
>
<option
v-for="user in users"
:key="user.id"
:value="user.id"
>
{{ user.name }}
</option>
</select>
</form>
</dd>
</div>
</template>
<script setup lang="ts">
import { useTasksStore } from "@/stores/tasks";
import type { TaskItem } from "@/types";
interface TaskAssigneeProps {
task: TaskItem;
}
const { task } = defineProps<TaskAssigneeProps>();
const { users, assignTask } = useTasksStore();
const updateTaskAssignee = (taskId: string, event: Event) => {
const target = event.target as HTMLSelectElement;
assignTask(taskId, target.value);
};
</script>
@@ -0,0 +1,38 @@
<template>
<div>
<dt>Priority:</dt>
<dd>
<form :id="`${task.id}-priority-form`">
<select
:id="`${task.id}-priority-select`"
:value="task.priority"
@change="updateTaskPriority(task.id, $event)"
>
<option
v-for="priority in TASK_PRIORITIES"
:key="priority"
:value="priority"
>
{{ priority }}
</option>
</select>
</form>
</dd>
</div>
</template>
<script setup lang="ts">
import { useTasksStore } from "@/stores/tasks";
import { TASK_PRIORITIES, type TaskItem, type TaskPriority } from "@/types";
interface TaskAssigneeProps {
task: TaskItem;
}
const { task } = defineProps<TaskAssigneeProps>();
const { updatePriority } = useTasksStore();
const updateTaskPriority = (taskId: string, event: Event) => {
const target = event.target as HTMLSelectElement;
updatePriority(taskId, target.value as TaskPriority);
};
</script>
+34
View File
@@ -0,0 +1,34 @@
<template>
<div>
<dt>Status:</dt>
<dd>
<form :id="`${task.id}-status-form`">
<select
:id="`${task.id}-status-select`"
:value="task.status"
@change="updateTaskStatus(task.id, $event)"
>
<option v-for="status in TASK_STATUSES" :key="status">
{{ status }}
</option>
</select>
</form>
</dd>
</div>
</template>
<script setup lang="ts">
import { useTasksStore } from "@/stores/tasks";
import { TASK_STATUSES, type TaskItem, type TaskStatus } from "@/types";
interface TaskAssigneeProps {
task: TaskItem;
}
const { task } = defineProps<TaskAssigneeProps>();
const { updateStatus } = useTasksStore();
const updateTaskStatus = (taskId: string, event: Event) => {
const target = event.target as HTMLSelectElement;
updateStatus(taskId, target.value as TaskStatus);
};
</script>
+16
View File
@@ -0,0 +1,16 @@
<template>
<ul id="todo-list" class="card-list">
<TaskListItem v-for="task in tasks" :key="task.id" :task="task" />
</ul>
</template>
<script setup lang="ts">
import type { TaskItem } from "@/types";
import TaskListItem from "./TaskListItem.vue";
interface TaskListProps {
tasks: TaskItem[];
}
const { tasks } = defineProps<TaskListProps>();
</script>
+64
View File
@@ -0,0 +1,64 @@
<template>
<li
class="card"
:class="{
'priority-high': task.priority === 'high',
'priority-medium': task.priority === 'medium',
'priority-low': task.priority === 'low',
}"
>
<div class="card-header">
<h3>{{ task.title }}</h3>
</div>
<dl>
<AssigneeDetails :task />
<StatusDetails :task />
<PriorityDetails :task />
<div>
<dt>Created At:</dt>
<dd>{{ formattedCreatedAt }}</dd>
</div>
<div>
<dt>Updated At:</dt>
<dd>{{ formattedUpdatedAt }}</dd>
</div>
</dl>
</li>
</template>
<script setup lang="ts">
import { computed } from "vue";
import type { TaskItem } from "@/types";
import AssigneeDetails from "./Details/AssigneeDetails.vue";
import StatusDetails from "./Details/StatusDetails.vue";
import PriorityDetails from "./Details/PriorityDetails.vue";
interface TaskListItemProps {
task: TaskItem;
}
const { task } = defineProps<TaskListItemProps>();
const formatterOptions = {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
timeZoneName: "short",
timeZone: "America/New_York",
} as const;
const formattedCreatedAt = computed(() =>
new Intl.DateTimeFormat("en-US", formatterOptions).format(
new Date(task.createdAt),
),
);
const formattedUpdatedAt = computed(() =>
new Intl.DateTimeFormat("en-US", formatterOptions).format(
new Date(task.updatedAt),
),
);
</script>
+10 -8
View File
@@ -1,12 +1,14 @@
import { createApp } from 'vue' import { createApp } from "vue";
import { createPinia } from 'pinia' import { createPinia } from "pinia";
import App from './App.vue' import App from "./App.vue";
import router from './router' import router from "./router";
const app = createApp(App) import "./styles/main.css";
app.use(createPinia()) const app = createApp(App);
app.use(router)
app.mount('#app') app.use(createPinia());
app.use(router);
app.mount("#app");
+221
View File
@@ -0,0 +1,221 @@
import { ref, computed } from "vue";
import { defineStore } from "pinia";
import type {
TaskItem,
TaskStatus,
TaskPriority,
TaskTag,
TaskUser,
} from "@/types";
export const useTasksStore = defineStore("tasks", () => {
const users = ref<TaskUser[]>([
{ id: "user-1", name: "Ryan Trimble" },
{ id: "user-2", name: "Vickie Chinnick" },
{ id: "user-3", name: "James Okafor" },
{ id: "user-4", name: "Sara Mendez" },
]);
const tags = ref<TaskTag[]>([
{ id: "tag-1", name: "project-1" },
{ id: "tag-2", name: "project-2" },
{ id: "tag-3", name: "bug" },
{ id: "tag-4", name: "feature" },
{ id: "tag-5", name: "design" },
{ id: "tag-6", name: "devops" },
]);
const tasks = ref<TaskItem[]>([
{
id: "task-1",
title: "Set up CI/CD pipeline",
assignee: { id: "user-3", name: "James Okafor" },
status: "done",
priority: "high",
tags: [{ id: "tag-6", name: "devops" }],
createdAt: "2026-05-10T08:00:00Z",
updatedAt: "2026-05-14T10:30:00Z",
},
{
id: "task-2",
title: "Design onboarding flow mockups",
assignee: { id: "user-4", name: "Sara Mendez" },
status: "done",
priority: "medium",
tags: [{ id: "tag-5", name: "design" }],
createdAt: "2026-05-11T09:00:00Z",
updatedAt: "2026-05-15T14:00:00Z",
},
{
id: "task-3",
title: "Implement authentication module",
assignee: { id: "user-1", name: "Ryan Trimble" },
status: "in-progress",
priority: "high",
tags: [
{ id: "tag-4", name: "feature" },
{ id: "tag-1", name: "project-1" },
],
createdAt: "2026-05-12T10:00:00Z",
updatedAt: "2026-05-17T09:15:00Z",
},
{
id: "task-4",
title: "Fix broken pagination on dashboard",
assignee: { id: "user-2", name: "Vickie Chinnick" },
status: "in-progress",
priority: "high",
tags: [{ id: "tag-3", name: "bug" }],
createdAt: "2026-05-13T11:00:00Z",
updatedAt: "2026-05-17T08:00:00Z",
},
{
id: "task-5",
title: "Write unit tests for task store",
assignee: { id: "user-1", name: "Ryan Trimble" },
status: "in-progress",
priority: "medium",
tags: [{ id: "tag-1", name: "project-1" }],
createdAt: "2026-05-14T12:00:00Z",
updatedAt: "2026-05-16T16:45:00Z",
},
{
id: "task-6",
title: "Add dark mode support",
assignee: { id: "user-4", name: "Sara Mendez" },
status: "to-do",
priority: "low",
tags: [
{ id: "tag-5", name: "design" },
{ id: "tag-4", name: "feature" },
],
createdAt: "2026-05-15T08:30:00Z",
updatedAt: "2026-05-15T08:30:00Z",
},
{
id: "task-7",
title: "Migrate database to PostgreSQL",
assignee: { id: "user-3", name: "James Okafor" },
status: "to-do",
priority: "high",
tags: [
{ id: "tag-6", name: "devops" },
{ id: "tag-2", name: "project-2" },
],
createdAt: "2026-05-15T09:00:00Z",
updatedAt: "2026-05-15T09:00:00Z",
},
{
id: "task-8",
title: "Audit and update dependencies",
assignee: { id: "user-2", name: "Vickie Chinnick" },
status: "to-do",
priority: "low",
tags: [{ id: "tag-6", name: "devops" }],
createdAt: "2026-05-16T10:00:00Z",
updatedAt: "2026-05-16T10:00:00Z",
},
{
id: "task-9",
title: "Fix null reference error on profile page",
assignee: { id: "user-1", name: "Ryan Trimble" },
status: "to-do",
priority: "high",
tags: [
{ id: "tag-3", name: "bug" },
{ id: "tag-2", name: "project-2" },
],
createdAt: "2026-05-17T07:00:00Z",
updatedAt: "2026-05-17T07:00:00Z",
},
{
id: "task-10",
title: "Document REST API endpoints",
assignee: { id: "user-4", name: "Sara Mendez" },
status: "to-do",
priority: "medium",
tags: [
{ id: "tag-1", name: "project-1" },
{ id: "tag-2", name: "project-2" },
],
createdAt: "2026-05-17T08:00:00Z",
updatedAt: "2026-05-17T08:00:00Z",
},
]);
const findTask = (taskId: string): TaskItem | undefined =>
tasks.value.find((task) => task.id === taskId);
const findUser = (userId: string): TaskUser | undefined =>
users.value.find((user) => user.id === userId);
const updateTimestamp = (task: TaskItem) => {
task.updatedAt = new Date().toISOString();
};
const addTask = (newTask: TaskItem) => {
if (!newTask) return;
tasks.value = [...tasks.value, newTask];
};
const assignTask = (taskId: string, userId: string) => {
const task = findTask(taskId);
const user = findUser(userId);
if (task && task.assignee) {
task.assignee = user as TaskUser;
updateTimestamp(task);
}
};
const updateStatus = (taskId: string, newStatus: TaskStatus) => {
const task = findTask(taskId);
if (!task) return;
task.status = newStatus;
updateTimestamp(task);
};
const updatePriority = (taskId: string, newPriority: TaskPriority) => {
const task = findTask(taskId);
if (!task) return;
task.priority = newPriority;
updateTimestamp(task);
};
const todoTasks = computed(() => {
return tasks.value
.filter((task) => task.status === "to-do")
.sort((a, b) => a.id.localeCompare(b.id));
});
const inProgressTasks = computed(() => {
return tasks.value
.filter((task) => task.status === "in-progress")
.sort((a, b) => a.id.localeCompare(b.id));
});
const doneTasks = computed(() => {
return tasks.value
.filter((task) => task.status === "done")
.sort((a, b) => a.id.localeCompare(b.id));
});
return {
tags,
tasks,
users,
addTask,
assignTask,
updatePriority,
updateStatus,
todoTasks,
inProgressTasks,
doneTasks,
};
});
+97
View File
@@ -0,0 +1,97 @@
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
height: 100svh;
overflow: clip;
padding: 0;
margin: 0;
}
header {
height: 60px;
display: grid;
place-content: center;
h1 {
padding: 0;
margin: 0;
}
}
.three-col-layout {
display: grid;
height: 100%;
margin-inline: 1rem;
gap: 1rem;
@media screen and (min-width: 640px) {
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr;
}
}
.card-list {
display: flex;
flex-direction: column;
height: calc(100vh - 150px);
list-style: "";
margin: 0;
overflow-y: auto;
padding: 0;
scrollbar-gutter: stable;
h2 {
padding: 0;
margin: 0;
height: 30px;
display: flex;
align-items: center;
}
> * + * {
margin-block-start: 1rem;
}
}
.card {
border-radius: 0.25rem;
margin-inline: 0.5rem;
padding: 1rem;
.card-header {
display: flex;
justify-content: space-between;
}
h3 {
padding: 0;
margin: 0;
}
dl {
> * {
margin-block-start: 0.5rem;
}
}
dt {
font-weight: bold;
}
dd {
padding: 0;
margin: 0;
}
&.priority-high {
background-color: lightcoral;
}
&.priority-medium {
background-color: lightgreen;
}
&.priority-low {
background-color: lightgoldenrodyellow;
}
}
+26
View File
@@ -0,0 +1,26 @@
export const TASK_STATUSES = ["to-do", "in-progress", "done"] as const;
export const TASK_PRIORITIES = ["low", "medium", "high"] as const;
export type TaskStatus = (typeof TASK_STATUSES)[number];
export type TaskPriority = (typeof TASK_PRIORITIES)[number];
export interface TaskUser {
id: string;
name: string;
}
export interface TaskTag {
id: string;
name: string;
}
export interface TaskItem {
id: string;
title: string;
status: TaskStatus;
assignee: TaskUser;
priority: TaskPriority;
tags?: TaskTag[];
createdAt: string;
updatedAt: string;
}