Initial: Basic functionality with fancy UI

This commit is contained in:
NetMan 2024-02-14 00:06:52 +01:00
commit fe864bb51a
26 changed files with 4202 additions and 0 deletions

14
.eslintrc.cjs Normal file
View File

@ -0,0 +1,14 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

35
README.md Normal file
View File

@ -0,0 +1,35 @@
# odin-todo-vue
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

12
index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To-Do</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

3569
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "odin-todo-vue",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"lucide-vue-next": "^0.330.0",
"v-dropdown-menu": "^2.0.4",
"vue": "^3.4.15",
"vue-router": "^4.2.5",
"vue3-dropdown-navbar": "^0.1.4",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^8.0.0",
"autoprefixer": "^10.4.17",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"postcss": "^8.4.35",
"prettier": "^3.0.3",
"tailwindcss": "^3.4.1",
"vite": "^5.0.11"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

17
src/App.vue Normal file
View File

@ -0,0 +1,17 @@
<script setup>
import NavBar from './components/NavBar.vue'
import ProjectsView from './views/ProjectsView.vue'
import TasksView from './views/TasksView.vue'
</script>
<template>
<NavBar />
<div class="wrapper">
<ProjectsView />
<TasksView />
</div>
</template>
<style scoped>
</style>

26
src/components/NavBar.vue Normal file
View File

@ -0,0 +1,26 @@
<script setup>
import {
TheDropDownNavbar,
TheDropDownItem,
} from "vue3-dropdown-navbar";
let currentPage = "Projects";
</script>
<template>
<TheDropDownNavbar class="">
<template #logo>
<h1 class="text-2xl">{{ currentPage }}</h1>
</template>
<TheDropDownItem link="#" class="text-lg">Projects</TheDropDownItem>
<TheDropDownItem link="#" class="text-lg">Tasks</TheDropDownItem>
<TheDropDownItem link="#" class="text-lg">Somethings</TheDropDownItem>
</TheDropDownNavbar>
</template>
<style scoped>
</style>

6
src/current.js Normal file
View File

@ -0,0 +1,6 @@
import { ref, toRaw } from 'vue'
import { Project } from './project';
let currentProject = ref(null);
export {currentProject}

9
src/index.css Normal file
View File

@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.dd-nav-dark {
@apply text-slate-50 bg-slate-800;
}
}

35
src/instances/Project.vue Normal file
View File

@ -0,0 +1,35 @@
<script setup>
import { X, GripVertical, MoreVertical, Edit3 } from 'lucide-vue-next';
import DropdownMenu from 'v-dropdown-menu';
// import 'v-dropdown-menu/dist/vue3/v-dropdown-menu.css'
const props = defineProps(['item', 'remove', 'currentProject', 'rename'])
</script>
<template>
<div class="flex items-center justify-between gap-2 *:block">
<p class="handle text-xl cursor-pointer rounded-lg *:stroke-slate-400 *:stroke-1 hover:*:stroke-slate-300" title="Drag"><GripVertical /></p>
<p class="!overflow-hidden whitespace-nowrap text-ellipsis text-xl flex-1 cursor-pointer hover:bg-slate-600 rounded-lg p-1" :title="props.item.name" @click="$emit('changeCurrentProject', props.item)">{{ props.item.name }}</p>
<!-- <button class="cursor-pointer hover:bg-slate-600 rounded-lg p-1" type="button" @click="props.remove(item)"><X /></button> -->
<dropdown-menu class="relative rounded-full hover:bg-slate-500" title="More">
<template #trigger>
<button class="p-2"><MoreVertical /></button>
</template>
<template #body>
<ul class="*:flex absolute right-0 bg-slate-600 p-2 rounded-lg flex flex-col gap-1 *:cursor-pointer *:p-1 *:rounded border border-slate-500 z-10">
<li class="hover:bg-slate-500" @click="props.rename(item)"><Edit3 /> Rename</li>
<li class="hover:bg-slate-500" @click="props.remove(item)"><X /> Delete</li>
</ul>
</template>
</dropdown-menu>
</div>
</template>
<style scoped>
</style>

40
src/instances/Task.vue Normal file
View File

