Skip to content

Commit 25cf3f7

Browse files
committed
first commit
0 parents  commit 25cf3f7

25 files changed

+18409
-0
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
*.log*
3+
.nuxt
4+
.nitro
5+
.cache
6+
.output
7+
.env
8+
dist
9+
.DS_Store

.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
shamefully-hoist=true
2+
strict-peer-dependencies=false

.vscode/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"cSpell.words": [
3+
"maxlength",
4+
"Nuxt",
5+
"toastification"
6+
]
7+
}

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Nuxt 3 Minimal Starter
2+
3+
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
4+
5+
## Setup
6+
7+
Make sure to install the dependencies:
8+
9+
```bash
10+
# yarn
11+
yarn install
12+
13+
# npm
14+
npm install
15+
16+
# pnpm
17+
pnpm install
18+
```
19+
20+
## Development Server
21+
22+
Start the development server on `http://localhost:3000`
23+
24+
```bash
25+
npm run dev
26+
```
27+
28+
## Production
29+
30+
Build the application for production:
31+
32+
```bash
33+
npm run build
34+
```
35+
36+
Locally preview production build:
37+
38+
```bash
39+
npm run preview
40+
```
41+
42+
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

app.vue

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script lang="ts" setup>
2+
import type { ISetResponse, ITodo } from 'types'
3+
4+
interface IAllItems extends ISetResponse {
5+
data: ITodo[]
6+
}
7+
const { data: items, refresh, error } = await useFetch<IAllItems>('/api/v1/item/all-items')
8+
9+
if (error.value) {
10+
throw createError(error.value)
11+
}
12+
13+
</script>
14+
<template>
15+
<div class="p-20 flex justify-center">
16+
<div class="max-w-xl w-full bg-white p-4 rounded shadow-md">
17+
<h1 class="text-slate-800 text-xl font-bold text-center">Simple MongoDB Crud Example. Nuxt 3</h1>
18+
<div class="mt-6">
19+
<TheForm @add-item="refresh" />
20+
<div class="text-slate-400 text-xs mt-2">Total: {{ items?.data.length }}</div>
21+
<ul
22+
v-if="items!.data.length > 0"
23+
class="mt-4 divide-y"
24+
>
25+
<TheItem
26+
v-for="item in items!.data"
27+
:item="item"
28+
@delete="refresh"
29+
@update="refresh"
30+
/>
31+
</ul>
32+
<div
33+
v-else
34+
class="text-slate-400 text-center mt-4"
35+
>
36+
Oops! Looks like there are no items yet. Add some.
37+
</div>
38+
</div>
39+
<NuxtErrorBoundary>
40+
<!-- ... -->
41+
<template #error>
42+
<p>An error occurred: {{ error }}</p>
43+
</template>
44+
</NuxtErrorBoundary>
45+
</div>
46+
</div>
47+
</template>
48+
<style scoped lang="css">
49+
:global(body) {
50+
@apply bg-slate-100;
51+
}
52+
</style>

assets/css/main.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
.Vue-Toastification__toast {
6+
padding: 12px 12px !important;
7+
border-radius: 5px !important;
8+
min-height: 30px !important;
9+
min-width: 240px !important;
10+
}
11+
12+
.Vue-Toastification__toast-body {
13+
font-size: 14px !important;
14+
}

components/DeleteIcon.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<template>
2+
<svg
3+
xmlns="http://www.w3.org/2000/svg"
4+
viewBox="0 0 24 24"
5+
fill="currentColor"
6+
>
7+
<path
8+
fill-rule="evenodd"
9+
d="M16.5 4.478v.227a48.816 48.816 0 013.878.512.75.75 0 11-.256 1.478l-.209-.035-1.005 13.07a3 3 0 01-2.991 2.77H8.084a3 3 0 01-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 01-.256-1.478A48.567 48.567 0 017.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 013.369 0c1.603.051 2.815 1.387 2.815 2.951zm-6.136-1.452a51.196 51.196 0 013.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 00-6 0v-.113c0-.794.609-1.428 1.364-1.452zm-.355 5.945a.75.75 0 10-1.5.058l.347 9a.75.75 0 101.499-.058l-.346-9zm5.48.058a.75.75 0 10-1.498-.058l-.347 9a.75.75 0 001.5.058l.345-9z"
10+
clip-rule="evenodd"
11+
/>
12+
</svg>
13+
</template>

components/EditIcon.vue

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<template>
2+
<svg
3+
xmlns="http://www.w3.org/2000/svg"
4+
viewBox="0 0 24 24"
5+
fill="currentColor"
6+
>
7+
<path
8+
d="M21.731 2.269a2.625 2.625 0 00-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 000-3.712zM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 00-1.32 2.214l-.8 2.685a.75.75 0 00.933.933l2.685-.8a5.25 5.25 0 002.214-1.32l8.4-8.4z"
9+
/>
10+
<path
11+
d="M5.25 5.25a3 3 0 00-3 3v10.5a3 3 0 003 3h10.5a3 3 0 003-3V13.5a.75.75 0 00-1.5 0v5.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5V8.25a1.5 1.5 0 011.5-1.5h5.25a.75.75 0 000-1.5H5.25z"
12+
/>
13+
</svg>
14+
</template>

