feat: basic ui and workflow
TS Lint / ESLint (push) Failing after 38s
TS Lint / Oxlint (push) Successful in 42s
TS Lint / TypeScript (push) Successful in 43s

This commit is contained in:
2026-07-06 00:22:46 +02:00
parent 56a41989f0
commit 73067f51ce
8 changed files with 125 additions and 59 deletions
+3
View File
@@ -5,6 +5,7 @@
"": { "": {
"name": "tout-doux", "name": "tout-doux",
"dependencies": { "dependencies": {
"@lucide/vue": "^1.23.0",
"vue": "^3.5.38", "vue": "^3.5.38",
}, },
"devDependencies": { "devDependencies": {
@@ -141,6 +142,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@lucide/vue": ["@lucide/vue@1.23.0", "", { "peerDependencies": { "vue": ">=3.0.1" } }, "sha512-9SIYeY5K+R1iv8F8JUKMGSL7Pck/86BJ8djtZz/lnYsoHtKFUj1H2z6+PvKnC/8blZ8tqTDeDXAoTKobuX80hg=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
+1 -2
View File
@@ -1,8 +1,7 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<!-- TODO: 1. rename app --> <title>Tout Doux</title>
<title>Change this you moron</title>
<link rel="stylesheet" href="/resources/css/style.css" /> <link rel="stylesheet" href="/resources/css/style.css" />
<!-- <link rel="icon" href="/favicon.ico"> --> <!-- <link rel="icon" href="/favicon.ico"> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+1
View File
@@ -20,6 +20,7 @@
"lint-fix:oxlint": "oxlint . --fix" "lint-fix:oxlint": "oxlint . --fix"
}, },
"dependencies": { "dependencies": {
"@lucide/vue": "^1.23.0",
"vue": "^3.5.38" "vue": "^3.5.38"
}, },
"devDependencies": { "devDependencies": {
+11 -1
View File
@@ -1,2 +1,12 @@
@import url("https://fonts.googleapis.com/css2?family=Sour+Gummy:ital,wght@0,100..900;1,100..900&display=swap");
@import "tailwindcss"; @import "tailwindcss";
@plugin "daisyui"; @plugin "daisyui" {
themes: cupcake --default;
}
body {
font-family: "Sour Gummy", sans-serif;
font-optical-sizing: auto;
font-style: normal;
font-variation-settings: "wdth" 100;
}
+40 -49
View File
@@ -1,36 +1,45 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onBeforeMount } from "vue"; import { ref, onMounted, onBeforeMount } from "vue";
import { getComments, postComment } from "@/api/comments"; import type { Task } from "@/types";
import type { Comment } from "@/types"; import { getTasks, createTask } from "./api/tasks";
import { CirclePlus } from "@lucide/vue";
import TaskItem from "@/components/TaskItem.vue";
const visible = ref<boolean>(false); const visible = ref<boolean>(false);
const comments = ref<Comment[]>([]); const taskInput = ref<string>("");
const commentInput = ref<string>(""); const tasks = ref<Task[]>([]);
const list = ref<string>("test");
function fetchComments() { function fetchTasks() {
getComments() getTasks(list.value)
.then((data: Comment[]) => { .then((data: Task[]) => {
comments.value = data; tasks.value = data;
setTimeout(fetchTasks, 10000);
}) })
.catch(() => { .catch(() => {
//ignore // ignore
}); });
} }
function sendComment() { function newTask() {
if (commentInput.value.trim()) { if (taskInput.value.trim()) {
postComment(commentInput.value.trim()) createTask(list.value, {
.finally(() => { name: taskInput.value.trim(),
commentInput.value = ""; reset_cron: null,
fetchComments(); })
.then((data: Task) => {
tasks.value.push(data);
}) })
.catch(() => { .catch(() => {
//ignore //ignore
})
.finally(() => {
taskInput.value = "";
}); });
} }
} }
onBeforeMount(fetchComments); onBeforeMount(fetchTasks);
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {
@@ -43,52 +52,34 @@ onMounted(() => {
<main :style="{ display: visible ? 'inherit' : 'none' }"> <main :style="{ display: visible ? 'inherit' : 'none' }">
<div class="hero bg-base-200 min-h-screen"> <div class="hero bg-base-200 min-h-screen">
<div class="hero-content text-center"> <div class="hero-content text-center">
<div class="max-w-md"> <div class="max-w-md min-w-96">
<ul class="list bg-base-100 rounded-box shadow-md"> <ul class="list bg-base-100 rounded-box shadow-md">
<li class="p-4 pb-2 text-xl opacity-60 tracking-wide"> <li
Comments class="p-4 pb-2 text-4xl font-bold opacity-60 tracking-wide select-none"
>
Tout Doux
</li> </li>
<li <li v-for="(task, i) in tasks" :key="task.id">
v-for="comment in comments" <TaskItem v-if="tasks[i]" v-model="tasks[i]" />
:key="comment.id"
class="list-row"
>
<div class="avatar avatar-placeholder">
<div
class="bg-neutral text-neutral-content size-10 rounded-box"
>
<span class="text-xs">{{
comment.content[0]
}}</span>
</div>
</div>
<div>
<div class="text-xs opacity-60">
{{ comment.created_at }}
</div>
<div class="font-semibold">
{{ comment.content }}
</div>
</div>
</li> </li>
<li class="list-row"> <li class="list-row">
<div> <div class="list-col-grow">
<input <input
v-model="commentInput" v-model="taskInput"
type="text" type="text"
placeholder="Type here" placeholder="New Task"
class="input" class="input"
@keyup.enter="sendComment" @keyup.enter="newTask"
/> />
</div> </div>
<div> <div>
<button <button
class="btn" class="btn btn-square"
:disabled="commentInput.trim().length === 0" :disabled="taskInput.trim().length === 0"
@click="sendComment" @click="newTask"
> >
Send <CirclePlus />
</button> </button>
</div> </div>
</li> </li>
+19 -7
View File
@@ -1,32 +1,44 @@
import type { RawTask, TaskCreateData, TaskUpdateData } from "@/types"; import type { RawTask, Task, TaskCreateData, TaskUpdateData } from "@/types";
export async function getTasks(list: string): Promise<RawTask[]> { function parseTask(task: RawTask): Task {
return {
id: task.id,
name: task.name,
reset_cron: task.reset_cron,
check_date: task.check_date ? new Date(task.check_date) : null,
previous_check_date: task.previous_check_date
? new Date(task.previous_check_date)
: null,
};
}
export async function getTasks(list: string): Promise<Task[]> {
const response = await fetch(`/api/lists/${list}/tasks`); const response = await fetch(`/api/lists/${list}/tasks`);
return await response.json(); return (await response.json()).map(parseTask);
} }
export async function createTask( export async function createTask(
list: string, list: string,
data: TaskCreateData, data: TaskCreateData,
): Promise<RawTask> { ): Promise<Task> {
const response = await fetch(`/api/lists/${list}/tasks`, { const response = await fetch(`/api/lists/${list}/tasks`, {
method: "POST", method: "POST",
body: JSON.stringify({ ...data }), body: JSON.stringify({ ...data }),
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });
return await response.json(); return parseTask(await response.json());
} }
export async function updateTask( export async function updateTask(
taskId: string, taskId: string,
data: TaskUpdateData, data: TaskUpdateData,
): Promise<RawTask> { ): Promise<Task> {
const response = await fetch(`/api/tasks/${taskId}`, { const response = await fetch(`/api/tasks/${taskId}`, {
method: "PUT", method: "PUT",
body: JSON.stringify({ ...data }), body: JSON.stringify({ ...data }),
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });
return await response.json(); return parseTask(await response.json());
} }
export async function deleteTask(taskId: string): Promise<void> { export async function deleteTask(taskId: string): Promise<void> {
+42
View File
@@ -0,0 +1,42 @@
<script setup lang="ts">
import { ref } from "vue";
import type { Task } from "@/types";
import { Copy, Trash, Check, X } from "@lucide/vue";
const editMode = ref<boolean>(false);
const task = defineModel<Task>({ required: true });
</script>
<template>
<div class="list-row text-left">
<div class="content-center">
<input type="checkbox" class="checkbox" />
</div>
<div
v-if="!editMode"
class="cursor-pointer text-lg select-none"
@click.prevent="editMode = true"
>
{{ task.name }}
</div>
<template v-else>
<div>
<input v-model="task.name" type="text" class="input mb-1" />
<br />
<button class="btn btn-square me-1" @click="editMode = false">
<X />
</button>
<button class="btn btn-square me-1">
<Check />
</button>
<button class="btn btn-square me-1">
<Copy />
</button>
<button class="btn btn-square">
<Trash />
</button>
</div>
</template>
</div>
</template>
+8
View File
@@ -16,6 +16,14 @@ export interface RawTask {
previous_check_date: string | null; previous_check_date: string | null;
} }
export interface Task {
id: string;
name: string;
reset_cron: string | null;
check_date: Date | null;
previous_check_date: Date | null;
}
export interface TaskCreateData { export interface TaskCreateData {
name: string; name: string;
reset_cron: string | null; reset_cron: string | null;