@ -0,0 +1,40 @@
<script setup>
import { X, GripVertical, MoreVertical, Edit3, CheckCircle2, Star } from 'lucide-vue-next';
import DropdownMenu from 'v-dropdown-menu';
// import 'v-dropdown-menu/dist/vue3/v-dropdown-menu.css'
const props = defineProps(['item', 'remove', 'currentProject', 'rename'])
</script>
<template>
<div class="flex items-center justify-between gap-2 *:block" :class="item.done ? 'line-through text-slate-300' : ''">
<p v-if="!item.done" class="handle text-xl cursor-pointer rounded-lg *:stroke-slate-400 *:stroke-1 hover:*:stroke-slate-300" title="Drag"><GripVertical /></p>
<p class="text-xl cursor-pointer rounded-lg *:stroke-slate-400 *:stroke-1 hover:*:stroke-slate-300"
:class="item.done ? '*:fill-blue-300 *:stroke-slate-800 hover:*:stroke-slate-700 hover:*:fill-blue-400 ' : ''" @click="$emit('toggleDone', props.item)" title="Toggle status"><CheckCircle2 /></p>
<p class="!overflow-hidden whitespace-nowrap text-ellipsis text-xl flex-1 cursor-pointer hover:bg-slate-600 rounded-lg p-1" :title="`${props.item.name} - show details`" @click="$emit('showDetails', props.item)">{{ props.item.name }}</p>
<p class="text-xl cursor-pointer rounded-lg *:stroke-slate-400 *:stroke-1 hover:*:stroke-slate-300"
:class="item.priority ? '*:fill-yellow-300 *:stroke-0 hover:*:fill-yellow-400 ' : ''" @click="$emit('togglePriority', props.item)" title="Toggle priority"><Star /></p>
<!-- <button class="cursor-pointer hover:bg-slate-600 rounded-lg p-1" type="button" @click="props.remove(item)"><X /></button> -->
<dropdown-menu class="relative rounded-full hover:bg-slate-500" title="More">
<template #trigger>
<button class="p-2"><MoreVertical /></button>
</template>
<template #body>
<ul class="*:flex absolute right-0 bg-slate-600 p-2 rounded-lg flex flex-col gap-1 *:cursor-pointer *:p-1 *:rounded border border-slate-500">
<li class="hover:bg-slate-500" @click="props.rename(item)"><Edit3 /> Rename</li>
<li class="hover:bg-slate-500" @click="props.remove(item)"><X /> Delete</li>
</ul>
</template>
</dropdown-menu>
</div>
</template>
<style scoped>
</style>

10
src/localStorage.js Normal file
View File

@ -0,0 +1,10 @@
import { Project } from "./project";
function publishToLocalStorage() {
localStorage.setItem(
"tasks-projects-todo",
JSON.stringify(Project.projects)
);
}
export { publishToLocalStorage };

29
src/main.js Normal file
View File

@ -0,0 +1,29 @@
import './index.css'
import { createApp } from 'vue'
import App from './App.vue'
import { Project as ProjectClass } from './project.js';
import DropdownMenu from 'v-dropdown-menu'
console.log(JSON.parse(localStorage.getItem("tasks-projects-todo")));
JSON.parse(localStorage.getItem("tasks-projects-todo")).forEach(element => {
let proj = ProjectClass.newProject(element.name);
element.tasks.forEach(task => {
// console.log(task)
proj.newTask(
task.name,
task.priority,
task.dueDate,
task.description,
task.done,
)
})
});
const app = createApp(App)
app.use(DropdownMenu)
// import router from './router'
// app.use(router)
app.mount('#app')
document.documentElement.classList.toggle("dd-nav-dark");

31
src/project.js Normal file
View File

@ -0,0 +1,31 @@
import { toRaw } from "vue";
import { Task } from "./tasks";
class Project {
constructor(name, tasks) {
this.name = name;
this.tasks = tasks || [];
}
static projects = [];
static newProject(name, tasks) {
this.projects.push(new this(name, tasks || []));
return this.projects[this.projects.length - 1];
}
newTask(name, priority, dueDate, description, done) {
if (priority == undefined) {
this.tasks.push(new Task(name));
} else {
this.tasks.push(new Task(name, priority, dueDate, description, done));
}
return this.tasks[this.tasks.length - 1];
}
changeName(name) {
this.name = name;
}
remove() {
let thisProjectInArray = Project.projects.indexOf(toRaw(this));
Project.projects.splice(thisProjectInArray, 1);
}
}
export { Project };

23
src/router/index.js Normal file
View File

@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
}
]
})
export default router

28
src/tasks.js Normal file
View File

@ -0,0 +1,28 @@
import { toRaw } from "vue";
import { currentProject } from "./current.js";
class Task {
constructor(name, priority, dueDate, description, done) {
this.name = name;
this.priority = priority || false;
this.dueDate = dueDate || null;
this.description = description || null;
this.done = done || false;
}
changeName(name) {
this.name = name;
}
toggleDone() {
this.done = !this.done;
}
togglePriority() {
this.priority = !this.priority;
}
remove() {
console.log(this, toRaw(currentProject.value).tasks);
let thisTaskInArray = toRaw(currentProject.value).tasks.indexOf(toRaw(this));
toRaw(currentProject.value).tasks.splice(thisTaskInArray, 1);
}
}
export { Task };

View File

