Compare commits

...

12 commits

Author SHA1 Message Date
c23e5ac85a aggiunto segnapreso senza auth 2022-08-29 17:27:00 +02:00
4c05e40a73 add images api 2022-08-26 16:31:33 +02:00
f49fdd247a fix serving images 2022-08-25 18:57:12 +02:00
fc1d145698 remove empty tags 2022-08-25 17:03:17 +02:00
0377942842 carousel + upload multiplo + form validation 2022-08-24 17:46:36 +02:00
3472b11168 upload multiplo, validation aummaumma 2022-08-23 23:26:57 +02:00
9c2ff71a2c Update TODO.md 2022-08-23 09:22:52 +02:00
a577e078bb update yarn.lock 2022-08-23 00:47:56 +02:00
93dc75d3ba Merge remote-tracking branch 'blat/feat/ws' 2022-08-23 00:42:55 +02:00
ddc0b07f7d minor 2022-08-22 17:18:45 +02:00
87765ba2c0 startup 2022-08-22 17:18:00 +02:00
dbdccd45ba add basic ws 2022-08-22 17:10:11 +02:00
19 changed files with 1348 additions and 846 deletions

22
TODO.md
View file

@ -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
View 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>

View file

@ -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>

View 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>

View file

@ -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
View 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`)
})
})
})
}

View file

@ -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' }
],
})

View file

@ -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"
}
}

View file

@ -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'>

View file

@ -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
View 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')

View 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 }
}
})

View 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 }
}
})

View file

@ -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) {

View 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))
})

View file

@ -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)
}

View file

@ -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
View file

@ -0,0 +1,5 @@
import { load } from '../server/controller'
export default () => {
load()
}

1835
yarn.lock

File diff suppressed because it is too large Load diff