Compare commits
12 commits
main
...
feat/segna
Author | SHA1 | Date | |
---|---|---|---|
c23e5ac85a | |||
4c05e40a73 | |||
f49fdd247a | |||
fc1d145698 | |||
0377942842 | |||
3472b11168 | |||
9c2ff71a2c | |||
a577e078bb | |||
93dc75d3ba | |||
ddc0b07f7d | |||
87765ba2c0 | |||
dbdccd45ba |
19 changed files with 1348 additions and 846 deletions
22
TODO.md
22
TODO.md
|
@ -1,7 +1,15 @@
|
|||
[ ] attach db connection / setup on nuxt listen hook
|
||||
[ ] carousel in detail
|
||||
[ ] next / prev in page
|
||||
[ ] favicon
|
||||
[ ] form validation
|
||||
[ ] api rate
|
||||
[ ] mail in lista
|
||||
|
||||
- [x] attach db connection / setup on nuxt listen hook
|
||||
- [x] filtrare i file dell'input type file del form per mostrare solo mimetype image/*
|
||||
- [x] carousel in detail (o un modo per avere piu' immagini per un oggetto)
|
||||
- [x] stringo la pagina dei dettagli
|
||||
- [x] next / prev in page
|
||||
- [x] i tag da trimmare ed eliminare se son vuoti
|
||||
- [x] form validation
|
||||
- [x] servire immagini anche x il deploy
|
||||
- [ ] un loading per next / prev
|
||||
- [ ] immagine del banner da scaricare in locale
|
||||
- [ ] favicon (solo perche' senza ci mette un sacco a fare il load)
|
||||
- [x] ci vuole un modo per segnare che un oggetto e' stato preso? forse no se facciamo un'asta live
|
||||
- [ ] deploy
|
||||
- [ ] mail in lista
|
||||
|
|
13
components/carousel.vue
Normal file
13
components/carousel.vue
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script setup>
|
||||
// piccolo carosello da mettere in alto alle pagine delle cosette
|
||||
|
||||
const { images } = defineProps(['images'])
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class='carousel max-h-100 bg-neutral'>
|
||||
<div v-for='image in images' class='carousel-item w-full' :key='image'>
|
||||
<img class='carousel-center object-contain' :src="`/api/image/${image}`">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -8,7 +8,7 @@ const { cosetta } = defineProps({
|
|||
</script>
|
||||
<template>
|
||||
<div class="rounded overflow-hidden shadow-lg flex flex-col justify-between">
|
||||
<img class="object-cover w-full h-48" :src="`/imgs/${cosetta.images[0]}`" alt="Mountain">
|
||||
<img class="object-cover w-full h-48" :src="`/api/image/${cosetta.images[0]}`" alt="Mountain">
|
||||
<div class="px-6 py-4 grow">
|
||||
<nuxt-link :to="`/c/${cosetta.uuid}`">
|
||||
<h2 class="text-pink-500 card-title hover:underline uppercase mb-2 line-clamp-2" v-text='cosetta.name' />
|
||||
|
@ -20,5 +20,6 @@ const { cosetta } = defineProps({
|
|||
class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2"
|
||||
v-text='tag' />
|
||||
</div>
|
||||
<toggleTaken :cosetta='cosetta'/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
25
components/toggleTaken.vue
Normal file
25
components/toggleTaken.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<script setup>
|
||||
const { cosetta } = defineProps({
|
||||
cosetta: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
watch(cosetta, () => { deep: true });
|
||||
|
||||
let toggleTaken = async () => {
|
||||
const ret = await $fetch(`/api/cosetta/${cosetta.uuid}/taken`, { method: 'POST' })
|
||||
const q = await $fetch(`/api/cosetta/${cosetta.uuid}`)
|
||||
cosetta.taken = q.cosetta.taken
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer flex justify-end">
|
||||
<span class="label-text px-4" v-html="cosetta.taken && 'Cosetta disponibile' || 'Cosetta assegnata'">
|
||||
</span>
|
||||
<input type="checkbox" class="toggle toggle-lg" @click='toggleTaken' :checked='cosetta.taken' />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
|
@ -3,37 +3,43 @@
|
|||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg uppercase">Aggiungo una cosetta</h3>
|
||||
<form action="/api/cosette" method="post" enctype="multipart/form-data">
|
||||
<label class="label">
|
||||
<span class="label-text">Che roba è?</span>
|
||||
</label>
|
||||
<input type="text" name='name' placeholder="Nome" class="input input-bordered w-full" />
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Metti il nome, modello, qualsiasi cosa che possa aiutare ad identificarmi</span>
|
||||
</label>
|
||||
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Descrizione</span>
|
||||
</label>
|
||||
<textarea name='description' class='input input-bordered w-full'></textarea>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">In che stato è? Dove l'hai trovato? </span>
|
||||
</label>
|
||||
<!-- NAME -->
|
||||
<label for='name' class="label label-text pb-0">Che roba è?</label>
|
||||
<input type="text" name='name' id='name' placeholder="Nome" required autocomplete='off'
|
||||
class="input secondary peer input-bordered invalid:border-orange-300 w-full" />
|
||||
<label class="label label-text-alt peer-invalid:text-orange-500 transition pt-0" for='name'>Metti il nome
|
||||
della cosetta, il modello, una sigla</label>
|
||||
|
||||
<label class="label">
|
||||
<span class="label-text">Tag</span>
|
||||
</label>
|
||||
<input type="text" name='tags' placeholder="Tags" class="input input-bordered w-full" /><br />
|
||||
<!-- DESCRIPTION -->
|
||||
<label for="description" class="label label-text pb-0">Descrizione</label>
|
||||
<textarea name='description' id='description'
|
||||
class='textarea input-bordered w-full peer invalid:border-orange-300' required></textarea>
|
||||
<label class="label label-text-alt peer-valid:text-grey-300 peer-invalid:text-orange-500 pt-0"
|
||||
for='description'>In che stato è?
|
||||
Dove l'hai
|
||||
trovato? Insomma dacci una corposa descrizione.</label>
|
||||
|
||||
<!-- TAG -->
|
||||
<label class="label label-text pb-0">Tag</label>
|
||||
<input type="text" name='tags' placeholder="Tags" class="input input-bordered w-full" />
|
||||
<label class="label label-text-alt pt-0">Delle parole chiavi per venirne a capo, separate da virgole (no non si
|
||||
autocompleta)</label>
|
||||
|
||||
|
||||
<label class="label">
|
||||
<span class="label-text">Immagini</span>
|
||||
</label>
|
||||
<input class='w-full input input-bordered' type="file" name='imgs' />
|
||||
<label for='images' class="label label-text mt-3 pb-0">Immagini - massimo 10 </label>
|
||||
<input class='text-sm text-grey-500
|
||||
file:mr-5 file:py-2 file:px-6
|
||||
file:rounded-md file:border-0
|
||||
file:text-sm file:font-medium
|
||||
file:bg-blue-50 file:text-blue-700
|
||||
hover:file:cursor-pointer hover:file:bg-amber-50
|
||||
hover:file:text-orange-700' required type="file" name='images' accept="image/*" multiple />
|
||||
|
||||
|
||||
<div class="modal-action">
|
||||
<div class="modal-action divider mt-10">
|
||||
<a href="#" class="btn">Annulla</a>
|
||||
<button class='btn btn-success' type='submit'>Aggiungi</button>
|
||||
<button class='btn btn-primary' type='submit'>Aggiungi</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
42
modules/socket.js
Normal file
42
modules/socket.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { Server } from 'socket.io'
|
||||
|
||||
export default (_, nuxt) => {
|
||||
nuxt.hook('listen', server => {
|
||||
const io = new Server(server)
|
||||
|
||||
nuxt.hook('close', () => io.close())
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('Connection', socket.id)
|
||||
})
|
||||
|
||||
io.on('connect', (socket) => {
|
||||
socket.emit('message', `welcome ${socket.id}`)
|
||||
socket.broadcast.emit('message', `${socket.id} joined`)
|
||||
|
||||
socket.on('joinRoom', (room) => {
|
||||
socket.join(room)
|
||||
socket.emit('message', `joinRoom ${room}`)
|
||||
socket.broadcast.to(room).emit('message', `${socket.id} joined ${room}`)
|
||||
})
|
||||
|
||||
socket.on('newComment',
|
||||
function comment(message, room) {
|
||||
console.log('new comment received: %s', message)
|
||||
socket.broadcast.to(room).emit('newComment', { message })
|
||||
})
|
||||
|
||||
socket.on('message',
|
||||
function message(data) {
|
||||
console.log('message received: %s', data)
|
||||
socket.broadcast.emit('message', { data })
|
||||
})
|
||||
|
||||
socket.on('disconnecting',
|
||||
() => {
|
||||
console.log('disconnected', socket.id)
|
||||
socket.broadcast.emit('message', `${socket.id} left`)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
|
@ -6,10 +6,10 @@ import serveStatic from 'serve-static'
|
|||
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
||||
export default defineNuxtConfig({
|
||||
ssr: true,
|
||||
nitro: {
|
||||
plugins: ['~/server/startup.js'],
|
||||
},
|
||||
buildModules: ['@nuxtjs/tailwindcss', 'unplugin-icons/nuxt', '@nuxtjs/svg'],
|
||||
serverMiddleware: [
|
||||
{ path: '/imgs', handler: serveStatic(__dirname + '/imgs') }
|
||||
],
|
||||
vite: {
|
||||
plugins: [
|
||||
UnpluginComponentsVite({
|
||||
|
@ -20,5 +20,8 @@ export default defineNuxtConfig({
|
|||
}),
|
||||
],
|
||||
},
|
||||
|
||||
modules: ["./modules/socket"],
|
||||
plugins: [
|
||||
{ src:'@/plugins/socket.client.js', mode: 'client' }
|
||||
],
|
||||
})
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
"preview": "nuxt preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.1.94",
|
||||
"@iconify/json": "^2.1.96",
|
||||
"@nuxtjs/svg": "^0.4.0",
|
||||
"@nuxtjs/tailwindcss": "^5.3.2",
|
||||
"daisyui": "^2.24.0",
|
||||
"nuxt": "3.0.0-rc.6",
|
||||
"nuxt": "npm:nuxt3@latest",
|
||||
"unplugin-icons": "^0.14.8",
|
||||
"unplugin-vue-components": "^0.22.4"
|
||||
},
|
||||
|
@ -20,6 +20,8 @@
|
|||
"better-sqlite3": "^7.6.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"serve-static": "^1.15.0",
|
||||
"socket.io": "^4.5.1",
|
||||
"socket.io-client": "^4.5.1",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script setup>
|
||||
const route = useRoute()
|
||||
const { $socket } = useNuxtApp()
|
||||
|
||||
let { cosetta, comments } = reactive(await $fetch(`/api/cosetta/${route.params.cosetta}`))
|
||||
|
||||
|
@ -7,15 +8,26 @@ const comment = reactive({ cosetta_uuid: route.params.cosetta, message: '' })
|
|||
const addComment = async () => {
|
||||
const ret = await $fetch(`/api/comment`, { method: 'POST', body: { ...comment } })
|
||||
comment.message = ''
|
||||
comments.unshift(ret.comment)
|
||||
const db_comment = await $fetch(`/api/comment/${ret.comment.uuid}`)
|
||||
comments.unshift(db_comment)
|
||||
|
||||
// should we put this server-side?
|
||||
$socket.emit("newComment", db_comment, route.params.cosetta)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
$socket.emit("joinRoom", route.params.cosetta)
|
||||
$socket.on("newComment", newComment => comments.unshift(newComment.message))
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<section class="bg-white py-8">
|
||||
<section class="bg-white py-8 mx-auto max-w-4xl">
|
||||
|
||||
<!-- CAROUSEL -->
|
||||
<Carousel :images='cosetta.images' />
|
||||
<div class="container mx-auto pt-4 pb-12">
|
||||
<img v-if='cosetta.images' :src="`/imgs/${cosetta.images[0]}`" />
|
||||
<div class='carousel text-'></div>
|
||||
<h2 class="text-pink-500 text-2xl card-title uppercase mb-2 divider" v-text='cosetta.name' />
|
||||
<div class="px-6 pt-4 pb-2">
|
||||
<span v-for='tag in cosetta.tags' :key='tag'
|
||||
|
@ -27,6 +39,9 @@ const addComment = async () => {
|
|||
</div>
|
||||
|
||||
<div class="container mx-auto">
|
||||
|
||||
<toggleTaken :cosetta='cosetta'/>
|
||||
|
||||
<p class="font-bold text-xl uppercase">chiacchiere</p>
|
||||
<div class='form-control'>
|
||||
<div class='input-group'>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
const page = ref(0)
|
||||
let { data, refresh } = await useFetch(() => `/api/cosette?page=${page.value}`)
|
||||
let { data, pending, refresh } = await useFetch(() => `/api/cosette?page=${page.value}`)
|
||||
|
||||
function next() {
|
||||
page.value++
|
||||
|
|
18
plugins/socket.client.js
Normal file
18
plugins/socket.client.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Nuxt 3: plugins/socket.client.js
|
||||
import io from 'socket.io-client'
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
const socket = io('http://localhost:3000')
|
||||
|
||||
socket.on("message", (arg) => {
|
||||
console.log(arg); // "world"
|
||||
});
|
||||
|
||||
return {
|
||||
provide: {
|
||||
socket: socket
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const socket = io('http://localhost:3000')
|
9
server/api/comment/[uuid].js
Normal file
9
server/api/comment/[uuid].js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { getComment } from '../../controller'
|
||||
|
||||
export default defineEventHandler(event => {
|
||||
try {
|
||||
return getComment(event.context.params.uuid)
|
||||
} catch (e) {
|
||||
return { success: false, reason: e.message }
|
||||
}
|
||||
})
|
10
server/api/cosetta/[uuid]/taken.post.js
Normal file
10
server/api/cosetta/[uuid]/taken.post.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { toggleTaken } from '../../../controller'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
toggleTaken(event.context.params.uuid)
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return { success: false, reason: e.message }
|
||||
}
|
||||
})
|
|
@ -1,13 +1,13 @@
|
|||
import { add } from '../controller'
|
||||
import { useBody, callHandler, sendRedirect } from 'h3'
|
||||
import { readBody, callHandler, sendRedirect } from 'h3'
|
||||
import { uploadService } from '../services/upload-service'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const handler = uploadService().generateHandler()
|
||||
await callHandler(handler, event.req, event.res)
|
||||
const body = await useBody(event)
|
||||
body.imgs = [event.req.file?.filename]
|
||||
const body = await readBody(event)
|
||||
body.images = event.req.files.map(f => f.filename)
|
||||
add(body)
|
||||
return sendRedirect(event, '/')
|
||||
} catch (e) {
|
||||
|
|
12
server/api/image/[uuid].js
Normal file
12
server/api/image/[uuid].js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import fs from 'node:fs'
|
||||
import path from 'path'
|
||||
import { sendStream } from 'h3'
|
||||
|
||||
export default defineEventHandler(event => {
|
||||
const uuid = event.context.params.uuid
|
||||
const filePath = path.resolve('./server/public/images/', uuid)
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error('File not found!')
|
||||
}
|
||||
return sendStream(event, fs.createReadStream(filePath))
|
||||
})
|
|
@ -4,20 +4,21 @@ import { v4 } from 'uuid'
|
|||
const db = new Database('./cosette.db')
|
||||
|
||||
|
||||
function load() {
|
||||
export function load() {
|
||||
db.pragma('journal_mode = WAL')
|
||||
db.exec('CREATE TABLE IF NOT EXISTS cosette (uuid TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, tags TEXT, images TEXT, updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP)')
|
||||
db.exec('CREATE INDEX cosette_updated_at_index ON cosette (updatedAt)')
|
||||
db.exec('CREATE TABLE IF NOT EXISTS cosette (uuid TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, tags TEXT, images TEXT, taken TINYINT DEFAULT 0, updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP)')
|
||||
db.exec('CREATE INDEX IF NOT EXISTS cosette_updated_at_index ON cosette (updatedAt)')
|
||||
db.exec('CREATE TABLE IF NOT EXISTS chan (uuid TEXT PRIMARY KEY, cosetta_uuid REFERENCES cosette(uuid), message TEXT, updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP)')
|
||||
}
|
||||
// TODO: da gestire in qualche modo all'avvio
|
||||
// load()
|
||||
|
||||
export function add(cosetta) {
|
||||
const q = db.prepare('INSERT INTO cosette (uuid, name, description, tags, images) VALUES(:uuid, :name, :description, :tags, :imgs)')
|
||||
const q = db.prepare('INSERT INTO cosette (uuid, name, description, tags, images) VALUES(:uuid, :name, :description, :tags, :images)')
|
||||
cosetta.uuid = v4()
|
||||
cosetta.tags = JSON.stringify(cosetta.tags.split(',').map(t => t.toLowerCase().trim()))
|
||||
cosetta.imgs = JSON.stringify(cosetta.imgs)
|
||||
cosetta.tags = JSON.stringify(
|
||||
cosetta.tags.split(',')
|
||||
.map(t => t.toLowerCase().trim())
|
||||
.filter(t => t)) // tolgo i vuoti
|
||||
cosetta.images = JSON.stringify(cosetta.images)
|
||||
q.run(cosetta)
|
||||
return cosetta
|
||||
}
|
||||
|
@ -63,3 +64,14 @@ export function getComments(cosetta_uuid) {
|
|||
const q = db.prepare('SELECT * from chan WHERE cosetta_uuid = ?')
|
||||
return q.all(cosetta_uuid)
|
||||
}
|
||||
|
||||
export function getComment(uuid) {
|
||||
const q = db.prepare('SELECT * from chan WHERE uuid = ?')
|
||||
const comment = q.get(uuid)
|
||||
return comment
|
||||
}
|
||||
|
||||
export function toggleTaken(uuid) {
|
||||
const q = db.prepare('UPDATE cosette SET taken = ABS(taken - 1) WHERE uuid = ?')
|
||||
return q.run(uuid)
|
||||
}
|
||||
|
|
|
@ -5,42 +5,42 @@ import { v4 } from 'uuid'
|
|||
import path from 'path'
|
||||
|
||||
export const uploadService = () => {
|
||||
let folderPath = './imgs/'
|
||||
if (!fs.existsSync(folderPath)) {
|
||||
fs.mkdirSync(folderPath, { recursive: true })
|
||||
}
|
||||
const { limits: templateLimits }: Options = {
|
||||
limits: {
|
||||
files: 1,
|
||||
fieldNameSize: 400,
|
||||
fileSize: 80 * 1024 * 1024,
|
||||
},
|
||||
};
|
||||
let folderPath = './server/public/images/'
|
||||
if (!fs.existsSync(folderPath)) {
|
||||
fs.mkdirSync(folderPath, { recursive: true })
|
||||
}
|
||||
const { limits: templateLimits }: Options = {
|
||||
limits: {
|
||||
files: 10,
|
||||
fieldNameSize: 400,
|
||||
fileSize: 800 * 1024 * 1024,
|
||||
},
|
||||
};
|
||||
|
||||
const { filename }: multer.DiskStorageOptions = {
|
||||
filename: (_req, file, cb) => {
|
||||
const type = path.extname(file.originalname)
|
||||
cb(null, v4() + type)
|
||||
}
|
||||
}
|
||||
const { filename }: multer.DiskStorageOptions = {
|
||||
filename: (_req, file, cb: Function) => {
|
||||
const type = path.extname(file.originalname)
|
||||
cb(null, v4() + type)
|
||||
}
|
||||
}
|
||||
|
||||
const generateHandler = () => {
|
||||
try {
|
||||
const options: Options = {
|
||||
limits: {
|
||||
...templateLimits,
|
||||
},
|
||||
storage: multer.diskStorage({
|
||||
filename,
|
||||
destination: folderPath
|
||||
}),
|
||||
};
|
||||
const generateHandler = () => {
|
||||
try {
|
||||
const options: Options = {
|
||||
limits: {
|
||||
...templateLimits,
|
||||
},
|
||||
storage: multer.diskStorage({
|
||||
filename,
|
||||
destination: folderPath
|
||||
}),
|
||||
};
|
||||
|
||||
return multer(options).single('imgs');
|
||||
} catch (e) {
|
||||
console.error('Upload error', e)
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
return { generateHandler };
|
||||
};
|
||||
return multer(options).array('images', 10);
|
||||
} catch (e) {
|
||||
console.error('Upload error', e)
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
return { generateHandler };
|
||||
};
|
||||
|
|
5
server/startup.js
Normal file
5
server/startup.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { load } from '../server/controller'
|
||||
|
||||
export default () => {
|
||||
load()
|
||||
}
|
Loading…
Reference in a new issue