6 Commits
11 changed files with 3444 additions and 310 deletions
+103 -53
View File
@@ -1,75 +1,125 @@
# Nuxt Minimal Starter <div align="center">
<img src="https://git.reloia.uk/SnitchItaly/frontend/raw/branch/master/public/icon-512.webp" width="128" height="128" alt="Logo Visione">
<h1>Visione</h1>
<p><b>Un'occhio per la sicurezza del cittadino.</b></p>
</div>
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. **Visione** è una piattaforma progettata per gestire le segnalazioni dei cittadini, implementando un flusso operativo
sicuro, trasparente e istituzionalmente conforme.
## Setup ## Punti di Forza e Funzionalità Principali
Make sure to install dependencies: - **Conformità Istituzionale e UX/UI Standardizzata:** Il frontend è sviluppato utilizzando **Bootstrap Italia** (creato
da Designers Italia). Questa scelta assicura un'identità visiva coerente con i servizi pubblici italiani, garantendo
uniformità, riconoscibilità e accessibilità.
- **Trasparenza e Open Data:** Il sistema espone un endpoint pubblico Open Data con dataset anonimizzati relativi alle
segnalazioni chiuse o risolte, favorendo la trasparenza amministrativa.
- **Automazione e Smistamento Intelligente:** Il calcolo automatico della "Zona" di competenza tramite le coordinate
geografiche elimina i colli di bottiglia durante il triage iniziale, instradando immediatamente la segnalazione
all'operatore corretto.
- **Sicurezza e Onboarding Smart:** L'autenticazione è gestita tramite **OTP (One-Time Password)** e integrazione con *
*CIE (Carta d'Identità Elettronica)**, riducendo l'uso di password statiche e garantendo identità verificate.
- **Infrastruttura DevOps e CI/CD:** Sfruttiamo un'istanza self-hosted di **Gitea** per il versionamento del codice e un
flusso di deployment automatizzato basato su webhook con **Dokploy**, assicurando un rilascio continuo e stabile.
- **Ottimizzazione SEO e Reperibilità:** Configurazione avanzata per sitemap e `robots.txt` (via `@nuxtjs/robots` e
`@nuxtjs/sitemap`) per esplicitare chiaramente le pagine pubbliche e separarle dalle aree private.
## Tecnologie Utilizzate
Il progetto è costruito su uno stack moderno orientato a performance, accessibilità e manutenibilità:
- **Framework Core:** [Nuxt 4](https://nuxt.com/) (Vue 3) con rendering SSR attivato.
- **UI / Styling:** [Bootstrap Italia](https://italia.github.io/bootstrap-italia/) per componenti accessibili e conformi
agli standard AGID.
- **Mappe e Georeferenziazione:** [Leaflet](https://leafletjs.com/) interfacciato con Nominatim/OpenStreetMap per il
suggerimento e la validazione degli indirizzi.
- **PWA e Ottimizzazione:** Modulo `@vite-pwa/nuxt` per Progressive Web App (manifest auto-generato) e moduli ufficiali
Nuxt per icone, font (Google Fonts: Lora, Roboto Mono, Titillium Web) ed elaborazione immagini.
- **Infrastruttura:** Docker, Docker Compose, NodeJS 22.
- **Package Manager:** **Yarn**.
## Limiti e Punti di Attenzione
L'attuale architettura presenta alcune criticità e aree di miglioramento individuate durante i test:
1. **Rischio di Abuso e Moderazione:** L'apertura della piattaforma all'invio di messaggi e allegati potrebbe generare
un elevato carico di moderazione. In futuro, andranno valutati filtri automatici o code dedicate.
2. **Dipendenza da API Cartografiche di Terze Parti:** L'utilizzo di Nominatim/OpenStreetMap per il geocoding esonera
dal lock-in di provider commerciali (es. Google Maps), ma introduce dipendenze su disponibilità e policy d'uso di
servizi esterni.
3. **Barriere all'Ingresso:** I flussi OTP e CIE, seppur estremamente sicuri, possono rappresentare un "attrito" (
digital divide) per le fasce di utenza più anziane o meno avvezze agli strumenti digitali.
---
## Come utilizzare il progetto (Sviluppo)
### Requisiti
- Node.js (v22 o superiore consigliato)
- **Yarn** (utilizza esclusivamente `yarn`, non `npm`)
### 1. Installazione Dipendenze
```bash ```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install yarn install
# bun
bun install
``` ```
## Development Server ### 2. Configurazione Variabili d'Ambiente (.env)
Start the development server on `http://localhost:3000`: Copia il file di esempio per creare la tua configurazione locale:
```bash ```bash
# npm cp .env.example .env
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
``` ```
## Production Il file `.env` richiede le seguenti chiavi:
Build the application for production: - `NUXT_PUBLIC_URL`: URL pubblico dell'applicativo frontend (es. `http://localhost:3000`).
- `NUXT_PUBLIC_API`: URL base per il backend API (es. `http://localhost:5555`).
*(Nota: il proxy integrato in `nuxt.config.ts` inoltrerà già le richieste di `/api/v1` al target di staging
configurato `https://api.visionebari.it` in ambiente di sviluppo).*
### 3. Avvio del Server di Sviluppo
```bash ```bash
# npm yarn run dev
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
``` ```
Locally preview production build: Il server sarà accessibile all'indirizzo [http://localhost:3000](http://localhost:3000).
---
## Distribuzione e Produzione (Docker)
Il progetto è ottimizzato per essere deployato tramite container Docker, sfruttando processi multi-stage per generare
un'immagine finale snella e performante basata su `node:22-bookworm`.
### Build locale e avvio tramite Docker Compose
Il file `docker-compose.yml` espone il servizio `visione-frontend`.
1. Assicurati che il file `.env` sia valorizzato correttamente.
2. Esegui il build e avvia in background:
```bash ```bash
# npm docker compose up -d --build
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
``` ```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. ### Dettagli sul Dockerfile
- **Fase di Build (`AS build`):** Copia `package.json` e `yarn.lock`, esegue `yarn install`, copia i sorgenti ed esegue
`yarn run build`. Iniettando gli argomenti di environment per le API.
- **Fase di Run (`AS run`):** Esegue `yarn install --production` per alleggerire le dipendenze finali. Trasferisce
l'artefatto compilato `.output` e avvia il webserver sulla porta `80` tramite `node /app/server/index.mjs`.
## Testing e Azioni Migliorative Future
I test attuali coprono con successo i flussi primari (registrazione, autenticazione, gestione segnalazioni,
moderazione).
Prossimi step suggeriti:
- Consolidamento con test end-to-end (E2E) automatici per le rotte critiche (in particolare CIE/OTP).
- Verifiche di regressione per gli endpoint Open Data per garantirne l'integrità.
- Monitoraggio della qualità del dato geografico (segnalazioni in aree limite).
+1
View File
@@ -9,6 +9,7 @@ useSeoMeta({
}); });
</script> </script>
<template> <template>
<NuxtPwaManifest/>
<NuxtLayout> <NuxtLayout>
<NuxtPage /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
+2 -2
View File
@@ -70,8 +70,8 @@ export const useAuth = () => {
const res = await fetchCurrentUser() const res = await fetchCurrentUser()
if (res.ok) { if (res.ok) {
user.value = res.user user.value = res.user
await refreshUnreadNotifications() await nuxtApp.runWithContext(() => refreshUnreadNotifications())
const path = currentPath ?? useNuxtApp().$router.currentRoute.value.path const path = currentPath ?? nuxtApp.$router.currentRoute.value.path
if (redirectOnSuccess && path.startsWith('/auth')) { if (redirectOnSuccess && path.startsWith('/auth')) {
await nuxtApp.runWithContext(() => navigateTo('/dashboard')) await nuxtApp.runWithContext(() => navigateTo('/dashboard'))
+15 -5
View File
@@ -1,11 +1,17 @@
import type { import type {
CategoryListResponse, CategoryListResponse,
CreateReportRequest, CreateReportRequest,
CreateReportResponse, FeedbackRequest, FlagRequest, FlagResponse, CreateReportResponse,
FeedbackRequest,
FlagRequest,
FlagResponse,
MediaUploadResponse, MediaUploadResponse,
MessageListResponse, MessageRequest, OkResponse, MessageListResponse,
MessageRequest,
OkResponse,
ReportDetailResponse, ReportDetailResponse,
ReportListResponse ReportListResponse,
TeamListResponse
} from "~/types/api"; } from "~/types/api";
export const useReports = () => { export const useReports = () => {
@@ -20,7 +26,7 @@ export const useReports = () => {
const getReportDetail = (id: number) => const getReportDetail = (id: number) =>
useApiFetch<ReportDetailResponse>(`/api/v1/reports/${id}`, { method: 'GET' }) useApiFetch<ReportDetailResponse>(`/api/v1/reports/${id}`, { method: 'GET' })
const uploadMedia = (id: number, formData: FormData) => const uploadMedia = (id: number, formData: FormData) =>
useApiFetch<MediaUploadResponse>(`/api/v1/reports/${id}/media`, { method: 'POST', body: formData }) useApiFetch<MediaUploadResponse>(`/api/v1/reports/${id}/media`, { method: 'POST', body: formData })
@@ -39,6 +45,9 @@ export const useReports = () => {
const flagMessage = (reportId: number, messageId: number, body: FlagRequest) => const flagMessage = (reportId: number, messageId: number, body: FlagRequest) =>
useApiFetch<FlagResponse>(`/api/v1/reports/${reportId}/messages/${messageId}/flags`, { method: 'POST', body }) useApiFetch<FlagResponse>(`/api/v1/reports/${reportId}/messages/${messageId}/flags`, { method: 'POST', body })
const getTeamSuggestions = (id: number) =>
useApiFetch<TeamListResponse>(`/api/v1/reports/${id}/team-suggestions`, { method: 'GET' })
return { return {
getReports, getReports,
getCategories, getCategories,
@@ -50,5 +59,6 @@ export const useReports = () => {
addFeedback, addFeedback,
flagReport, flagReport,
flagMessage, flagMessage,
getTeamSuggestions,
} }
} }
+4 -5
View File
@@ -36,7 +36,7 @@ const getStatusBadge = (status: string) => {
<div class="it-hero-text-wrapper p-4"> <div class="it-hero-text-wrapper p-4">
<h1 class="no_toc font-weight-bold">Miglioriamo insieme la città</h1> <h1 class="no_toc font-weight-bold">Miglioriamo insieme la città</h1>
<p class="d-none d-lg-block mt-3 mb-4 text-muted"> <p class="d-none d-lg-block mt-3 mb-4 text-muted">
Il portale ufficiale nazionale per segnalare disservizi urbani. Scatta una foto, invia la segnalazione Il portale ufficiale della città metropolitana di Bari per segnalare disservizi urbani. Scatta una foto, invia la segnalazione
e monitora l'intervento in totale trasparenza. e monitora l'intervento in totale trasparenza.
</p> </p>
<div class="it-btn-container d-flex justify-content-center gap-3"> <div class="it-btn-container d-flex justify-content-center gap-3">
@@ -61,22 +61,21 @@ const getStatusBadge = (status: string) => {
<BIIcon color="primary" name="it-camera" size="xl"/> <BIIcon color="primary" name="it-camera" size="xl"/>
</div> </div>
<h3 class="h5 font-weight-semibold">1. Segnala</h3> <h3 class="h5 font-weight-semibold">1. Segnala</h3>
<p class="text-muted">Usa lo smartphone per fotografare il problema stradale e inviare la posizione <p class="text-muted">Fotografa il problema stradale e condividi la tua posizione.</p>
esatta.</p>
</div> </div>
<div class="col-12 col-md-4 mb-4"> <div class="col-12 col-md-4 mb-4">
<div class="mb-3"> <div class="mb-3">
<BIIcon color="primary" name="it-tool" size="xl"/> <BIIcon color="primary" name="it-tool" size="xl"/>
</div> </div>
<h3 class="h5 font-weight-semibold">2. Intervento</h3> <h3 class="h5 font-weight-semibold">2. Intervento</h3>
<p class="text-muted">Le squadre comunali prendono in carico la tua segnalazione e avviano i lavori.</p> <p class="text-muted">Il Comune attiva subito le squadre operative per risolvere il problema.</p>
</div> </div>
<div class="col-12 col-md-4 mb-4"> <div class="col-12 col-md-4 mb-4">
<div class="mb-3"> <div class="mb-3">
<BIIcon color="primary" name="it-star-outline" size="xl"/> <BIIcon color="primary" name="it-star-outline" size="xl"/>
</div> </div>
<h3 class="h5 font-weight-semibold">3. Punti e Feedback</h3> <h3 class="h5 font-weight-semibold">3. Punti e Feedback</h3>
<p class="text-muted">Problema risolto! Ottieni Punti Cittadino e lascia un feedback sull'intervento.</p> <p class="text-muted">Problema risolto! Guadagna Punti Cittadino e valuta l'intervento.</p>
</div> </div>
</div> </div>
</div> </div>
+79 -29
View File
@@ -16,7 +16,7 @@ if (!reportId) {
navigateTo('/operator') navigateTo('/operator')
} }
const { getReportDetail, getMessages, addMessage, flagMessage, flagReport } = useReports() const { getReportDetail, getMessages, addMessage, flagMessage, flagReport, getTeamSuggestions } = useReports()
const { const {
takeReport, takeReport,
getInterventionSummary, getInterventionSummary,
@@ -31,7 +31,9 @@ const { user } = useAuth()
const { data: reportData, refresh: refreshReport } = await useAsyncData(`op-report-${reportId}`, () => getReportDetail(reportId)) const { data: reportData, refresh: refreshReport } = await useAsyncData(`op-report-${reportId}`, () => getReportDetail(reportId))
const { data: messagesData, refresh: refreshMessages } = await useAsyncData(`op-messages-${reportId}`, () => getMessages(reportId)) const { data: messagesData, refresh: refreshMessages } = await useAsyncData(`op-messages-${reportId}`, () => getMessages(reportId))
const { data: summaryData, refresh: refreshSummary } = await useAsyncData(`op-summary-${reportId}`, () => getInterventionSummary(reportId)) const { data: summaryData, refresh: refreshSummary } = await useAsyncData(`op-summary-${reportId}`, () => getInterventionSummary(reportId))
const { data: teamsData } = await useAsyncData('operator-teams', () => getTeams())
const { data: allTeamsData } = await useAsyncData('operator-teams', () => getTeams())
const { data: suggestedTeamsData } = await useAsyncData(`op-team-suggestions-${reportId}`, () => getTeamSuggestions(reportId))
const report = computed<Report | undefined>(() => (reportData.value as any)?.report) const report = computed<Report | undefined>(() => (reportData.value as any)?.report)
const media = computed(() => (reportData.value as any)?.media || []) const media = computed(() => (reportData.value as any)?.media || [])
@@ -39,7 +41,14 @@ const statusHistory = computed(() => (reportData.value as any)?.history || [])
const messages = computed<Message[]>(() => (messagesData.value as any)?.messages || []) const messages = computed<Message[]>(() => (messagesData.value as any)?.messages || [])
const summary = computed(() => (summaryData.value as any)?.summary) const summary = computed(() => (summaryData.value as any)?.summary)
const summaryMedia = computed(() => (summaryData.value as any)?.media || []) const summaryMedia = computed(() => (summaryData.value as any)?.media || [])
const teams = computed(() => teamsData.value?.teams || [])
const allTeams = computed(() => (allTeamsData.value as any)?.teams || [])
const suggestedTeams = computed(() => (suggestedTeamsData.value as any)?.suggestions || [])
const otherTeams = computed(() => {
const suggestedIds = new Set(suggestedTeams.value.map((t: any) => Number(t.id)))
return allTeams.value.filter((t: any) => !suggestedIds.has(Number(t.id)))
})
const currentUserId = computed(() => user.value?.id ?? null) const currentUserId = computed(() => user.value?.id ?? null)
const isTakenByCurrentOperator = computed(() => report.value?.assigned_operator_id === currentUserId.value) const isTakenByCurrentOperator = computed(() => report.value?.assigned_operator_id === currentUserId.value)
@@ -47,7 +56,7 @@ const isOpenAndUnassigned = computed(() => report.value?.status === 'OPEN' && !r
const isReadOnlyPreview = computed(() => !isTakenByCurrentOperator.value) const isReadOnlyPreview = computed(() => !isTakenByCurrentOperator.value)
const canTakeReport = computed(() => isOpenAndUnassigned.value && !isTakenByCurrentOperator.value) const canTakeReport = computed(() => isOpenAndUnassigned.value && !isTakenByCurrentOperator.value)
const isFinalStatus = computed(() => ['REJECTED', 'RESOLVED', 'CLOSED'].includes(report.value?.status || '')) const isFinalStatus = computed(() => ['REJECTED', 'RESOLVED', 'CLOSED'].includes(report.value?.status || ''))
const canAssignTeam = computed(() => !isReadOnlyPreview.value && !isFinalStatus.value && teams.value.length > 0) const canAssignTeam = computed(() => !isReadOnlyPreview.value && !isFinalStatus.value && allTeams.value.length > 0)
const canSendMessage = computed(() => !isReadOnlyPreview.value && !isFinalStatus.value) const canSendMessage = computed(() => !isReadOnlyPreview.value && !isFinalStatus.value)
const canChangeStatus = computed(() => !isReadOnlyPreview.value && !isFinalStatus.value && selectableStatuses.value.length > 0) const canChangeStatus = computed(() => !isReadOnlyPreview.value && !isFinalStatus.value && selectableStatuses.value.length > 0)
@@ -131,6 +140,26 @@ const reportFlagForm = reactive<FlagRequest>({
details: '' details: ''
}) })
// === GESTIONE SCROLL SCHERMO CON MODALI ===
watch(
() => isAssignModalOpen.value || isFlagReportModalOpen.value || isRevokeInterventionModalOpen.value || isFlagMessageModalOpen.value,
(isOpen) => {
if (import.meta.client) {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
}
}
)
onUnmounted(() => {
if (import.meta.client) {
document.body.style.overflow = ''
}
})
watch(() => report.value?.status, (val) => { watch(() => report.value?.status, (val) => {
if (val) { if (val) {
const allowed = statusTransitions[val] || [] const allowed = statusTransitions[val] || []
@@ -550,8 +579,8 @@ const formattedInterventionLinkExpiry = computed(() =>
<strong>{{ summary.assigned_operator_id ? `#${summary.assigned_operator_id}` : 'Nessuno' }}</strong> <strong>{{ summary.assigned_operator_id ? `#${summary.assigned_operator_id}` : 'Nessuno' }}</strong>
</li> </li>
<li> <li>
<span>Team disponibili</span> <span>Team suggeriti</span>
<strong>{{ teams.length }}</strong> <strong :class="{'text-primary': suggestedTeams.length > 0}">{{ suggestedTeams.length }}</strong>
</li> </li>
<li> <li>
<span>Allegati</span> <span>Allegati</span>
@@ -688,48 +717,69 @@ const formattedInterventionLinkExpiry = computed(() =>
</div> </div>
</div> </div>
<div v-if="isAssignModalOpen" class="modal-backdrop fade show d-block" style="background: rgba(0,0,0,0.5); z-index: 1040;"></div> <!-- BACKGROUND MODALE COMUNE -->
<div v-if="isAssignModalOpen" class="modal fade show d-block" tabindex="-1" style="z-index: 1050;"> <div v-if="isAssignModalOpen || isFlagReportModalOpen" class="modal-backdrop fade show d-block"
style="background: rgba(0,0,0,0.5); z-index: 1040;"></div>
<!-- MODALE: GESTIONE INTERVENTO -->
<div v-if="isAssignModalOpen" class="modal fade show d-block" style="z-index: 1050; overflow-y: auto;"
tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Gestione Intervento</h5> <h5 class="modal-title">Gestione Intervento</h5>
<button type="button" class="btn-close" @click="isAssignModalOpen = false"></button> <button type="button" class="btn-close" @click="isAssignModalOpen = false"></button>
</div> </div>
<div class="modal-body"> <!-- FORM AGGIORNATO CON MODAL-BODY E MODAL-FOOTER -->
<form @submit.prevent="handleAssignTeam"> <form @submit.prevent="handleAssignTeam">
<div class="mb-3"> <div class="modal-body">
<div class="mb-0">
<label class="form-label">Team incaricato</label> <label class="form-label">Team incaricato</label>
<select v-model.number="selectedTeamId" class="form-select" required> <select v-model.number="selectedTeamId" class="form-select" required>
<option :value="null">Seleziona team</option> <option :value="null">Seleziona team</option>
<option v-for="team in teams" :key="team.id" :value="team.id">
{{ team.name }}<template v-if="team.category_name"> · {{ team.category_name }}</template> <optgroup v-if="suggestedTeams.length > 0" label="Team Suggeriti">
<template v-if="team.area_name"> · {{ team.area_name }}</template> <option v-for="team in suggestedTeams" :key="'sug-' + team.id" :value="Number(team.id)">
</option> {{ team.name }}
<template v-if="team.is_recommended"> ★</template>
<template v-if="team.category_name"> · {{ team.category_name }}</template>
<template v-if="team.area_name"> · {{ team.area_name }}</template>
</option>
</optgroup>
<optgroup :label="suggestedTeams.length > 0 ? 'Altri Team' : 'Tutti i Team'">
<option v-for="team in otherTeams" :key="'oth-' + team.id" :value="Number(team.id)">
{{ team.name }}
<template v-if="team.category_name"> · {{ team.category_name }}</template>
<template v-if="team.area_name"> · {{ team.area_name }}</template>
</option>
</optgroup>
</select> </select>
<small v-if="teams.length === 0" class="form-text text-danger"> <small v-if="allTeams.length === 0" class="form-text text-danger mt-1">
Nessun team attivo disponibile. Nessun team attivo disponibile.
</small> </small>
</div> </div>
<div class="d-flex justify-content-end gap-2 mt-4"> </div>
<button type="button" class="btn btn-outline-secondary" @click="isAssignModalOpen = false">Annulla</button> <div class="modal-footer">
<button type="submit" class="btn btn-primary" :disabled="isAssigning || !selectedTeamId"> <button class="btn btn-outline-secondary" type="button" @click="isAssignModalOpen = false">Annulla
<span v-if="isAssigning" class="spinner-border spinner-border-sm me-2"></span> </button>
Salva Assegnazione <button :disabled="isAssigning || !selectedTeamId" class="btn btn-primary" type="submit">
</button> <span v-if="isAssigning" class="spinner-border spinner-border-sm me-2"></span>
</div> Salva Assegnazione
</form> </button>
</div> </div>
</form>
</div> </div>
</div> </div>
</div> </div>
<div v-if="isFlagReportModalOpen" class="modal-backdrop fade show d-block" style="background: rgba(0,0,0,0.5); z-index: 1040;"></div> <!-- MODALE: SEGNALA ALLA MODERAZIONE -->
<div v-if="isFlagReportModalOpen" class="modal fade show d-block" tabindex="-1" style="z-index: 1050;"> <div v-if="isFlagReportModalOpen" class="modal fade show d-block" style="z-index: 1050; overflow-y: auto;"
tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Segnala segnalazione alla moderazione</h5> <h5 class="modal-title">Segnala alla moderazione</h5>
<button type="button" class="btn-close" :disabled="isFlaggingReport" @click="isFlagReportModalOpen = false; resetReportFlagForm()"></button> <button type="button" class="btn-close" :disabled="isFlaggingReport" @click="isFlagReportModalOpen = false; resetReportFlagForm()"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -738,7 +788,7 @@ const formattedInterventionLinkExpiry = computed(() =>
<label class="form-label">Motivo</label> <label class="form-label">Motivo</label>
<input v-model="reportFlagForm.reason" type="text" class="form-control" maxlength="200" /> <input v-model="reportFlagForm.reason" type="text" class="form-control" maxlength="200" />
</div> </div>
<div> <div class="mb-0">
<label class="form-label">Dettagli</label> <label class="form-label">Dettagli</label>
<textarea v-model="reportFlagForm.details" class="form-control" rows="4" placeholder="Dettagli aggiuntivi opzionali"></textarea> <textarea v-model="reportFlagForm.details" class="form-control" rows="4" placeholder="Dettagli aggiuntivi opzionali"></textarea>
</div> </div>
+44 -5
View File
@@ -6,11 +6,12 @@ export default defineNuxtConfig({
'@nuxt/image', '@nuxt/image',
'@nuxt/fonts', '@nuxt/fonts',
'@nuxtjs/sitemap', '@nuxtjs/sitemap',
'@nuxtjs/robots' '@nuxtjs/robots',
'@vite-pwa/nuxt'
], ],
// TODO: riattivare prima di pubblicare il sito
ssr: false, ssr: true,
runtimeConfig: { runtimeConfig: {
public: { public: {
url: 'http://localhost:3000', url: 'http://localhost:3000',
@@ -22,7 +23,7 @@ export default defineNuxtConfig({
url: 'https://visione.studentindipendenti.it', url: 'https://visione.studentindipendenti.it',
name: 'Visione - Un\'occhio per la sicurezza del cittadino.', name: 'Visione - Un\'occhio per la sicurezza del cittadino.',
}, },
css: [ css: [
'leaflet/dist/leaflet.css', 'leaflet/dist/leaflet.css',
'bootstrap-italia/dist/css/bootstrap-italia.min.css', 'bootstrap-italia/dist/css/bootstrap-italia.min.css',
@@ -39,9 +40,47 @@ export default defineNuxtConfig({
app: { app: {
head: { head: {
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0' },
{ name: 'theme-color', content: '#003366' },
{ name: 'description', content: 'Visione - Un\'occhio per la sicurezza' },
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
{ name: 'apple-mobile-web-app-title', content: 'Visione' }
],
link: [ link: [
{ rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' } { rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' }
] ]
} }
},
pwa: {
registerType: 'autoUpdate',
manifest: {
name: 'Visione - Un\'occhio per la sicurezza',
short_name: 'Visione',
theme_color: '#003366',
background_color: '#003366',
display: 'standalone',
orientation: 'portrait',
icons: [
{
src: '/icon-192.webp',
sizes: '192x192',
type: 'image/webp',
purpose: 'any'
},
{
src: '/icon-512.webp',
sizes: '512x512',
type: 'image/webp',
purpose: 'any'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,ico,webp}']
}
} }
}) })
+1
View File
@@ -14,6 +14,7 @@
"@nuxt/image": "^2.0.0", "@nuxt/image": "^2.0.0",
"@nuxtjs/robots": "6.0.9", "@nuxtjs/robots": "6.0.9",
"@nuxtjs/sitemap": "8.0.15", "@nuxtjs/sitemap": "8.0.15",
"@vite-pwa/nuxt": "1.1.1",
"bootstrap-italia": "^2.18.1", "bootstrap-italia": "^2.18.1",
"codice-fiscale-js": "^2.3.23", "codice-fiscale-js": "^2.3.23",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

+3195 -211
View File
File diff suppressed because it is too large Load Diff