components/TheForm.vue

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script lang="ts" setup>
2+
import { useToast } from 'vue-toastification'
3+
4+
const emit = defineEmits<{
5+
(e: 'addItem'): void
6+
}>()
7+
8+
const itemValue = ref('')
9+
const handleAddItem = async () => {
10+
if (itemValue.value === '') {
11+
return useToast().error('Field is required.')
12+
}
13+
await useFetch('/api/v1/item/add', {
14+
method: 'POST',
15+
body: { item: itemValue.value },
16+
onResponse(context) {
17+
const { statusCode, statusMessage } = context.response._data
18+
if (statusCode === 200) {
19+
useToast().success('New item has been added.')
20+
itemValue.value = ''
21+
} else {
22+
useToast().error(statusMessage)
23+
throw createError({ statusCode, statusMessage, fatal: true })
24+
}
25+
}
26+
})
27+
emit('addItem')
28+
}
29+
</script>
30+
<template>
31+
<div>
32+
<form
33+
class="flex gap-2"
34+
@submit.prevent="handleAddItem"
35+
>
36+
<input
37+
type="text"
38+
v-model="itemValue"
39+
placeholder="Type something...."
40+
class="outline outline-1 outline-slate-300 rounded min-h-[40px] w-full text-sm pl-2 focus:outline-slate-500"
41+
/>
42+
<button
43+
type="submit"
44+
class="min-w-fit bg-blue-700 text-white px-6 py-2 rounded text-sm font-bold uppercase"
45+
>Add Item</button>
46+
</form>
47+
</div>
48+
</template>
49+
<style scoped lang="css"></style>

components/TheItem.vue

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<script lang="ts" setup>
2+
import { Types } from 'mongoose'
3+
import { useToast } from 'vue-toastification'
4+
import { ITodo } from '~/types'
5+
6+
const props = defineProps<{
7+
item: ITodo
8+
}>()
9+
const emit = defineEmits<{
10+
(e: 'delete'): void,
11+
(e: 'update'): void
12+
}>()
13+
14+
const itemRef = ref<HTMLElement | null>(null)
15+
16+
const handleDelete = async (id: Types.ObjectId): Promise<void> => {
17+
const { error } = await useFetch(`/api/v1/item/${id}`, {
18+
method: 'DELETE',
19+
onResponse(context) {
20+
const { statusCode, statusMessage } = context.response._data
21+
if (statusCode === 200) {
22+
useToast().success(statusMessage)
23+
}
24+
}
25+
})
26+
emit('delete')
27+
if (error.value) {
28+
alert(error.value.statusMessage)
29+
}
30+
}
31+
32+
const removeEditActiveClasses = (): void => {
33+
const lists = Array.from(document.querySelectorAll('.list'))
34+
lists.forEach((el) => el.classList.remove('edit-active'))
35+
}
36+
37+
const handleEdit = (): void => {
38+
removeEditActiveClasses()
39+
itemRef.value?.classList.add('edit-active')
40+
}
41+
42+
const handleCancel = (): void => {
43+
removeEditActiveClasses()
44+
const formInput = itemRef.value?.querySelector('input')
45+
formInput!.value = props.item.item
46+
}
47+
48+
const handleSaveEdit = async (id: Types.ObjectId): Promise<void> => {
49+
const formInput = itemRef.value?.querySelector('input')
50+
await useFetch(`/api/v1/item/${id}`, {
51+
method: 'PUT',
52+
body: { item: formInput!.value }
53+
})
54+
emit('update')
55+
removeEditActiveClasses()
56+
}
57+
58+
</script>
59+
<template>
60+
<li
61+
ref="itemRef"
62+
:id="item._id.toString()"
63+
class="list p-2 hover:bg-slate-100 flex gap-2"
64+
>
65+
<div class="w-full text-sm text-slate-600">
66+
<div class="item">
67+
{{ item.item }}
68+
</div>
69+
<div>
70+
<form class="form-edit flex gap-2">
71+
<input
72+
type="text"
73+
:value="item.item"
74+
class="outline outline-slate-200 outline-1 bg-white text-sm pl-2 py-1 rounded w-full"
75+
/>
76+
<!-- Save -->
77+
<button
78+
class="bg-blue-700 text-white rounded text-xs px-2 uppercase"
79+
@click.prevent="handleSaveEdit(item._id)"
80+
>Save</button>
81+
<!-- Cancel -->
82+
<button
83+
class="bg-slate-300 text-slate-500 rounded text-xs px-2 uppercase"
84+
@click.prevent="handleCancel"
85+
>Cancel</button>
86+
</form>
87+
</div>
88+
</div>
89+
<div class="w-min-fit flex gap-4 actions">
90+
<div>
91+
<EditIcon
92+
class="w-4 h-4 cursor-pointer text-slate-500"
93+
@click="handleEdit"
94+
/>
95+
</div>
96+
<div>
97+
<DeleteIcon
98+
class="w-4 h-4 text-rose-400 cursor-pointer"
99+
@click="handleDelete(item._id)"
100+
/>
101+
</div>
102+
</div>
103+
</li>
104+
</template>
105+
<style scoped lang="css">
106+
.form-edit {
107+
display: none;
108+
}
109+
110+
.edit-active .form-edit {
111+
display: flex;
112+
}
113+
114+
.edit-active .item {
115+
display: none;
116+
}
117+
118+
.edit-active .actions {
119+
display: none;
120+
}
121+
</style>

0 commit comments

Comments
 (0)