Everywhere: More work on the todo page
This commit is contained in:
+6
-2
@@ -65,12 +65,16 @@ $ bun install -g @vue/language-server
|
|||||||
$ bun install -g @vue/typescript-plugin
|
$ bun install -g @vue/typescript-plugin
|
||||||
|
|
||||||
# Pinia
|
# Pinia
|
||||||
$ bun install pinia
|
$ bun install @pinia/nuxt # also add it to nuxt.config modules!
|
||||||
|
$ bun install pinia-plugin-persistedstate # same as above
|
||||||
|
|
||||||
# PrimeVue
|
# PrimeVue
|
||||||
$ bun install primevue primeicons @primevue/themes
|
$ bun install primevue primeicons @primevue/themes @primevue/forms
|
||||||
$ bun install --dev @primevue/nuxt-module
|
$ bun install --dev @primevue/nuxt-module
|
||||||
|
|
||||||
# Zod
|
# Zod
|
||||||
$ bun install zod
|
$ bun install zod
|
||||||
|
|
||||||
|
# UUID
|
||||||
|
$ bun install uuid
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
|
|||||||
+19
-5
@@ -4,13 +4,30 @@ import Aura from "@primevue/themes/aura";
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2024-11-01",
|
compatibilityDate: "2024-11-01",
|
||||||
css: [
|
css: [
|
||||||
"primeicons/primeicons.css"
|
"primeicons/primeicons.css",
|
||||||
|
"~/assets/css/style.css"
|
||||||
],
|
],
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
|
dir: {
|
||||||
|
public: "../public"
|
||||||
|
},
|
||||||
modules: [
|
modules: [
|
||||||
"@nuxt/eslint",
|
"@nuxt/eslint",
|
||||||
"@primevue/nuxt-module"
|
"@pinia/nuxt",
|
||||||
|
"@primevue/nuxt-module",
|
||||||
|
"pinia-plugin-persistedstate/nuxt",
|
||||||
],
|
],
|
||||||
|
pinia: {
|
||||||
|
storesDirs: ["src/stores/**"] // also auto-import nested directories
|
||||||
|
},
|
||||||
|
piniaPluginPersistedstate: {
|
||||||
|
debug: process.env.NODE_ENV === "development", // log error to console
|
||||||
|
storage: "cookies",
|
||||||
|
cookieOptions: {
|
||||||
|
sameSite: "lax", // prevent CSRF
|
||||||
|
secure: process.env.NODE_ENV !== "development" // only send over HTTPS
|
||||||
|
}
|
||||||
|
},
|
||||||
primevue: {
|
primevue: {
|
||||||
options: {
|
options: {
|
||||||
theme: {
|
theme: {
|
||||||
@@ -18,9 +35,6 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dir: {
|
|
||||||
public: "../public"
|
|
||||||
},
|
|
||||||
srcDir: "src/",
|
srcDir: "src/",
|
||||||
ssr: false
|
ssr: false
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-1
@@ -10,11 +10,14 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@pinia/nuxt": "^0.7.0",
|
||||||
|
"@primevue/forms": "^4.2.3",
|
||||||
"@primevue/themes": "^4.2.3",
|
"@primevue/themes": "^4.2.3",
|
||||||
"nuxt": "^3.14.1592",
|
"nuxt": "^3.14.1592",
|
||||||
"pinia": "^2.2.6",
|
"pinia-plugin-persistedstate": "^4.1.3",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.2.3",
|
"primevue": "^4.2.3",
|
||||||
|
"uuid": "^11.0.3",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-router": "latest",
|
"vue-router": "latest",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
|
<Toast />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/*----------------------------------------*/
|
||||||
|
/* General */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Segoe UI", "DejaVu Sans", sans-serif;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
adasda
|
||||||
|
<Column field="title">
|
||||||
|
<template #header>
|
||||||
|
<span @click="toggleSorting('title')" class="p-datatable-column-title">Title
|
||||||
|
<span :class="['pi', sorting.title === 'asc' ? 'pi-arrow-circle-down' : 'pi-arrow-circle-up']"></span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const sorting = defineModel<Record<string, string>>({ required: true });
|
||||||
|
|
||||||
|
function toggleSorting(key: string) {
|
||||||
|
sorting.value[key] = sorting.value[key] === 'asc' ? 'desc' : 'asc';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<Form v-slot="$form" ref="formRef" :initialValues :resolver @submit="onFormSubmit"
|
||||||
|
class="flex flex-col gap-4 w-full sm:w-56">
|
||||||
|
<!-- {{ $form }} -->
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<InputText @input="initial = false;" v-model="formData.title" name="title" type="text" placeholder="Title"
|
||||||
|
autocomplete="off" fluid />
|
||||||
|
<Message v-if="$form.title?.invalid" severity="error" size="small" variant="simple">
|
||||||
|
{{ $form.title.error?.message }}
|
||||||
|
</Message>
|
||||||
|
</div>
|
||||||
|
<Button :disabled="initial || !$form.valid" class="fr" type="submit" severity="secondary" label="Submit" />
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useTodoStore } from "@/stores/todoStore";
|
||||||
|
import { todoSchema } from "@/schemas/todo";
|
||||||
|
import { zodResolver } from '@primevue/forms/resolvers/zod';
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { type FormSubmitEvent } from "@primevue/forms/form";
|
||||||
|
|
||||||
|
const store = useTodoStore();
|
||||||
|
const toast = useToast();
|
||||||
|
const resolver = zodResolver(todoSchema);
|
||||||
|
|
||||||
|
const initial = ref(true); // makes submit button disabled
|
||||||
|
const initialValues = ref({
|
||||||
|
title: ""
|
||||||
|
});
|
||||||
|
const formRef = ref<HTMLElement | null>(null);
|
||||||
|
const formData = ref({ ...initialValues.value }); // copy data from initialValues
|
||||||
|
|
||||||
|
async function onFormSubmit(e: FormSubmitEvent) {
|
||||||
|
if (e.valid) {
|
||||||
|
toast.add({ severity: "success", summary: "Todo added", life: 3000 });
|
||||||
|
|
||||||
|
store.todos.push({
|
||||||
|
id: uuidv4(),
|
||||||
|
title: e.values?.title || e.states?.title.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset the form
|
||||||
|
initial.value = true;
|
||||||
|
formData.value = { ...initialValues.value }; // copy data from initialValues
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.add({ severity: "error", summary: "Invalid request", life: 3000 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fr {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<ConfirmDialog></ConfirmDialog>
|
||||||
|
<DataTable :value="computedTodos" tableStyle="min-width: 50rem" @sort="onSort" sortMode="multiple" removableSort>
|
||||||
|
<Column field="number" header="#"></Column>
|
||||||
|
<Column field="id" header="ID" sortable filterField="id" showFilterMenu filter="true"></Column>
|
||||||
|
<Column field="title" header="Title" sortable filterField="title" showFilterMenu filter="true"></Column>
|
||||||
|
<Column header="Modifier">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<a @click="removeTodo(slotProps.data.id)">
|
||||||
|
<span class="pi pi-trash" style="color: var(--p-red-600);"></span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useConfirm } from "primevue/useconfirm";
|
||||||
|
import { useToast } from "primevue/usetoast";
|
||||||
|
import { useTodoStore } from "@/stores/todoStore";
|
||||||
|
|
||||||
|
import type { Todo } from "@/schemas/todo";
|
||||||
|
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const store = useTodoStore();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const onSort = (event: any) => {
|
||||||
|
console.log(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sorting = ref<Record<string, string>>({
|
||||||
|
id: "asc",
|
||||||
|
title: "asc",
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleSorting(key: string) {
|
||||||
|
sorting.value[key] = sorting.value[key] === 'asc' ? 'desc' : 'asc';
|
||||||
|
};
|
||||||
|
|
||||||
|
function sortTodos(): Todo[] {
|
||||||
|
let todos: Todo[] = JSON.parse(JSON.stringify(store.todos)); // deep copy
|
||||||
|
|
||||||
|
// Number
|
||||||
|
|
||||||
|
// ID
|
||||||
|
|
||||||
|
// Title
|
||||||
|
todos = (sorting.value.title === "desc")
|
||||||
|
? todos.sort((a, b) => b.title.localeCompare(a.title))
|
||||||
|
: todos.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
|
||||||
|
return todos;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedTodos = computed(() => {
|
||||||
|
return sortTodos().map((todo, index) => ({
|
||||||
|
number: index + 1,
|
||||||
|
...todo
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeTodo = (id: String) => {
|
||||||
|
confirm.require({
|
||||||
|
message: "Do you want to delete this todo?",
|
||||||
|
header: "Confirmation",
|
||||||
|
icon: "pi pi-info-circle",
|
||||||
|
rejectLabel: "Cancel",
|
||||||
|
rejectProps: {
|
||||||
|
label: "Cancel",
|
||||||
|
severity: "secondary",
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: "Delete",
|
||||||
|
severity: "danger"
|
||||||
|
},
|
||||||
|
accept: () => {
|
||||||
|
toast.add({ severity: "info", summary: "Confirmed", detail: "Todo deleted", life: 3000 });
|
||||||
|
store.todos = store.todos.filter(todo => todo.id !== id);
|
||||||
|
},
|
||||||
|
reject: () => {}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
+10
-16
@@ -1,24 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<TodoBrowse />
|
||||||
<DataTable :value="todos" tableStyle="min-width: 50rem">
|
<br>
|
||||||
<Column field="number" header="#"></Column>
|
<TodoAdd />
|
||||||
<Column field="id" header="ID"></Column>
|
<br>
|
||||||
<Column field="title" header="Title"></Column>
|
<Button @click="showToast()" type="button">Toast</Button>
|
||||||
<Column field="modifier" header="Modifier"></Column>
|
|
||||||
</DataTable>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { useToast } from "primevue/usetoast";
|
||||||
// import { ProductService } from '@/service/ProductService';
|
|
||||||
|
|
||||||
onMounted(() => {
|
const toast = useToast();
|
||||||
todos.value = [
|
|
||||||
{ number: "1", id: "toby", title: "Do stuff..", modifier: "hehe"}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const todos = ref();
|
async function showToast() {
|
||||||
|
toast.add({ severity: "success", summary: "This is a toast", detail: "Wow!", life: 3000 });
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const todoSchema = z.object({
|
||||||
|
id: z.optional(z.string().uuid()),
|
||||||
|
title: z.string().min(1, "Title can't be empty"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Todo = z.infer<typeof todoSchema>;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineStore } from "pinia"
|
||||||
|
import type { Todo } from "@/schemas/todo"
|
||||||
|
|
||||||
|
export const useTodoStore = defineStore("todo", () => {
|
||||||
|
|
||||||
|
const todos = ref<Todo[]>([
|
||||||
|
{ id: "d8681644-74d0-4a30-90db-06baa277d0a0", title: "laundry" },
|
||||||
|
{ id: "03c5bf55-f528-43a2-89a1-1a1afb0fa4f6", title: "feed pet" },
|
||||||
|
{ id: "356cd252-bef8-4a1c-ba81-5a68d89df56e", title: "run" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { todos }
|
||||||
|
}, {
|
||||||
|
persist: process.env.NODE_ENV === 'development' ? true : false,
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user