@ -0,0 +1,84 @@
<script setup>
import { ref, onMounted, toRaw, reactive } from 'vue'
import draggable from 'vuedraggable'
import { Plus, X } from 'lucide-vue-next';
import Project from '../instances/Project.vue';
import { Project as ProjectClass } from "../project.js";
import { publishToLocalStorage } from '../localStorage.js';
import { currentProject } from '../current.js';
let list = ref(ProjectClass.projects)
function refreshProjects() {
list.value = [];
list.value.push(...ProjectClass.projects);
publishToLocalStorage();
}
function newProject() {
let projName = prompt("Project name:");
if (projName != null && projName != "") {
ProjectClass.newProject(projName);
}
refreshProjects();
}
function renameProject(item) {
let newName = prompt(`Rename ${item.name} to:`);
if (newName != null && newName != "") {
item.changeName(newName);
}
refreshProjects();
}
function removeProject(item) {
if (confirm(`Are you sure you want to remove project ${item.name}?`)) {
if (currentProject.value == reactive(toRaw(item))) {
currentProject.value = null;
}
item.remove();
}
refreshProjects();
}
function onMoveCallback(){
ProjectClass.projects = toRaw(list.value.map(element => toRaw(element)));
refreshProjects();
}
</script>
<template>
<div class="projects lg:w-[1000px] lg:ml-auto lg:mr-auto m-3 p-3 bg-slate-700 rounded-md border border-slate-600">
<div class="flex justify-center">
<button type="button" @click="newProject()">
<div class="flex items-center gap-1 rounded-xl border p-1 hover:border-slate-500 hover:bg-slate-600 border-slate-600 bg-slate-700"><Plus /> <p>Add project</p></div>
</button>
</div>
<draggable
v-if="list.length > 0"
v-model="list"
@start="drag=true"
@end="drag=false"
item-key="id"
@change="onMoveCallback()"
handle=".handle"
animation="150">
<template #item="{element}">
<Project @change-current-project="(item) => {currentProject == reactive(toRaw(item)) ? currentProject = null : currentProject = toRaw(item); console.log(currentProject)}" :item="element" :remove="removeProject" :rename="renameProject" />
</template>
</draggable>
<div v-else class="text-2xl font-light text-center">No projects. Add one!</div>
</div>
</template>
<style scoped>
</style>

114
src/views/TasksView.vue Normal file
View File

@ -0,0 +1,114 @@
<script setup>
import { ref, onMounted, toRaw, watch } from 'vue'
import draggable from 'vuedraggable'
import { Plus, X, ChevronDown, ChevronUp } from 'lucide-vue-next';
import Task from '../instances/Task.vue';
import { currentProject } from '../current.js';
import { publishToLocalStorage } from '../localStorage.js';
let list = ref([])
function refreshTasks() {
list.value = [];
if (currentProject.value != null) {
list.value.push(...currentProject.value.tasks);
publishToLocalStorage();
}
}
refreshTasks();
function newTask() {
if (currentProject.value != null) {
let taskName = prompt("Task name:");
if (taskName != null && taskName != "") {
toRaw(currentProject.value).newTask(taskName);
}
} else {
alert("No project selected!")
}
refreshTasks();
}
function removeTask(item) {
console.log(toRaw(item))
if (confirm(`Are you sure you want to remove task ${toRaw(item).name}?`)) {
toRaw(item).remove();
}
refreshTasks();
}
function onMoveCallback(){
toRaw(currentProject.value).tasks = toRaw(list.value.map(element => toRaw(element)));
}
watch(currentProject, (test) => {
console.log(test);
refreshTasks();
})
function toggleDone(item) {
item.toggleDone();
refreshTasks();
}
function togglePriority(item) {
item.togglePriority();
refreshTasks();
}
function checkIfAtLeastOneDone() {
return currentProject.value.tasks.some(element => element.done);
}
</script>
<template>
<div class="tasks lg:w-[1000px] lg:ml-auto lg:mr-auto m-3 p-3 bg-slate-700 rounded-md border border-slate-600">
<div class="flex justify-center">
<button type="button" @click="newTask()">
<div class="flex items-center gap-1 rounded-xl border p-1 hover:border-slate-500 hover:bg-slate-600 border-slate-600 bg-slate-700"><Plus /> <p>Add task</p></div>
</button>
</div>
<div
v-if="list.length > 0">
<draggable
v-model="list"
@start="drag=true"
@end="drag=false"
item-key="id"
@change="onMoveCallback()"
handle=".handle"
animation="150">
<template #item="{element}">
<Task :item="element" v-if="!element.done" :remove="removeTask" @toggle-done="toggleDone" @toggle-priority="togglePriority" />
</template>
</draggable>
<dropdown-menu class="relative rounded-full" title="More" :closeOnClickOutside="false">
<template #trigger>
<h1 v-if="checkIfAtLeastOneDone()" class="flex cursor-pointer text-slate-300"><ChevronDown /> Completed</h1>
</template>
<template #body>
<div v-for="element in list">
<Task :item="element" v-if="element.done" :remove="removeTask" @toggle-done="toggleDone" @toggle-priority="togglePriority" />
</div>
</template>
</dropdown-menu>
</div>
<div v-else-if="currentProject == null" class="text-2xl font-light text-center">Choose a project</div>
<div v-else class="text-2xl font-light text-center">No tasks. Add one!</div>
</div>
</template>
<style scoped>
</style>

11
tailwind.config.js Normal file
View File

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./src/**/*.{html,js,vue}',
],
theme: {
extend: {},
},
plugins: [],
}

16
vite.config.js Normal file
View File

@ -0,0 +1,16 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})