- aggiunto componenti per Home Template... ma ancora da provare
- sistemato catprods - Sistemato menu
This commit is contained in:
@@ -24,29 +24,35 @@
|
||||
</q-expansion-item>
|
||||
|
||||
<!-- Foglia -->
|
||||
<router-link v-else :to="getroute(item)">
|
||||
<q-item clickable :to="getroute(item)" active-class="my-menu-active">
|
||||
<q-item-section thumbnail>
|
||||
<q-avatar
|
||||
:icon="item.materialIcon"
|
||||
:size="item.iconsize || '2rem'"
|
||||
:font-size="item.iconsize || '2rem'"
|
||||
text-color="primary"
|
||||
square
|
||||
rounded
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span :class="item.extraclass">{{ tools.getLabelByItem(item) }}</span>
|
||||
<span v-if="item.subtitle" class="subtitle">{{ item.subtitle }}</span>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</router-link>
|
||||
<q-item
|
||||
v-else
|
||||
clickable
|
||||
:to="getroute(item)"
|
||||
active-class="my-menu-active"
|
||||
>
|
||||
<q-item-section thumbnail>
|
||||
<q-avatar
|
||||
:icon="item.materialIcon"
|
||||
:size="item.iconsize || '2rem'"
|
||||
:font-size="item.iconsize || '2rem'"
|
||||
text-color="primary"
|
||||
square
|
||||
rounded
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<span :class="item.extraclass">{{ tools.getLabelByItem(item) }}</span>
|
||||
<span
|
||||
v-if="item.subtitle"
|
||||
class="subtitle"
|
||||
>{{ item.subtitle }}</span
|
||||
>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./CMenuItem.ts">
|
||||
</script>
|
||||
<script lang="ts" src="./CMenuItem.ts"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './CMenuItem.scss';
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<q-editor
|
||||
ref="editorRef"
|
||||
content-class="wrap_anywhere"
|
||||
content-class="styled-content"
|
||||
toolbar-text-color="white"
|
||||
toolbar-toggle-color="yellow-8"
|
||||
toolbar-bg="primary"
|
||||
|
||||
0
src/components/HeroSection/HeroSection.scss
Executable file
0
src/components/HeroSection/HeroSection.scss
Executable file
43
src/components/HeroSection/HeroSection.ts
Executable file
43
src/components/HeroSection/HeroSection.ts
Executable file
@@ -0,0 +1,43 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
// Importiamo icone da Quasar - esempio con quelle disponibili tramite Quasar Extras
|
||||
// Assicurati di averle abilitate in quasar.config.js
|
||||
import { IFeatSection } from 'app/src/model';
|
||||
import { tools } from 'app/src/store/Modules/tools';
|
||||
import { useQuasar } from 'quasar';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HeroSection',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isDark: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
features: {
|
||||
type: Array as () => IFeatSection[],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
|
||||
return {
|
||||
tools,
|
||||
};
|
||||
},
|
||||
});
|
||||
10
src/components/HeroSection/HeroSection.vue
Executable file
10
src/components/HeroSection/HeroSection.vue
Executable file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div :class="{'bg-dark': isDark}" class="q-py-xl">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" src="./HeroSection.ts"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './HeroSection.scss';
|
||||
</style>
|
||||
1
src/components/HeroSection/index.ts
Executable file
1
src/components/HeroSection/index.ts
Executable file
@@ -0,0 +1 @@
|
||||
export { default as HeroSection } from './HeroSection.vue'
|
||||
@@ -10,7 +10,7 @@ export default defineComponent({
|
||||
},
|
||||
emits: ['select', 'edit', 'delete', 'open'],
|
||||
setup(props, { emit }) {
|
||||
const showGrip = computed(() => props.variant === 'menu');
|
||||
const showGrip = true
|
||||
|
||||
const displayPath = (path?: string) => {
|
||||
if (!path) return '-';
|
||||
|
||||
@@ -516,7 +516,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function imglogo() {
|
||||
return `../../${tools.getimglogo()}`;
|
||||
return `${tools.getimglogo()}`;
|
||||
}
|
||||
|
||||
function getappname() {
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
<template>
|
||||
<div v-if="globalStore.showHeader">
|
||||
<q-header v-if="site" reveal elevated :class="getClassColorHeader" :style="`color: ` + getColorText + `;`">
|
||||
<q-toolbar color="primary" :glossy="!$q.platform.is.ios && !$q.platform.is.android" :inverted="$q.platform.is.ios"
|
||||
class="toolbar">
|
||||
<q-btn flat dense round @click="clickMenu3Orizz" aria-label="Menu">
|
||||
<q-header
|
||||
v-if="site"
|
||||
reveal
|
||||
elevated
|
||||
:class="getClassColorHeader"
|
||||
:style="`color: ` + getColorText + `;`"
|
||||
>
|
||||
<q-toolbar
|
||||
color="primary"
|
||||
:glossy="!$q.platform.is.ios && !$q.platform.is.android"
|
||||
:inverted="$q.platform.is.ios"
|
||||
class="toolbar"
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
round
|
||||
@click="clickMenu3Orizz"
|
||||
aria-label="Menu"
|
||||
>
|
||||
<q-icon name="menu" />
|
||||
</q-btn>
|
||||
|
||||
@@ -19,15 +35,34 @@
|
||||
<!--I'm only rendered on Electron!-->
|
||||
</div>
|
||||
|
||||
<q-btn size="md" id="newvers" v-if="isNewVersionAvailable() || data.updateExists" color="secondary" rounded
|
||||
icon="refresh" class="btnNewVersShow" @click="RefreshApp()" :label="t('notification.newVersionAvailable')">
|
||||
<q-btn
|
||||
size="md"
|
||||
id="newvers"
|
||||
v-if="isNewVersionAvailable() || data.updateExists"
|
||||
color="secondary"
|
||||
rounded
|
||||
icon="refresh"
|
||||
class="btnNewVersShow"
|
||||
@click="RefreshApp()"
|
||||
:label="t('notification.newVersionAvailable')"
|
||||
>
|
||||
</q-btn>
|
||||
|
||||
<q-toolbar-title class="row items-center">
|
||||
<q-avatar @click="toHome" class="imglink">
|
||||
<img :src="imglogo()" height="27" alt="Immagine Logo" />
|
||||
<q-avatar
|
||||
@click="toHome"
|
||||
class="imglink"
|
||||
>
|
||||
<img
|
||||
:src="imglogo()"
|
||||
height="27"
|
||||
alt="Immagine Logo"
|
||||
/>
|
||||
</q-avatar>
|
||||
<div v-if="$q.screen.gt.xs" class="q-mx-sm titlesite">
|
||||
<div
|
||||
v-if="$q.screen.gt.xs"
|
||||
class="q-mx-sm titlesite"
|
||||
>
|
||||
{{ getappname() }}
|
||||
</div>
|
||||
</q-toolbar-title>
|
||||
@@ -45,28 +80,76 @@
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div v-if="site.confpages && site.confpages?.show_darkopt" class="text-h7">
|
||||
<q-toggle :icon="'fas fa-moon'" v-model="dark"> </q-toggle>
|
||||
<div
|
||||
v-if="site.confpages && site.confpages?.show_darkopt"
|
||||
class="text-h7"
|
||||
>
|
||||
<q-toggle
|
||||
:icon="'fas fa-moon'"
|
||||
v-model="dark"
|
||||
>
|
||||
</q-toggle>
|
||||
</div>
|
||||
<div v-if="
|
||||
tools.isLogged() &&
|
||||
(isAdmin() || tools.isCollaboratore())
|
||||
" class="text-h7">
|
||||
<q-toggle :icon="'fas fa-pencil-alt'" v-model="editOn"> </q-toggle>
|
||||
<div
|
||||
v-if="tools.isLogged() && (isAdmin() || tools.isCollaboratore())"
|
||||
class="text-h7"
|
||||
>
|
||||
<q-toggle
|
||||
:icon="'fas fa-pencil-alt'"
|
||||
v-model="editOn"
|
||||
>
|
||||
</q-toggle>
|
||||
</div>
|
||||
<q-btn v-if="!isonline() && site.confpages && site.confpages?.showConnected" flat dense round
|
||||
aria-label="Connection">
|
||||
<q-icon :name="iconConn" :class="clIconConn"></q-icon>
|
||||
<q-icon v-if="isUserNotAuth" name="device_unknown"></q-icon>
|
||||
<div
|
||||
v-if="tools.isLogged() && (isAdmin() || tools.isCollaboratore())"
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
round
|
||||
icon="settings"
|
||||
:to="{ name: 'admin-dashboard' }"
|
||||
aria-label="Apri pannello amministrazione"
|
||||
/>
|
||||
</div>
|
||||
<q-btn
|
||||
v-if="!isonline() && site.confpages && site.confpages?.showConnected"
|
||||
flat
|
||||
dense
|
||||
round
|
||||
aria-label="Connection"
|
||||
>
|
||||
<q-icon
|
||||
:name="iconConn"
|
||||
:class="clIconConn"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
v-if="isUserNotAuth"
|
||||
name="device_unknown"
|
||||
></q-icon>
|
||||
</q-btn>
|
||||
|
||||
<q-btn-dropdown stretch v-if="isfinishLoading && static_data.lang_available.length > 1" flat :label="langshort"
|
||||
auto-close>
|
||||
<q-btn-dropdown
|
||||
stretch
|
||||
v-if="isfinishLoading && static_data.lang_available.length > 1"
|
||||
flat
|
||||
:label="langshort"
|
||||
auto-close
|
||||
>
|
||||
<q-list bordered>
|
||||
<q-item clickable v-ripple v-for="langrec in static_data.lang_available" :key="langrec.value"
|
||||
@click="lang = langrec.value">
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
v-for="langrec in static_data.lang_available"
|
||||
:key="langrec.value"
|
||||
@click="lang = langrec.value"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<img :src="langrec.image" class="flagimg" alt="flag" />
|
||||
<img
|
||||
:src="langrec.image"
|
||||
class="flagimg"
|
||||
alt="flag"
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ langrec.label }}
|
||||
@@ -92,155 +175,315 @@
|
||||
|
||||
<!-- BUTTON USER BAR -->
|
||||
|
||||
<q-btn class="q-mx-xs" v-if="
|
||||
site.confpages && site.confpages?.enableEcommerce && tools.isLogged()
|
||||
" round dense flat @click="rightCartOpen = !rightCartOpen" icon="fas fa-shopping-cart">
|
||||
<q-badge v-if="getnumItemsCart() > 0" color="red" floating transparent>
|
||||
<q-btn
|
||||
class="q-mx-xs"
|
||||
v-if="site.confpages && site.confpages?.enableEcommerce && tools.isLogged()"
|
||||
round
|
||||
dense
|
||||
flat
|
||||
@click="rightCartOpen = !rightCartOpen"
|
||||
icon="fas fa-shopping-cart"
|
||||
>
|
||||
<q-badge
|
||||
v-if="getnumItemsCart() > 0"
|
||||
color="red"
|
||||
floating
|
||||
transparent
|
||||
>
|
||||
{{ getnumItemsCart() }}
|
||||
</q-badge>
|
||||
</q-btn>
|
||||
|
||||
<q-btn class="q-mx-xs" v-if="
|
||||
site.confpages &&
|
||||
site.confpages?.enableEcommerce &&
|
||||
tools.isLogged() &&
|
||||
getnumOrdersCart() > 0
|
||||
" round dense flat to="/orderinfo" icon="fas fa-list-ol">
|
||||
<q-badge v-if="getnumOrdersCart() > 0" color="blue" floating transparent>
|
||||
<q-btn
|
||||
class="q-mx-xs"
|
||||
v-if="
|
||||
site.confpages &&
|
||||
site.confpages?.enableEcommerce &&
|
||||
tools.isLogged() &&
|
||||
getnumOrdersCart() > 0
|
||||
"
|
||||
round
|
||||
dense
|
||||
flat
|
||||
to="/orderinfo"
|
||||
icon="fas fa-list-ol"
|
||||
>
|
||||
<q-badge
|
||||
v-if="getnumOrdersCart() > 0"
|
||||
color="blue"
|
||||
floating
|
||||
transparent
|
||||
>
|
||||
{{ getnumOrdersCart() }}
|
||||
</q-badge>
|
||||
</q-btn>
|
||||
|
||||
<q-btn class="q-mx-xs" v-if="
|
||||
site.confpages && site.confpages?.showUserMenu && !tools.isLogged()
|
||||
" dense flat round icon="fas fa-user" @click="rightDrawerOpen = !rightDrawerOpen">
|
||||
<q-btn
|
||||
class="q-mx-xs"
|
||||
v-if="site.confpages && site.confpages?.showUserMenu && !tools.isLogged()"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="fas fa-user"
|
||||
@click="rightDrawerOpen = !rightDrawerOpen"
|
||||
>
|
||||
</q-btn>
|
||||
<q-avatar v-else-if="
|
||||
site.confpages &&
|
||||
site.confpages?.showUserMenu &&
|
||||
tools.isLogged() &&
|
||||
getMyImg() &&
|
||||
$q.screen.gt.sm
|
||||
" size="36px" class="center_img cursor-pointer" @click="rightDrawerOpen = !rightDrawerOpen">
|
||||
<q-img ratio="1" fit="cover" :src="getMyImg()" :alt="Username()" img-class="imgprofile_small"
|
||||
stretch="false" />
|
||||
<q-avatar
|
||||
v-else-if="
|
||||
site.confpages &&
|
||||
site.confpages?.showUserMenu &&
|
||||
tools.isLogged() &&
|
||||
getMyImg() &&
|
||||
$q.screen.gt.sm
|
||||
"
|
||||
size="36px"
|
||||
class="center_img cursor-pointer"
|
||||
@click="rightDrawerOpen = !rightDrawerOpen"
|
||||
>
|
||||
<q-img
|
||||
ratio="1"
|
||||
fit="cover"
|
||||
:src="getMyImg()"
|
||||
:alt="Username()"
|
||||
img-class="imgprofile_small"
|
||||
stretch="false"
|
||||
/>
|
||||
</q-avatar>
|
||||
<q-btn v-else-if="$q.screen.gt.xs" class="q-mx-xs iconprofile_small" round dense flat
|
||||
@click="rightDrawerOpen = !rightDrawerOpen" :icon="getMyImgforIcon()" :color="getcolormenu()">
|
||||
<q-btn
|
||||
v-else-if="$q.screen.gt.xs"
|
||||
class="q-mx-xs iconprofile_small"
|
||||
round
|
||||
dense
|
||||
flat
|
||||
@click="rightDrawerOpen = !rightDrawerOpen"
|
||||
:icon="getMyImgforIcon()"
|
||||
:color="getcolormenu()"
|
||||
>
|
||||
</q-btn>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-drawer side="left" bordered :show-if-above="globalStore.leftDrawerOpen" :breakpoint="800"
|
||||
v-model="leftDrawerOpen" :content-class="['bg-grey-1', 'q-pa-sm']" :content-style="{ padding: '0px' }">
|
||||
<q-drawer
|
||||
side="left"
|
||||
bordered
|
||||
:show-if-above="globalStore.leftDrawerOpen"
|
||||
:breakpoint="800"
|
||||
v-model="leftDrawerOpen"
|
||||
:content-class="['bg-grey-1', 'q-pa-sm']"
|
||||
:content-style="{ padding: '0px' }"
|
||||
>
|
||||
<drawer :clBase="clBase"></drawer>
|
||||
</q-drawer>
|
||||
|
||||
<!-- USER BAR -->
|
||||
<q-drawer v-if="site.confpages && site.confpages?.enableEcommerce" v-model="rightCartOpen" class="q-drawer-cart"
|
||||
side="right" elevated>
|
||||
<q-btn class="absolute-top-right" :style="`margin-right: 10px; color:` + getColorText + `;`" dense flat round
|
||||
icon="close" @click="rightCartOpen = !rightCartOpen">
|
||||
<q-drawer
|
||||
v-if="site.confpages && site.confpages?.enableEcommerce"
|
||||
v-model="rightCartOpen"
|
||||
class="q-drawer-cart"
|
||||
side="right"
|
||||
elevated
|
||||
>
|
||||
<q-btn
|
||||
class="absolute-top-right"
|
||||
:style="`margin-right: 10px; color:` + getColorText + `;`"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="close"
|
||||
@click="rightCartOpen = !rightCartOpen"
|
||||
>
|
||||
</q-btn>
|
||||
|
||||
<CSelectUserActive></CSelectUserActive>
|
||||
|
||||
|
||||
<div v-if="tools.isLogged()" class="bg-primary text-white q-pa-sm q-mb-md" style="border-radius: 0px">
|
||||
<q-icon name="fas fa-shopping-cart" class="q-mr-sm" />
|
||||
{{ $t("ecomm.carrello_di", { user: products.userActive.username }) }}
|
||||
<div
|
||||
v-if="tools.isLogged()"
|
||||
class="bg-primary text-white q-pa-sm q-mb-md"
|
||||
style="border-radius: 0px"
|
||||
>
|
||||
<q-icon
|
||||
name="fas fa-shopping-cart"
|
||||
class="q-mr-sm"
|
||||
/>
|
||||
{{ $t('ecomm.carrello_di', { user: products.userActive.username }) }}
|
||||
</div>
|
||||
<CMyCart v-if="isfinishLoading"></CMyCart>
|
||||
</q-drawer>
|
||||
<!-- USER BAR -->
|
||||
<q-drawer v-if="site.confpages && site.confpages?.showUserMenu" v-model="rightDrawerOpen" side="right" elevated>
|
||||
<q-drawer
|
||||
v-if="site.confpages && site.confpages?.showUserMenu"
|
||||
v-model="rightDrawerOpen"
|
||||
side="right"
|
||||
elevated
|
||||
>
|
||||
<div id="profile">
|
||||
<q-img class="absolute-top" src="/images/landing_first_section.png" style="height: 150px" alt="section page">
|
||||
<q-img
|
||||
class="absolute-top"
|
||||
src="/images/landing_first_section.png"
|
||||
style="height: 150px"
|
||||
alt="section page"
|
||||
>
|
||||
</q-img>
|
||||
<div class="absolute-top bg-transparent text-black center_img" style="margin-top: 10px">
|
||||
<div :class="`text-center q-ma-xs boldhigh text-` + getColorText + ` text-h7`
|
||||
">
|
||||
{{ t("header.area_personale") }}
|
||||
<div
|
||||
class="absolute-top bg-transparent text-black center_img"
|
||||
style="margin-top: 10px"
|
||||
>
|
||||
<div :class="`text-center q-ma-xs boldhigh text-` + getColorText + ` text-h7`">
|
||||
{{ t('header.area_personale') }}
|
||||
</div>
|
||||
|
||||
<div v-if="getMyImg()" class="row justify-center q-pa-md">
|
||||
<q-avatar size="80px" class="center_img q-ma-md">
|
||||
<q-img fit="cover" :src="getMyImg()" :alt="Username()" img-class="imgprofile" height="80px" />
|
||||
<div
|
||||
v-if="getMyImg()"
|
||||
class="row justify-center q-pa-md"
|
||||
>
|
||||
<q-avatar
|
||||
size="80px"
|
||||
class="center_img q-ma-md"
|
||||
>
|
||||
<q-img
|
||||
fit="cover"
|
||||
:src="getMyImg()"
|
||||
:alt="Username()"
|
||||
img-class="imgprofile"
|
||||
height="80px"
|
||||
/>
|
||||
</q-avatar>
|
||||
</div>
|
||||
|
||||
<div v-if="tools.isLogged()" class="text-weight-bold text-user">
|
||||
<div
|
||||
v-if="tools.isLogged()"
|
||||
class="text-weight-bold text-user"
|
||||
>
|
||||
{{ Username() }}<span v-if="myName()"> - {{ myName() }}</span>
|
||||
<span v-if="mySurname()"> {{ mySurname() }}</span>
|
||||
</div>
|
||||
<div class="row justify-evenly q-pa-xs-sm">
|
||||
<div v-if="tools.isLogged() && isAdmin()" class="text-weight-bold text-user bg-red q-px-xs">
|
||||
<div
|
||||
v-if="tools.isLogged() && isAdmin()"
|
||||
class="text-weight-bold text-user bg-red q-px-xs"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
<div v-if="isSocio" class="text-weight-bold text-user q-px-xs">
|
||||
<div
|
||||
v-if="isSocio"
|
||||
class="text-weight-bold text-user q-px-xs"
|
||||
>
|
||||
Socio
|
||||
</div>
|
||||
<div v-if="isSocioResidente()" class="text-weight-bold text-user q-px-xs bg-amber">
|
||||
<div
|
||||
v-if="isSocioResidente()"
|
||||
class="text-weight-bold text-user q-px-xs bg-amber"
|
||||
>
|
||||
Residente
|
||||
</div>
|
||||
<div v-if="isConsiglio()" class="text-weight-bold text-user q-px-xs bg-deep-orange-10">
|
||||
<div
|
||||
v-if="isConsiglio()"
|
||||
class="text-weight-bold text-user q-px-xs bg-deep-orange-10"
|
||||
>
|
||||
Consiglio
|
||||
</div>
|
||||
<div v-if="tools.isManager()" class="text-weight-bold text-user bg-blue q-px-xs">
|
||||
<div
|
||||
v-if="tools.isManager()"
|
||||
class="text-weight-bold text-user bg-blue q-px-xs"
|
||||
>
|
||||
Segreteria
|
||||
</div>
|
||||
<div v-if="tools.isEditor()" class="text-weight-bold text-user bg-indigo q-px-xs">
|
||||
<div
|
||||
v-if="tools.isEditor()"
|
||||
class="text-weight-bold text-user bg-indigo q-px-xs"
|
||||
>
|
||||
Editore
|
||||
</div>
|
||||
<div v-if="tools.isCommerciale()" class="text-weight-bold text-user bg-brown q-px-xs">
|
||||
<div
|
||||
v-if="tools.isCommerciale()"
|
||||
class="text-weight-bold text-user bg-brown q-px-xs"
|
||||
>
|
||||
Commerciale
|
||||
</div>
|
||||
<div v-if="isFacilitatore()" class="text-weight-bold text-user q-px-xs">
|
||||
<div
|
||||
v-if="isFacilitatore()"
|
||||
class="text-weight-bold text-user q-px-xs"
|
||||
>
|
||||
Facilitatore
|
||||
</div>
|
||||
<div v-if="isTratuttrici()" class="text-weight-bold text-user q-px-xs">
|
||||
<div
|
||||
v-if="isTratuttrici()"
|
||||
class="text-weight-bold text-user q-px-xs"
|
||||
>
|
||||
Editor
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!tools.isLogged()" class="text-user text-italic bg-red">
|
||||
{{ t("user.loggati") }}
|
||||
<div
|
||||
v-if="!tools.isLogged()"
|
||||
class="text-user text-italic bg-red"
|
||||
>
|
||||
{{ t('user.loggati') }}
|
||||
</div>
|
||||
|
||||
<div v-if="tools.isLogged() && !tools.isVerified()" class="text-verified">
|
||||
{{ t("components.authentication.email_verification.verify_email") }}
|
||||
<div
|
||||
v-if="tools.isLogged() && !tools.isVerified()"
|
||||
class="text-verified"
|
||||
>
|
||||
{{ t('components.authentication.email_verification.verify_email') }}
|
||||
</div>
|
||||
|
||||
<div v-if="tools.isLogged()" class="text-verified">
|
||||
<div
|
||||
v-if="tools.isLogged()"
|
||||
class="text-verified"
|
||||
>
|
||||
<!-- <span class="text-white" v-if="Verificato()"> {{t('reg.verificato')}} </span> -->
|
||||
<span class="text-user text-italic bg-red" v-if="!tools.Verificato()">
|
||||
{{ t("reg.non_verificato") }}
|
||||
<span
|
||||
class="text-user text-italic bg-red"
|
||||
v-if="!tools.Verificato()"
|
||||
>
|
||||
{{ t('reg.non_verificato') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="tools.isLogged()" id="user-actions" class="column justify-center q-gutter-sm q-ma-sm center-150">
|
||||
<q-btn rounded color="primary" icon="person" :to="`/my/` + getMyUsername()">{{ t("pages.profile") }}
|
||||
<div
|
||||
v-if="tools.isLogged()"
|
||||
id="user-actions"
|
||||
class="column justify-center q-gutter-sm q-ma-sm center-150"
|
||||
>
|
||||
<q-btn
|
||||
rounded
|
||||
color="primary"
|
||||
icon="person"
|
||||
:to="`/my/` + getMyUsername()"
|
||||
>{{ t('pages.profile') }}
|
||||
</q-btn>
|
||||
|
||||
<q-btn rounded color="negative" icon="exit_to_app" @click="logoutHandler">{{ t("login.esci") }}</q-btn>
|
||||
<q-btn
|
||||
rounded
|
||||
color="negative"
|
||||
icon="exit_to_app"
|
||||
@click="logoutHandler"
|
||||
>{{ t('login.esci') }}</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 120px"></div>
|
||||
<div v-show="!tools.isLogged()">
|
||||
<div v-if="site.confpages && site.confpages?.showRegButton" class="q-ma-md" style="">
|
||||
<div
|
||||
v-if="site.confpages && site.confpages?.showRegButton"
|
||||
class="q-ma-md"
|
||||
style=""
|
||||
>
|
||||
<CSigninNoreg :showregbutt="site.confpages && site.confpages?.showRegButton">
|
||||
</CSigninNoreg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tools.isLogged()" class="q-mt-lg"></div>
|
||||
|
||||
<div
|
||||
v-if="tools.isLogged()"
|
||||
class="q-mt-lg"
|
||||
></div>
|
||||
</q-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./MyHeader.ts">
|
||||
</script>
|
||||
<script lang="ts" src="./MyHeader.ts"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./MyHeader.scss";
|
||||
@import './MyHeader.scss';
|
||||
</style>
|
||||
|
||||
0
src/components/home/HomePage.scss
Normal file
0
src/components/home/HomePage.scss
Normal file
168
src/components/home/HomePage.ts
Normal file
168
src/components/home/HomePage.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { defineComponent, computed, ref, onMounted, watch } from 'vue';
|
||||
import { useHomeStore } from 'src/stores/home.store';
|
||||
import type { HomeCMS, GalleryItem, Pillar } from 'src/types/home';
|
||||
import { date } from 'quasar';
|
||||
import { Notify } from 'quasar';
|
||||
import './HomePage.scss';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HomePage',
|
||||
props: {
|
||||
initialData: { type: Object as () => HomeCMS | undefined, default: undefined },
|
||||
enableParallax: { type: Boolean, default: true },
|
||||
// SEO/Head (esposti per integrazione router-meta)
|
||||
pageTitle: { type: String, default: 'Comunità & Permacultura' },
|
||||
pageDescription: { type: String, default: 'Vita di comunità, autosufficienza, eventi e progetti.' },
|
||||
ogImage: { type: String, default: '' }
|
||||
},
|
||||
setup(props) {
|
||||
const store = useHomeStore();
|
||||
|
||||
// Stato UI editor
|
||||
const sectionsEnabled = ref({
|
||||
hero: true,
|
||||
vision: true,
|
||||
pillars: true,
|
||||
events: true,
|
||||
collabora: true,
|
||||
testimonials: true,
|
||||
gallery: true,
|
||||
faq: true,
|
||||
posts: true,
|
||||
map: true,
|
||||
newsletter: true,
|
||||
finalCta: true
|
||||
});
|
||||
|
||||
const sectionOptions = [
|
||||
{ key: 'hero', label: 'Hero' },
|
||||
{ key: 'vision', label: 'Visione' },
|
||||
{ key: 'pillars', label: 'Pillars' },
|
||||
{ key: 'events', label: 'Eventi' },
|
||||
{ key: 'collabora', label: 'Collabora' },
|
||||
{ key: 'testimonials', label: 'Testimonianze' },
|
||||
{ key: 'gallery', label: 'Galleria' },
|
||||
{ key: 'faq', label: 'FAQ' },
|
||||
{ key: 'posts', label: 'News' },
|
||||
{ key: 'map', label: 'Mappa' },
|
||||
{ key: 'newsletter', label: 'Newsletter' },
|
||||
{ key: 'finalCta', label: 'CTA finale' }
|
||||
] as const;
|
||||
|
||||
// Lightbox
|
||||
const lightbox = ref<{ open: boolean; current?: GalleryItem | null }>({ open: false, current: null });
|
||||
const currentImage = computed(() => lightbox.value.current || null);
|
||||
const openLightbox = (g: GalleryItem) => { lightbox.value.open = true; lightbox.value.current = g; };
|
||||
|
||||
// Carousel
|
||||
const carouselSlide = ref(0);
|
||||
const pauseCarousel = ref(false);
|
||||
|
||||
// Newsletter
|
||||
const newsletter = ref({ email: '' });
|
||||
const emailRule = (val: string) => /.+@.+\..+/.test(val) || 'Email non valida';
|
||||
const subscribe = async () => {
|
||||
try {
|
||||
await store.subscribeNewsletter(newsletter.value.email);
|
||||
Notify.create({ type: 'positive', message: 'Iscrizione effettuata. Grazie!' });
|
||||
newsletter.value.email = '';
|
||||
} catch (e: any) {
|
||||
// Lo snackbar globale è già gestito dall’interceptor, ma mostriamo feedback locale
|
||||
Notify.create({ type: 'negative', message: e?.message || 'Errore iscrizione' });
|
||||
}
|
||||
};
|
||||
|
||||
// Collabora
|
||||
const collaboraOptions = [
|
||||
{ key: 'vol', title: 'Volontariato', icon: 'volunteer_activism', excerpt: 'Dai una mano ai progetti in corso.', cta: 'Scrivici', to: '/collabora' },
|
||||
{ key: 'res', title: 'Residenzialità', icon: 'home', excerpt: 'Vivi con noi periodi di prova e scambio.', cta: 'Info', to: '/residenzialita' },
|
||||
{ key: 'don', title: 'Sostieni', icon: 'diversity_2', excerpt: 'Sostieni l’ecovillaggio con una donazione.', cta: 'Dona ora', to: '/sostieni' }
|
||||
] as const;
|
||||
|
||||
// Vision values: usiamo i primi 4 pillars come “valori”
|
||||
const visionValues = computed<Pillar[]>(() => (store.data?.pillars || []).slice(0, 4));
|
||||
|
||||
// Link utili
|
||||
const collaboraLink = computed(() => '/collabora');
|
||||
const eventsLink = computed(() => '/calendario-eventi');
|
||||
const directionsLink = computed(() => 'https://maps.app.goo.gl/');
|
||||
|
||||
// Stato derivato
|
||||
const data = computed(() => store.data);
|
||||
const loading = computed(() => store.loading);
|
||||
const eventsState = computed(() => ({ loading: store.loadingEvents, error: store.errorEvents }));
|
||||
const postsState = computed(() => ({ loading: store.loadingPosts, error: store.errorPosts }));
|
||||
const nextEvents = computed(() => store.nextEvents);
|
||||
const latestPosts = computed(() => store.latestPosts);
|
||||
|
||||
// Formattazione date
|
||||
const formatDate = (iso: string) => date.formatDate(iso, 'D MMMM YYYY', { locale: 'it-IT' });
|
||||
|
||||
// CTA click (tracking, scroll, ecc.)
|
||||
const onCta = (cta: { label: string; to?: string; href?: string }) => {
|
||||
if (cta.to === '/calendario-eventi') {
|
||||
// esempio: potresti fare scroll a sezione eventi
|
||||
const el = document.getElementById('events-heading');
|
||||
el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
// Caricamento iniziale
|
||||
const reloadAll = async () => {
|
||||
await Promise.all([
|
||||
store.fetchHome(props.initialData),
|
||||
store.fetchEvents(),
|
||||
store.fetchPosts()
|
||||
]);
|
||||
};
|
||||
|
||||
// Salvataggio layout (solo client-side per demo)
|
||||
const saveLayout = () => {
|
||||
localStorage.setItem('home.sections', JSON.stringify(sectionsEnabled.value));
|
||||
Notify.create({ type: 'positive', message: 'Layout salvato (locale).' });
|
||||
};
|
||||
|
||||
// Ripristino layout
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const saved = localStorage.getItem('home.sections');
|
||||
if (saved) sectionsEnabled.value = JSON.parse(saved);
|
||||
} catch {}
|
||||
await reloadAll();
|
||||
});
|
||||
|
||||
// Aggiorna vision se cambia data
|
||||
watch(() => store.data?.pillars, () => { /* no-op, computed si aggiorna */ });
|
||||
|
||||
return {
|
||||
// props
|
||||
enableParallax: props.enableParallax,
|
||||
|
||||
// data
|
||||
data, loading,
|
||||
sectionsEnabled, sectionOptions,
|
||||
|
||||
// events
|
||||
nextEvents, latestPosts, eventsState, postsState,
|
||||
formatDate,
|
||||
|
||||
// collabora
|
||||
collaboraOptions, collaboraLink, eventsLink, directionsLink,
|
||||
|
||||
// gallery
|
||||
lightbox, currentImage, openLightbox,
|
||||
|
||||
// newsletter
|
||||
newsletter, emailRule, subscribe,
|
||||
|
||||
// carousel
|
||||
carouselSlide, pauseCarousel,
|
||||
|
||||
// editor actions
|
||||
reloadAll, saveLayout,
|
||||
|
||||
// cta
|
||||
onCta
|
||||
};
|
||||
}
|
||||
});
|
||||
392
src/components/home/HomePage.vue
Normal file
392
src/components/home/HomePage.vue
Normal file
@@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<q-page class="home-page">
|
||||
<!-- Skip link -->
|
||||
<a class="skip-link" href="#main-content">Salta al contenuto principale</a>
|
||||
|
||||
<!-- Toolbar editor (toggle sezioni) -->
|
||||
<div class="editor-toolbar q-pa-md q-gutter-sm" role="region" aria-label="Editor sezione pagina">
|
||||
<q-card flat bordered class="q-pa-md rounded-xl editor-card">
|
||||
<div class="row items-center q-col-gutter-sm">
|
||||
<div class="col-12 col-sm-auto">
|
||||
<div class="text-subtitle1 text-weight-medium">Sezioni visibili</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm">
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-auto" v-for="opt in sectionOptions" :key="opt.key">
|
||||
<q-toggle
|
||||
v-model="sectionsEnabled[opt.key]"
|
||||
:label="opt.label"
|
||||
size="md"
|
||||
color="primary"
|
||||
keep-color
|
||||
dense
|
||||
:aria-label="`Attiva sezione ${opt.label}`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-auto q-gutter-sm flex">
|
||||
<q-btn unelevated color="primary" icon="refresh" label="Ricarica contenuti" @click="reloadAll" />
|
||||
<q-btn flat color="primary" icon="save" label="Salva configurazione" @click="saveLayout" />
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- HERO -->
|
||||
<section v-if="sectionsEnabled.hero" class="section section--hero" aria-labelledby="hero-heading">
|
||||
<q-skeleton v-if="loading && !data?.hero" type="rect" height="60vh" />
|
||||
<template v-else>
|
||||
<q-parallax v-if="enableParallax" :src="data?.hero?.mediaUrl" :height="560">
|
||||
<div class="hero-overlay" aria-hidden="true"></div>
|
||||
<div class="hero-content">
|
||||
<q-badge v-if="data?.hero?.badge" color="secondary" class="q-mb-md">{{ data?.hero?.badge }}</q-badge>
|
||||
<h1 id="hero-heading" class="hero-title">{{ data?.hero?.title }}</h1>
|
||||
<p class="hero-subtitle" v-if="data?.hero?.subtitle">{{ data?.hero?.subtitle }}</p>
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn
|
||||
v-for="(cta, i) in data?.hero?.ctas"
|
||||
:key="'hero-cta-' + i"
|
||||
:label="cta.label"
|
||||
color="primary"
|
||||
unelevated
|
||||
:to="cta.to"
|
||||
:href="cta.href"
|
||||
@click="onCta(cta)"
|
||||
/>
|
||||
</div>
|
||||
<slot name="hero-extra" />
|
||||
</div>
|
||||
</q-parallax>
|
||||
|
||||
<div v-else class="hero-fallback">
|
||||
<q-img :src="data?.hero?.mediaUrl" ratio="16/9">
|
||||
<div class="absolute-full hero-overlay"></div>
|
||||
<div class="absolute-full flex flex-center column hero-content">
|
||||
<q-badge v-if="data?.hero?.badge" color="secondary" class="q-mb-md">{{ data?.hero?.badge }}</q-badge>
|
||||
<h1 id="hero-heading" class="hero-title">{{ data?.hero?.title }}</h1>
|
||||
<p class="hero-subtitle" v-if="data?.hero?.subtitle">{{ data?.hero?.subtitle }}</p>
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn
|
||||
v-for="(cta, i) in data?.hero?.ctas"
|
||||
:key="'hero-cta2-' + i"
|
||||
:label="cta.label"
|
||||
color="primary"
|
||||
unelevated
|
||||
:to="cta.to"
|
||||
:href="cta.href"
|
||||
@click="onCta(cta)"
|
||||
/>
|
||||
</div>
|
||||
<slot name="hero-extra" />
|
||||
</div>
|
||||
</q-img>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<main id="main-content">
|
||||
<!-- Visione/Mission -->
|
||||
<section v-if="sectionsEnabled.vision" class="section section--vision" aria-labelledby="vision-heading">
|
||||
<div class="container">
|
||||
<h2 id="vision-heading" class="section-title">Visione & Mission</h2>
|
||||
<q-skeleton v-if="loading && !data?.pillars?.length" type="text" class="q-mb-md" />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div
|
||||
v-for="(val, idx) in visionValues"
|
||||
:key="'vision-' + idx"
|
||||
class="col-12 col-sm-6 col-md-3"
|
||||
>
|
||||
<q-card flat bordered class="rounded-xl h-100">
|
||||
<q-card-section class="text-center">
|
||||
<q-icon :name="val.icon" size="md" aria-hidden="true" />
|
||||
<div class="text-subtitle1 q-mt-sm">{{ val.title }}</div>
|
||||
<div class="text-body2 text-secondary" v-html="val.excerpt"></div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pillars -->
|
||||
<section v-if="sectionsEnabled.pillars" class="section section--pillars" aria-labelledby="pillars-heading">
|
||||
<div class="container">
|
||||
<h2 id="pillars-heading" class="section-title">Il nostro Progetto</h2>
|
||||
<q-skeleton v-if="loading && !data?.pillars?.length" type="rect" height="120px" class="q-mb-md" />
|
||||
<div v-else class="row q-col-gutter-md">
|
||||
<div
|
||||
v-for="p in data?.pillars"
|
||||
:key="p.id"
|
||||
class="col-12 col-md-4"
|
||||
>
|
||||
<q-card flat bordered class="rounded-xl h-100">
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap">
|
||||
<q-icon :name="p.icon" size="md" class="q-mr-sm" :aria-label="p.title" />
|
||||
<div class="text-h6 q-my-none">{{ p.title }}</div>
|
||||
</div>
|
||||
<p class="q-mt-sm text-body2">{{ p.excerpt }}</p>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat color="primary" label="Scopri di più" :to="p.to" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Eventi -->
|
||||
<section v-if="sectionsEnabled.events" class="section section--events" aria-labelledby="events-heading">
|
||||
<div class="container">
|
||||
<div class="row items-end justify-between q-mb-md">
|
||||
<h2 id="events-heading" class="section-title col-auto">Eventi</h2>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="primary" label="Vedi tutti" to="/calendario-eventi" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="eventsState.loading" class="q-gutter-md">
|
||||
<q-skeleton type="rect" height="120px" v-for="i in 3" :key="'ev-sk-' + i" />
|
||||
</div>
|
||||
<div v-else-if="eventsState.error" class="empty-state">
|
||||
<q-icon name="warning" class="q-mr-sm" />
|
||||
<span>{{ eventsState.error }}</span>
|
||||
</div>
|
||||
<div v-else-if="!nextEvents?.length" class="empty-state">
|
||||
<q-icon name="event_busy" class="q-mr-sm" />
|
||||
<span>Nessun evento in programma. <q-btn flat color="primary" label="Proponi un evento" :to="collaboraLink" /></span>
|
||||
</div>
|
||||
<div v-else class="row q-col-gutter-md">
|
||||
<div v-for="ev in nextEvents" :key="ev.id" class="col-12 col-md-6 col-lg-3">
|
||||
<q-card flat bordered class="rounded-xl h-100">
|
||||
<q-img :src="ev.cover" ratio="16/9" />
|
||||
<q-card-section>
|
||||
<div class="text-subtitle1 q-mb-xs">{{ ev.title }}</div>
|
||||
<div class="text-caption text-secondary">
|
||||
<q-icon name="event" size="16px" aria-hidden="true" /> {{ formatDate(ev.start) }}
|
||||
<span v-if="ev.place"> · <q-icon name="place" size="16px" aria-hidden="true" /> {{ ev.place }}</span>
|
||||
</div>
|
||||
<p class="q-mt-sm text-body2 ellipsis-2-lines">{{ ev.teaser }}</p>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat color="primary" label="Dettagli/Iscriviti" :to="ev.to" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="below-events" />
|
||||
</section>
|
||||
|
||||
<!-- Collabora / Unisciti -->
|
||||
<section v-if="sectionsEnabled.collabora" class="section section--collabora" aria-labelledby="collabora-heading">
|
||||
<div class="container">
|
||||
<h2 id="collabora-heading" class="section-title">Collabora / Unisciti</h2>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div v-for="opt in collaboraOptions" :key="opt.key" class="col-12 col-md-4">
|
||||
<q-card flat bordered class="rounded-xl h-100">
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap">
|
||||
<q-icon :name="opt.icon" size="md" class="q-mr-sm" />
|
||||
<div class="text-h6 q-my-none">{{ opt.title }}</div>
|
||||
</div>
|
||||
<p class="q-mt-sm text-body2">{{ opt.excerpt }}</p>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right" class="q-pt-none">
|
||||
<q-btn :label="opt.cta" color="primary" flat :href="opt.href" :to="opt.to" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="below-collabora" />
|
||||
</section>
|
||||
|
||||
<!-- Testimonianze -->
|
||||
<section v-if="sectionsEnabled.testimonials" class="section section--testi" aria-labelledby="testi-heading">
|
||||
<div class="container">
|
||||
<h2 id="testi-heading" class="section-title">Testimonianze</h2>
|
||||
<q-carousel
|
||||
v-model="carouselSlide"
|
||||
:autoplay="5000"
|
||||
animated
|
||||
transition-prev="slide-right"
|
||||
transition-next="slide-left"
|
||||
swipeable
|
||||
infinite
|
||||
height="220px"
|
||||
@mouseenter="pauseCarousel = true"
|
||||
@mouseleave="pauseCarousel = false"
|
||||
:autoplay="pauseCarousel ? 0 : 5000"
|
||||
>
|
||||
<q-carousel-slide
|
||||
v-for="t in data?.testimonials"
|
||||
:key="t.id"
|
||||
>
|
||||
<div class="testimonial">
|
||||
<q-avatar v-if="t.avatar" size="56px" class="q-mb-sm"><img :src="t.avatar" :alt="t.author" /></q-avatar>
|
||||
<blockquote class="quote">“{{ t.quote }}”</blockquote>
|
||||
<div class="author">{{ t.author }} <span v-if="t.role" class="role">— {{ t.role }}</span></div>
|
||||
</div>
|
||||
</q-carousel-slide>
|
||||
</q-carousel>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Galleria -->
|
||||
<section v-if="sectionsEnabled.gallery" class="section section--gallery" aria-labelledby="gallery-heading">
|
||||
<div class="container">
|
||||
<h2 id="gallery-heading" class="section-title">Galleria</h2>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div v-for="g in data?.gallery" :key="g.id" class="col-6 col-md-3">
|
||||
<q-img :src="g.src" :alt="g.alt" ratio="1" class="rounded-xl cursor-pointer" @click="openLightbox(g)" />
|
||||
</div>
|
||||
</div>
|
||||
<q-dialog v-model="lightbox.open" persistent>
|
||||
<q-card class="bg-dark text-white">
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div>{{ currentImage?.alt }}</div>
|
||||
<q-btn flat dense round icon="close" v-close-popup aria-label="Chiudi" />
|
||||
</q-card-section>
|
||||
<q-img :src="currentImage?.src" :alt="currentImage?.alt" ratio="16/9" />
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ -->
|
||||
<section v-if="sectionsEnabled.faq" class="section section--faq" aria-labelledby="faq-heading">
|
||||
<div class="container">
|
||||
<h2 id="faq-heading" class="section-title">FAQ</h2>
|
||||
<q-list bordered class="rounded-xl">
|
||||
<q-expansion-item
|
||||
v-for="(f, i) in data?.faq"
|
||||
:key="'faq-' + i"
|
||||
expand-separator
|
||||
:label="f.q"
|
||||
dense
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section class="text-body2">{{ f.a }}</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- News / Blog -->
|
||||
<section v-if="sectionsEnabled.posts" class="section section--news" aria-labelledby="news-heading">
|
||||
<div class="container">
|
||||
<div class="row items-end justify-between q-mb-md">
|
||||
<h2 id="news-heading" class="section-title">News / Blog</h2>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="primary" label="Tutti gli articoli" to="/blog" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="postsState.loading" class="q-gutter-md">
|
||||
<q-skeleton type="rect" height="100px" v-for="i in 3" :key="'post-sk-' + i" />
|
||||
</div>
|
||||
<div v-else-if="postsState.error" class="empty-state">
|
||||
<q-icon name="warning" class="q-mr-sm" />
|
||||
<span>{{ postsState.error }}</span>
|
||||
</div>
|
||||
<div v-else class="row q-col-gutter-md">
|
||||
<div v-for="p in latestPosts" :key="p.id" class="col-12 col-md-4">
|
||||
<q-card flat bordered class="rounded-xl h-100">
|
||||
<q-img v-if="p.cover" :src="p.cover" ratio="16/9" />
|
||||
<q-card-section>
|
||||
<div class="text-subtitle1">{{ p.title }}</div>
|
||||
<div class="text-caption text-secondary q-mt-xs">
|
||||
<q-icon name="schedule" size="16px" aria-hidden="true" />
|
||||
{{ formatDate(p.date) }}
|
||||
<span v-if="p.category"> · {{ p.category }}</span>
|
||||
</div>
|
||||
<p class="q-mt-sm text-body2 ellipsis-3-lines">{{ p.teaser }}</p>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat color="primary" label="Leggi tutto" :to="p.to" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mappa / Sedi -->
|
||||
<section v-if="sectionsEnabled.map" class="section section--map" aria-labelledby="map-heading">
|
||||
<div class="container">
|
||||
<h2 id="map-heading" class="section-title">Dove siamo</h2>
|
||||
<div class="map-wrap rounded-xl">
|
||||
<slot name="map">
|
||||
<iframe
|
||||
class="map-iframe"
|
||||
title="Mappa della comunità"
|
||||
src="https://www.openstreetmap.org/export/embed.html?bbox=12.30%2C45.21%2C12.34%2C45.23&layer=mapnik"
|
||||
style="border:0;"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
></iframe>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="q-mt-md">
|
||||
<q-btn color="primary" unelevated label="Come arrivare" :href="directionsLink" target="_blank" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Newsletter -->
|
||||
<section v-if="sectionsEnabled.newsletter" class="section section--newsletter" aria-labelledby="newsletter-heading">
|
||||
<div class="container">
|
||||
<h2 id="newsletter-heading" class="section-title">Newsletter</h2>
|
||||
<q-form @submit.prevent="subscribe">
|
||||
<div class="row items-center q-col-gutter-sm">
|
||||
<div class="col-12 col-md">
|
||||
<q-input
|
||||
v-model="newsletter.email"
|
||||
type="email"
|
||||
label="La tua email"
|
||||
:rules="[emailRule]"
|
||||
dense
|
||||
outlined
|
||||
aria-label="Email per iscrizione newsletter"
|
||||
>
|
||||
<template #prepend><q-icon name="mail" /></template>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-auto">
|
||||
<q-btn type="submit" color="primary" unelevated label="Iscriviti" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-caption text-secondary q-mt-xs">
|
||||
Iscrivendoti accetti la <a :href="privacyLink">privacy policy</a>.
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA finale -->
|
||||
<section v-if="sectionsEnabled.finalCta" class="section section--cta" aria-labelledby="cta-heading">
|
||||
<div class="container">
|
||||
<div class="cta-card rounded-xl">
|
||||
<h2 id="cta-heading" class="cta-title">Progettiamo insieme un nuovo mondo</h2>
|
||||
<div class="q-gutter-sm">
|
||||
<q-btn color="secondary" unelevated label="Eventi" :to="eventsLink" />
|
||||
<q-btn color="primary" unelevated label="Collabora" :to="collaboraLink" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="footer-cta" />
|
||||
</section>
|
||||
</main>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./HomePage.ts"></script>
|
||||
<style lang="scss" scoped>
|
||||
@import './HomePage.scss';
|
||||
</style>
|
||||
@@ -2684,7 +2684,90 @@ body.body--dark {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ordine_scontato{
|
||||
.ordine_scontato {
|
||||
color: gray;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: white;
|
||||
|
||||
// Altezza minima per evitare il collassamento
|
||||
min-height: 400px;
|
||||
|
||||
// Centra il contenuto verticalmente
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
// Background: immagine + overlay
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5); // overlay scuro
|
||||
z-index: 1;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2; // sopra l'overlay
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.intro {
|
||||
font-size: 1.3rem;
|
||||
color: #ddd; // testo chiaro, visibile sopra lo sfondo scuro
|
||||
margin-bottom: 1.5rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
padding: 14px 28px;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border-radius: 50px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 10px rgba(76, 175, 80, 0.3);
|
||||
align-self: center; // centrato in flex-column
|
||||
|
||||
&:hover {
|
||||
background-color: #388e3c;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 14px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
// Stili aggiuntivi per la pagina
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.09);
|
||||
padding: 32px;
|
||||
margin-bottom: 32px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
@@ -8,8 +8,6 @@ const msg_website_enUs = {
|
||||
products: {
|
||||
quantity: 'Quantità',
|
||||
quantityAvailable: 'Disponibili',
|
||||
stockQty: 'In Magazzino',
|
||||
stockBloccatiQty: 'Bloccati In Magazzino',
|
||||
weight: 'Peso',
|
||||
stars: 'Voto',
|
||||
color: 'Colore',
|
||||
@@ -38,7 +36,6 @@ const msg_website_enUs = {
|
||||
productslist: 'Lista Prodotti',
|
||||
collabora: 'Collabora',
|
||||
storehouses: 'Magazzino',
|
||||
providers: 'Fornitori',
|
||||
departments: 'Uffici',
|
||||
orders: 'Ordini Ricevuti',
|
||||
orders2: 'Ordini Ricevuti',
|
||||
|
||||
@@ -8,7 +8,6 @@ const msg_website_es = {
|
||||
products: {
|
||||
quantity: 'Quantità',
|
||||
quantityAvailable: 'Disponibili',
|
||||
stockQty: 'In Magazzino',
|
||||
weight: 'Peso',
|
||||
stars: 'Voto',
|
||||
color: 'Colore',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const msg_website_it = {
|
||||
ws: {
|
||||
sitename: 'Nuovo Mondo',
|
||||
siteshortname: 'NuovoMondo',
|
||||
sitename: 'Gruppo Macro',
|
||||
siteshortname: 'Gruppo Macro',
|
||||
description: '',
|
||||
keywords: '',
|
||||
},
|
||||
@@ -30,27 +30,6 @@ const msg_website_it = {
|
||||
test: 'Test',
|
||||
projects: 'Progetti',
|
||||
report: 'Report Ore',
|
||||
producer: 'Produttore',
|
||||
orderinfo: 'Ordini Effettuati',
|
||||
products: 'Prodotti',
|
||||
cash: 'Cassa',
|
||||
productInfos: 'Info Prodotti',
|
||||
listinoprodotti: 'Listino Prodotti',
|
||||
productslist: 'Lista Prodotti',
|
||||
collabora: 'Collabora',
|
||||
categories: 'Categorie',
|
||||
storehouses: 'Magazzino',
|
||||
providers: 'Fornitori',
|
||||
catprods: 'Categorie',
|
||||
subcatprods: 'Sotto-Categorie',
|
||||
gasordine: 'Gas Ordine',
|
||||
scontisticas: 'Scontistica',
|
||||
departments: 'Uffici',
|
||||
orders: 'Ordini Ricevuti',
|
||||
orders2: 'Ordini Ricevuti',
|
||||
sharewithus: 'Condividi con Noi',
|
||||
checkout: 'Carrello',
|
||||
payment: 'Pagamenti',
|
||||
regok: 'Registrazione Confermata',
|
||||
presentazione: 'Presentazione',
|
||||
presentazione2: 'Presentazione',
|
||||
@@ -96,14 +75,12 @@ const msg_website_it = {
|
||||
eventodef: 'Evento:',
|
||||
prova: 'prova',
|
||||
dbop: 'Operazioni',
|
||||
dbopmacro: 'Operazioni Macro',
|
||||
projall: 'Comunitari',
|
||||
groups: 'Lista Gruppi',
|
||||
projectsShared: 'Condivisi da me',
|
||||
myprojects: 'Privati',
|
||||
favproj: 'Favoriti',
|
||||
admin_ecommerce: 'ECommerce',
|
||||
ecommerce: 'Prodotti',
|
||||
ecommerce_menu: 'ECommerce1',
|
||||
hours: 'Ore',
|
||||
department: 'Uffici',
|
||||
title: 'Titolo',
|
||||
@@ -132,9 +109,8 @@ const msg_website_it = {
|
||||
onlyif_logged: 'Solo se Loggati',
|
||||
only_residenti: 'Solo Residenti',
|
||||
only_consiglio: 'Solo Consiglieri',
|
||||
only_collab: 'Solo Collaboratori',
|
||||
color: 'Colore',
|
||||
gasordini: 'Gas Ordini',
|
||||
gestoreordini: 'Gestore Ordini',
|
||||
},
|
||||
msg: {
|
||||
myAppName: 'Più che Buono',
|
||||
@@ -196,7 +172,18 @@ const msg_website_it = {
|
||||
descr: '<ul class="mylist" style="padding-left: 20px;">'
|
||||
+ '<li>📱<strong>Condividendo la APP</strong> a tutti coloro che vogliono far parte insieme della crescita e sviluppo di una Nuova Era</li>'
|
||||
+ '<li>👥 Aiutando a creare Gruppi Territoriali nella vostra città, impegnandosi a realizzare progetti per il Bene Comune, in onore ai principi Amorevoli e di condivisione.</li>'
|
||||
+ '<li>🌱 Sostenendo le persone attorno a voi, e rispettando la nostra vera Casa: Madre Natura e Tutti gli Esseri Viventi. ❤️</li>' +
|
||||
+ '<li>🌱 Sostenendo le persone attorno a voi, e rispettando la nostra vera Casa: Madre Natura e Tutti gli Esseri Viventi. ❤️</li>'
|
||||
+ '<li>👨🏻💻 Con una <strong>piccola donazione</strong> per le spese dei Server, manutenzione e per i continui sviluppi e miglioramenti</li></ul>' +
|
||||
'1) Tramite <strong><a href="https://paypal.me/paoloarena" target="_blank">Paypal</a></strong>:<br>' +
|
||||
'<br>2) Tramite <strong>Satispay</strong>: <a href="https://www.satispay.com/app/match/link/money-box/S6Y-SVN--62712D42-35B0-4BB9-8511-410C2AB8CD45" target="_blank">Clicca qui</a><br>' +
|
||||
'<div style="font-size: 1rem; background-color: white; color: blue; border: solid 2px #f00; margin: 5px; padding: 5px; border-radius: 10px; " ' +
|
||||
'class="row justify-around">' +
|
||||
'Se ancora non hai Satispay <a href="https://www.satispay.com/promo/PAOLOARENA4">Richiedila cliccando qui</a></br>' +
|
||||
'</div>' +
|
||||
'<br>3) Tramite <strong>Bonifico Bancario</strong>:<br>' +
|
||||
'(Scrivi a Surya (<a href="https://t.me/surya1977">@surya1977</a>) per le coordinate</br>' +
|
||||
'' +
|
||||
'4) In alternativa scegli tu una forma di Dono <br />' +
|
||||
'Grazie Mille per l\'Aiuto ed il Supporto' +
|
||||
'<br>',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* PIUCHEBUONO APP
|
||||
/* GRUPPOMACRO APP
|
||||
*/
|
||||
import type {
|
||||
import {
|
||||
IListRoutes,
|
||||
ILang,
|
||||
IPreloadImages,
|
||||
@@ -8,6 +8,30 @@ import type {
|
||||
} from '@model'
|
||||
|
||||
|
||||
// const SHOW_PROJINTHEMENU = false
|
||||
//
|
||||
// let arrlistafavourite = []
|
||||
// let arrlistaprojtutti = []
|
||||
// let arrlistaprojmiei = []
|
||||
// if (SHOW_PROJINTHEMENU) {
|
||||
// arrlistaprojtutti = Projects.getters.listaprojects(RouteNames.projectsall)
|
||||
// arrlistaprojmiei = Projects.getters.listaprojects(RouteNames.myprojects)
|
||||
// arrlistafavourite = Projects.getters.listaprojects(RouteNames.favouriteprojects)
|
||||
// }
|
||||
// PROGETTI -> FAVORITI :
|
||||
|
||||
// if (arrlistafavourite.length > 0) {
|
||||
// arrMenu.push({
|
||||
// icon: 'favorite_border',
|
||||
// nametranslate: 'pages.' + RouteNames.favouriteprojects,
|
||||
// urlroute: RouteNames.favouriteprojects,
|
||||
// level_parent: 0.0,
|
||||
// level_child: 0.5,
|
||||
// routes2: arrlistafavourite,
|
||||
// idelem: ''
|
||||
// })
|
||||
// }
|
||||
|
||||
const firstPage = {
|
||||
active: true,
|
||||
order: 5,
|
||||
@@ -20,6 +44,7 @@ const firstPage = {
|
||||
infooter: true,
|
||||
}
|
||||
|
||||
|
||||
function getDynamicPages(site: ISites): IListRoutes[] {
|
||||
|
||||
const baseroutes: IListRoutes[] = [
|
||||
@@ -41,13 +66,43 @@ function getDynamicPages(site: ISites): IListRoutes[] {
|
||||
materialIcon: 'fas fa-test',
|
||||
name: 'mypages.test',
|
||||
component: () => import('@src/views/testServer/testServer.vue'),
|
||||
inmenu: false,
|
||||
infooter: false,
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
order: 400,
|
||||
path: '/test-lungo',
|
||||
materialIcon: 'fas fa-test',
|
||||
name: 'mypages.test_lungo',
|
||||
component: () => import('@src/views/testLungo/testLungo.vue'),
|
||||
inmenu: false,
|
||||
infooter: false,
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
order: 15,
|
||||
path: '/provapao',
|
||||
materialIcon: 'fas fa-house-user',
|
||||
name: 'mypages.provapao',
|
||||
component: () => import('@src/root/provapao/provapao.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
inmenu: false,
|
||||
infooter: false,
|
||||
},
|
||||
|
||||
/*{
|
||||
active: true,
|
||||
{
|
||||
active: site.confpages && site.confpages.enableCircuits,
|
||||
order: 16,
|
||||
path: '/circuits',
|
||||
materialIcon: 'fas fa-coins',
|
||||
name: 'mypages.circuits',
|
||||
component: () => import('@src/views/user/mycircuits/mycircuits.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
inmenu: true,
|
||||
infooter: true,
|
||||
},
|
||||
{
|
||||
active: site.confpages && site.confpages.enableEvents,
|
||||
order: 20,
|
||||
path: '/events',
|
||||
materialIcon: 'fas fa-bullhorn',
|
||||
@@ -56,17 +111,6 @@ function getDynamicPages(site: ISites): IListRoutes[] {
|
||||
meta: { requiresAuth: true },
|
||||
inmenu: true,
|
||||
infooter: true,
|
||||
},*/
|
||||
{
|
||||
active: site.confpages && site.confpages?.showProfile,
|
||||
order: 120,
|
||||
path: '/myprofile',
|
||||
materialIcon: 'fas fa-user',
|
||||
name: 'pages.profile',
|
||||
component: () => import('@src/views/user/myprofile/myprofile.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
inmenu: true,
|
||||
infooter: true,
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
@@ -80,7 +124,18 @@ function getDynamicPages(site: ISites): IListRoutes[] {
|
||||
infooter: false,
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
active: site.confpages && site.confpages.showProfile,
|
||||
order: 120,
|
||||
path: '/myprofile',
|
||||
materialIcon: 'fas fa-user',
|
||||
name: 'pages.profile',
|
||||
component: () => import('@src/views/user/myprofile/myprofile.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
inmenu: true,
|
||||
infooter: true,
|
||||
},
|
||||
{
|
||||
active: site.confpages && site.confpages.showProfile,
|
||||
order: 120,
|
||||
path: '/editprofile',
|
||||
materialIcon: 'fas fa-user',
|
||||
@@ -91,7 +146,7 @@ function getDynamicPages(site: ISites): IListRoutes[] {
|
||||
infooter: false,
|
||||
},
|
||||
{
|
||||
active: site.confpages && site.confpages?.showiscrittiMenu,
|
||||
active: site.confpages && site.confpages.showiscrittiMenu,
|
||||
order: 130,
|
||||
path: '/friends',
|
||||
materialIcon: 'fas fa-user-friends',
|
||||
@@ -102,20 +157,7 @@ function getDynamicPages(site: ISites): IListRoutes[] {
|
||||
infooter: true,
|
||||
},
|
||||
{
|
||||
active: site.confpages && site.confpages?.enableCircuits,
|
||||
order: 16,
|
||||
path: '/circuits',
|
||||
materialIcon: 'fas fa-coins',
|
||||
name: 'mypages.circuits',
|
||||
component: () => import('@src/views/user/mycircuits/mycircuits.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
inmenu: true,
|
||||
infooter: true,
|
||||
onlyAdmin: true,
|
||||
onlyManager: true,
|
||||
},
|
||||
{
|
||||
active: site.confpages && site.confpages?.enableGroups,
|
||||
active: site.confpages && site.confpages.enableGroups,
|
||||
order: 132,
|
||||
path: '/groups',
|
||||
materialIcon: 'fas fa-users',
|
||||
@@ -124,8 +166,6 @@ function getDynamicPages(site: ISites): IListRoutes[] {
|
||||
meta: { requiresAuth: true },
|
||||
inmenu: true,
|
||||
infooter: false,
|
||||
onlyAdmin: true,
|
||||
onlyManager: true,
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
@@ -201,16 +241,6 @@ function getDynamicPages(site: ISites): IListRoutes[] {
|
||||
inmenu: false,
|
||||
infooter: false,
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
order: 150,
|
||||
path: '/fundraising',
|
||||
materialIcon: 'fas fa-hand-holding-heart',
|
||||
name: 'pages.fundraising',
|
||||
component: () => import('@src/root/fundraising/fundraising.vue'),
|
||||
inmenu: false,
|
||||
infooter: false,
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
order: 80,
|
||||
|
||||
58
src/mocks/home.sample.json
Normal file
58
src/mocks/home.sample.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"hero": {
|
||||
"title": "Ecovillaggio Terra Viva",
|
||||
"subtitle": "Comunità, permacultura e autosufficienza per un futuro condiviso.",
|
||||
"badge": "Nuove date evento",
|
||||
"mediaUrl": "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=1600",
|
||||
"ctas": [
|
||||
{ "label": "Partecipa ad un incontro", "to": "/calendario-eventi" },
|
||||
{ "label": "Collabora / Unisciti", "to": "/collabora" ]
|
||||
]
|
||||
},
|
||||
"pillars": [
|
||||
{ "id": "perm", "icon": "spa", "title": "Permacultura", "excerpt": "Progettiamo sistemi rigenerativi.", "to": "/progetto/permacultura" },
|
||||
{ "id": "com", "icon": "groups", "title": "Comunità", "excerpt": "Relazioni consapevoli e mutuo aiuto.", "to": "/progetto/comunita" },
|
||||
{ "id": "edu", "icon": "school", "title": "Educazione", "excerpt": "Laboratori e formazione continua.", "to": "/progetto/educazione" }
|
||||
],
|
||||
"events": [
|
||||
{ "id": "e1", "title": "Open Day Ecovillaggio", "start": "2025-10-12T10:00:00Z", "place": "Podere Collealto", "teaser": "Visita guidata e pranzo condiviso.", "cover": "https://images.unsplash.com/photo-1501004318641-b39e6451bec6?q=80&w=1200", "to": "/eventi/open-day-ottobre" },
|
||||
{ "id": "e2", "title": "Corso di Permacultura Base", "start": "2025-11-02T09:00:00Z", "place": "Sala Comunitaria", "teaser": "Introduzione ai principi e al design.", "cover": "https://images.unsplash.com/photo-1461354464878-ad92f492a5a0?q=80&w=1200", "to": "/eventi/permacultura-base" },
|
||||
{ "id": "e3", "title": "Weekend di Costruzione Naturale", "start": "2025-12-06T09:00:00Z", "place": "Cantiere Paglia", "teaser": "Tecniche in paglia e terra cruda.", "cover": "https://images.unsplash.com/photo-1523419409543-8f3f3b00d3c1?q=80&w=1200", "to": "/eventi/costruzione-naturale" }
|
||||
],
|
||||
"testimonials": [
|
||||
{ "id": "t1", "quote": "Qui ho trovato persone e un progetto che risuonano con i miei valori.", "author": "Marta P.", "role": "Volontaria" },
|
||||
{ "id": "t2", "quote": "Un modo concreto di vivere la sostenibilità.", "author": "Giulio R.", "role": "Residente" },
|
||||
{ "id": "t3", "quote": "I laboratori mi hanno cambiato il punto di vista.", "author": "Elisa D.", "role": "Partecipante" }
|
||||
],
|
||||
"gallery": [
|
||||
{ "id": "g1", "src": "https://images.unsplash.com/photo-1520975793415-62f6d6c49c29?q=80&w=1200", "alt": "Orto sinergico" },
|
||||
{ "id": "g2", "src": "https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1200", "alt": "Case naturali" },
|
||||
{ "id": "g3", "src": "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?q=80&w=1200", "alt": "Cerchio di condivisione" },
|
||||
{ "id": "g4", "src": "https://images.unsplash.com/photo-1455218873509-8097305ee378?q=80&w=1200", "alt": "Compost e suolo vivo" },
|
||||
{ "id": "g5", "src": "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?q=80&w=1200", "alt": "Mulching" },
|
||||
{ "id": "g6", "src": "https://images.unsplash.com/photo-1493815793585-c19ebc0a49b9?q=80&w=1200", "alt": "Laboratorio educativo" }
|
||||
],
|
||||
"faq": [
|
||||
{ "q": "Si può venire a trovarvi?", "a": "Sì, durante gli Open Day o su appuntamento." },
|
||||
{ "q": "Come funziona il contributo spese?", "a": "Chiediamo un contributo libero e responsabile." },
|
||||
{ "q": "Accogliete famiglie con bambini?", "a": "Assolutamente sì." },
|
||||
{ "q": "Posso fare volontariato?", "a": "Scrivici dalla pagina Collabora." },
|
||||
{ "q": "Avete regole di convivenza?", "a": "Sì, basate su ascolto e responsabilità condivisa." },
|
||||
{ "q": "Sono previste residenzialità?", "a": "Periodi di prova e progettazione condivisa." },
|
||||
{ "q": "Come posso donare?", "a": "Trovi IBAN e PayPal nella pagina Sostieni." },
|
||||
{ "q": "Organizzate corsi?", "a": "Formazione continua su permacultura e autocostruzione." }
|
||||
],
|
||||
"posts": [
|
||||
{ "id": "p1", "title": "Raccolta dell’acqua piovana: guida pratica", "date": "2025-08-20", "category": "Permacultura", "teaser": "Sistemi semplici per risparmiare acqua.", "cover": "https://images.unsplash.com/photo-1451187580459-43490279c0fa?q=80&w=1200", "to": "/blog/raccolta-acqua" },
|
||||
{ "id": "p2", "title": "Vivere in comunità: strumenti di facilitazione", "date": "2025-09-01", "category": "Comunità", "teaser": "Decisioni condivise e gestione dei conflitti.", "cover": "https://images.unsplash.com/photo-1519681393784-d120267933ba?q=80&w=1200", "to": "/blog/facilitazione" },
|
||||
{ "id": "p3", "title": "Orto sinergico in 7 passi", "date": "2025-09-10", "category": "Orto", "teaser": "Dalla preparazione del suolo alla pacciamatura.", "cover": "https://images.unsplash.com/photo-1511690656952-34342bb7c2f2?q=80&w=1200", "to": "/blog/orto-sinergico" }
|
||||
],
|
||||
"partners": [
|
||||
{ "id": "pa1", "name": "Rete Permacultura", "logo": "https://dummyimage.com/160x60/4caf50/ffffff&text=Permacultura", "href": "#" },
|
||||
{ "id": "pa2", "name": "EcoHub", "logo": "https://dummyimage.com/160x60/6d4c41/ffffff&text=EcoHub", "href": "#" },
|
||||
{ "id": "pa3", "name": "GreenLab", "logo": "https://dummyimage.com/160x60/2e7d32/ffffff&text=GreenLab", "href": "#" },
|
||||
{ "id": "pa4", "name": "OpenSchool", "logo": "https://dummyimage.com/160x60/607d8b/ffffff&text=OpenSchool", "href": "#" },
|
||||
{ "id": "pa5", "name": "Terra Viva Coop", "logo": "https://dummyimage.com/160x60/795548/ffffff&text=Terra+Viva", "href": "#" },
|
||||
{ "id": "pa6", "name": "BioCostruire", "logo": "https://dummyimage.com/160x60/8bc34a/ffffff&text=BioCostruire", "href": "#" }
|
||||
]
|
||||
}
|
||||
27
src/pages/admin/AdminDashboard.vue
Normal file
27
src/pages/admin/AdminDashboard.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-card bordered flat class="q-pa-md">
|
||||
<div class="text-h6 q-mb-sm">Home</div>
|
||||
<div class="text-body2">Modifica Hero, Pillars e FAQ.</div>
|
||||
<q-btn class="q-mt-md" color="primary" :to="{ name:'admin-home' }" label="Apri editor Home" />
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-card bordered flat class="q-pa-md">
|
||||
<div class="text-h6 q-mb-sm">Eventi</div>
|
||||
<div class="text-body2">Gestisci calendario eventi.</div>
|
||||
<q-btn class="q-mt-md" color="primary" :to="{ name:'admin-events' }" label="Gestisci Eventi" />
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-card bordered flat class="q-pa-md">
|
||||
<div class="text-h6 q-mb-sm">Post</div>
|
||||
<div class="text-body2">News / Blog.</div>
|
||||
<q-btn class="q-mt-md" color="primary" :to="{ name:'admin-posts' }" label="Gestisci Post" />
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
101
src/pages/admin/AdminEvents.vue
Normal file
101
src/pages/admin/AdminEvents.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<div class="row items-center justify-between q-mb-md">
|
||||
<div class="text-h6">Eventi</div>
|
||||
<q-btn color="primary" icon="add" label="Nuovo Evento" @click="openCreate" />
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
row-key="_id"
|
||||
flat bordered
|
||||
:loading="loading"
|
||||
rows-per-page-label="Righe per pagina"
|
||||
>
|
||||
<template #body-cell-actions="props">
|
||||
<q-td :props="props">
|
||||
<q-btn dense flat icon="edit" @click="openEdit(props.row)" />
|
||||
<q-btn dense flat icon="delete" color="negative" @click="remove(props.row)" />
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-dialog v-model="dlg.open" persistent>
|
||||
<q-card style="min-width: 600px; max-width: 90vw;">
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div class="text-subtitle1">{{ dlg.mode === 'create' ? 'Nuovo Evento' : 'Modifica Evento' }}</div>
|
||||
<q-btn dense flat round icon="close" v-close-popup />
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-12 col-md-6"><q-input v-model="model.title" label="Titolo" dense /></div>
|
||||
<div class="col-12 col-md-6"><q-input v-model="model.place" label="Luogo" dense /></div>
|
||||
<div class="col-12 col-md-6"><q-input v-model="model.start" label="Inizio (ISO)" dense /></div>
|
||||
<div class="col-12 col-md-6"><q-input v-model="model.end" label="Fine (ISO)" dense /></div>
|
||||
<div class="col-12 col-md-12"><q-input v-model="model.teaser" label="Teaser" type="textarea" autogrow dense /></div>
|
||||
<div class="col-12 col-md-8"><q-input v-model="model.cover" label="Cover URL" dense /></div>
|
||||
<div class="col-12 col-md-4"><q-input v-model="model.to" label="to" dense /></div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Annulla" v-close-popup />
|
||||
<q-btn color="primary" :loading="saving" @click="save">Salva</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// @ts-check
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useAdminStore } from 'src/stores/admin.store';
|
||||
|
||||
const $q = useQuasar();
|
||||
const store = useAdminStore();
|
||||
|
||||
const loading = computed(() => store.loadingEvents);
|
||||
const saving = computed(() => store.saving);
|
||||
const rows = computed(() => store.events);
|
||||
|
||||
const columns = [
|
||||
{ name: 'title', label: 'Titolo', field: 'title', align: 'left' },
|
||||
{ name: 'start', label: 'Inizio', field: r => r.start?.slice?.(0, 16), align: 'left' },
|
||||
{ name: 'place', label: 'Luogo', field: 'place', align: 'left' },
|
||||
{ name: 'actions', label: 'Azioni', field: 'actions', align: 'right' }
|
||||
];
|
||||
|
||||
const dlg = ref({ open: false, mode: 'create' });
|
||||
const model = ref({ title:'', start:'', end:'', place:'', teaser:'', cover:'', to:'' });
|
||||
|
||||
onMounted(() => store.loadEvents({ limit: 100, sort: 'start' }));
|
||||
|
||||
function openCreate() {
|
||||
dlg.value = { open: true, mode: 'create' };
|
||||
model.value = { title:'', start:'', end:'', place:'', teaser:'', cover:'', to:'' };
|
||||
}
|
||||
function openEdit(row) {
|
||||
dlg.value = { open: true, mode: 'edit' };
|
||||
model.value = { ...row, start: row.start?.slice?.(0, 19), end: row.end?.slice?.(0, 19) };
|
||||
}
|
||||
async function save() {
|
||||
try {
|
||||
if (dlg.value.mode === 'create') await store.createEvent(model.value);
|
||||
else await store.updateEvent(model.value._id, model.value);
|
||||
dlg.value.open = false;
|
||||
$q.notify({ type:'positive', message:'Salvato' });
|
||||
} catch (e) {
|
||||
$q.notify({ type:'negative', message: e?.message || 'Errore salvataggio' });
|
||||
}
|
||||
}
|
||||
async function remove(row) {
|
||||
$q.dialog({ title:'Conferma', message:`Eliminare "${row.title}"?`, cancel:true }).onOk(async () => {
|
||||
await store.deleteEvent(row._id);
|
||||
$q.notify({ type:'positive', message:'Eliminato' });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
127
src/pages/admin/AdminHomeEditor.vue
Normal file
127
src/pages/admin/AdminHomeEditor.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<q-card flat bordered class="q-pa-md">
|
||||
<div class="row items-center justify-between">
|
||||
<div class="text-h6">Editor Home</div>
|
||||
<q-btn color="primary" :loading="saving" @click="onSave" label="Salva" />
|
||||
</div>
|
||||
|
||||
<q-separator class="q-my-md" />
|
||||
|
||||
<!-- HERO -->
|
||||
<div class="text-subtitle1 q-mb-sm">Hero</div>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-12 col-md-6"><q-input v-model="form.hero.title" label="Titolo (H1)" dense /></div>
|
||||
<div class="col-12 col-md-6"><q-input v-model="form.hero.subtitle" label="Sottotitolo" dense /></div>
|
||||
<div class="col-12 col-md-6"><q-input v-model="form.hero.badge" label="Badge" dense /></div>
|
||||
<div class="col-12 col-md-6"><q-input v-model="form.hero.mediaUrl" label="Immagine (URL)" dense /></div>
|
||||
</div>
|
||||
<div class="q-mt-sm">
|
||||
<div class="text-caption text-grey-7 q-mb-xs">CTAs</div>
|
||||
<div v-for="(c,i) in form.hero.ctas" :key="'cta-'+i" class="row q-col-gutter-sm q-mb-xs">
|
||||
<div class="col-12 col-md-4"><q-input v-model="c.label" label="Label" dense /></div>
|
||||
<div class="col-12 col-md-4"><q-input v-model="c.to" label="to (router)" dense /></div>
|
||||
<div class="col-12 col-md-3"><q-input v-model="c.href" label="href (link esterno)" dense /></div>
|
||||
<div class="col-12 col-md-1 flex items-center">
|
||||
<q-btn dense round icon="delete" color="negative" flat @click="form.hero.ctas.splice(i,1)" />
|
||||
</div>
|
||||
</div>
|
||||
<q-btn flat icon="add" color="primary" label="Aggiungi CTA" @click="form.hero.ctas.push({ label:'', to:'', href:'' })" />
|
||||
</div>
|
||||
|
||||
<q-separator class="q-my-lg" />
|
||||
|
||||
<!-- PILLARS -->
|
||||
<div class="row items-center justify-between">
|
||||
<div class="text-subtitle1">Pillars</div>
|
||||
<q-btn flat icon="add" color="primary" label="Aggiungi" @click="addPillar" />
|
||||
</div>
|
||||
<q-list bordered separator class="q-mt-sm">
|
||||
<q-item v-for="(p,i) in form.pillars" :key="p.id" clickable>
|
||||
<q-item-section avatar><q-icon :name="p.icon || 'category'" /></q-item-section>
|
||||
<q-item-section>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-12 col-md-2"><q-input v-model="p.id" label="ID" dense /></div>
|
||||
<div class="col-12 col-md-2"><q-input v-model="p.icon" label="Icona (Material)" dense /></div>
|
||||
<div class="col-12 col-md-3"><q-input v-model="p.title" label="Titolo" dense /></div>
|
||||
<div class="col-12 col-md-4"><q-input v-model="p.excerpt" label="Descrizione breve" dense /></div>
|
||||
<div class="col-12 col-md-1"><q-input v-model="p.to" label="to" dense /></div>
|
||||
</div>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-btn dense round flat icon="delete" color="negative" @click.stop="form.pillars.splice(i,1)" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<q-separator class="q-my-lg" />
|
||||
|
||||
<!-- FAQ -->
|
||||
<div class="row items-center justify-between">
|
||||
<div class="text-subtitle1">FAQ</div>
|
||||
<q-btn flat icon="add" color="primary" label="Aggiungi" @click="addFaq" />
|
||||
</div>
|
||||
<q-list bordered separator class="q-mt-sm">
|
||||
<q-expansion-item v-for="(f,i) in form.faq" :key="'faq-'+i" :label="f.q || ('FAQ #' + (i+1))" dense expand-separator>
|
||||
<div class="row q-col-gutter-sm q-pa-sm">
|
||||
<div class="col-12 col-md-6"><q-input v-model="f.q" label="Domanda" dense /></div>
|
||||
<div class="col-12 col-md-6"><q-input v-model="f.a" label="Risposta" dense type="textarea" autogrow /></div>
|
||||
<div class="col-12">
|
||||
<q-btn dense round flat icon="delete" color="negative" label="Rimuovi" @click="form.faq.splice(i,1)" />
|
||||
</div>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// @ts-check
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useAdminStore } from 'src/stores/admin.store';
|
||||
|
||||
const $q = useQuasar();
|
||||
const store = useAdminStore();
|
||||
|
||||
const saving = computed(() => store.savingHome);
|
||||
const form = ref({
|
||||
hero: { title:'', subtitle:'', badge:'', mediaUrl:'', ctas: [] },
|
||||
pillars: [],
|
||||
faq: []
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await store.loadHome();
|
||||
if (store.home) {
|
||||
// clona per editing
|
||||
form.value = JSON.parse(JSON.stringify({
|
||||
hero: store.home.hero || form.value.hero,
|
||||
pillars: store.home.pillars || [],
|
||||
faq: store.home.faq || []
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
function addPillar() {
|
||||
form.value.pillars.push({ id:'', icon:'category', title:'', excerpt:'', to:'' });
|
||||
}
|
||||
function addFaq() {
|
||||
form.value.faq.push({ q:'', a:'' });
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
try {
|
||||
await store.saveHome({
|
||||
...store.home,
|
||||
hero: form.value.hero,
|
||||
pillars: form.value.pillars,
|
||||
faq: form.value.faq
|
||||
});
|
||||
$q.notify({ type:'positive', message:'Home salvata' });
|
||||
} catch (e) {
|
||||
$q.notify({ type:'negative', message: e?.message || 'Errore salvataggio' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
19
src/pages/admin/AdminLayout.vue
Normal file
19
src/pages/admin/AdminLayout.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<q-layout view="hHh Lpr fFf">
|
||||
<q-header elevated>
|
||||
<q-toolbar>
|
||||
<q-btn flat dense round icon="admin_panel_settings" />
|
||||
<q-toolbar-title>Admin CMS</q-toolbar-title>
|
||||
<q-space />
|
||||
<q-btn flat :to="{ name:'admin-dashboard' }" label="Dashboard" />
|
||||
<q-btn flat :to="{ name:'admin-home' }" label="Home" />
|
||||
<q-btn flat :to="{ name:'admin-events' }" label="Eventi" />
|
||||
<q-btn flat :to="{ name:'admin-posts' }" label="Post" />
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
35
src/router/routes-admin.js
Normal file
35
src/router/routes-admin.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// @ts-check
|
||||
export default [
|
||||
{
|
||||
path: '/admin',
|
||||
component: () => import('src/pages/admin/AdminLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
active: true,
|
||||
path: '',
|
||||
name: 'admin-dashboard',
|
||||
component: () => import('src/pages/admin/AdminDashboard.vue'),
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
path: 'home',
|
||||
name: 'admin-home',
|
||||
component: () => import('src/pages/admin/AdminHomeEditor.vue'),
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
path: 'events',
|
||||
name: 'admin-events',
|
||||
component: () => import('src/pages/admin/AdminEvents.vue'),
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
path: 'posts',
|
||||
name: 'admin-posts',
|
||||
component: () => import('src/pages/admin/AdminPosts.vue'),
|
||||
},
|
||||
],
|
||||
// Semplice guard opzionale (sostituisci con la tua auth reale)
|
||||
meta: { requiresAuth: false },
|
||||
},
|
||||
];
|
||||
117
src/store/admin.store.js
Normal file
117
src/store/admin.store.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// @ts-check
|
||||
import { defineStore } from 'pinia';
|
||||
import api from 'src/services/api';
|
||||
|
||||
export const useAdminStore = defineStore('admin', {
|
||||
state: () => ({
|
||||
home: null,
|
||||
loadingHome: false,
|
||||
savingHome: false,
|
||||
events: [],
|
||||
posts: [],
|
||||
loadingEvents: false,
|
||||
loadingPosts: false,
|
||||
saving: false,
|
||||
error: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async loadHome() {
|
||||
this.loadingHome = true; this.error = null;
|
||||
try {
|
||||
const { data } = await api.get('/home');
|
||||
this.home = data;
|
||||
} catch (e) {
|
||||
this.error = e?.message || 'Errore caricamento Home';
|
||||
} finally {
|
||||
this.loadingHome = false;
|
||||
}
|
||||
},
|
||||
async saveHome(partial) {
|
||||
this.savingHome = true; this.error = null;
|
||||
try {
|
||||
const payload = { ...(this.home || {}), ...(partial || {}) };
|
||||
const { data } = await api.put('/home', payload);
|
||||
this.home = data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
this.error = e?.message || 'Errore salvataggio Home';
|
||||
throw e;
|
||||
} finally {
|
||||
this.savingHome = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadEvents(params = { limit: 50 }) {
|
||||
this.loadingEvents = true; this.error = null;
|
||||
try {
|
||||
const { data } = await api.get('/events', { params });
|
||||
this.events = Array.isArray(data?.items) ? data.items : data; // supporta entrambi i formati
|
||||
} catch (e) {
|
||||
this.error = e?.message || 'Errore caricamento Eventi';
|
||||
} finally {
|
||||
this.loadingEvents = false;
|
||||
}
|
||||
},
|
||||
async createEvent(item) {
|
||||
this.saving = true;
|
||||
try {
|
||||
const { data } = await api.post('/events', item);
|
||||
this.events.unshift(data);
|
||||
return data;
|
||||
} finally { this.saving = false; }
|
||||
},
|
||||
async updateEvent(id, partial) {
|
||||
this.saving = true;
|
||||
try {
|
||||
const { data } = await api.put(`/events/${id}`, partial);
|
||||
const i = this.events.findIndex(e => e._id === id);
|
||||
if (i >= 0) this.events[i] = data;
|
||||
return data;
|
||||
} finally { this.saving = false; }
|
||||
},
|
||||
async deleteEvent(id) {
|
||||
this.saving = true;
|
||||
try {
|
||||
await api.delete(`/events/${id}`);
|
||||
this.events = this.events.filter(e => e._id !== id);
|
||||
} finally { this.saving = false; }
|
||||
},
|
||||
|
||||
async loadPosts(params = { limit: 50, sort: '-date' }) {
|
||||
this.loadingPosts = true; this.error = null;
|
||||
try {
|
||||
const { data } = await api.get('/posts', { params });
|
||||
this.posts = Array.isArray(data?.items) ? data.items : data;
|
||||
} catch (e) {
|
||||
this.error = e?.message || 'Errore caricamento Post';
|
||||
} finally {
|
||||
this.loadingPosts = false;
|
||||
}
|
||||
},
|
||||
async createPost(item) {
|
||||
this.saving = true;
|
||||
try {
|
||||
const { data } = await api.post('/posts', item);
|
||||
this.posts.unshift(data);
|
||||
return data;
|
||||
} finally { this.saving = false; }
|
||||
},
|
||||
async updatePost(id, partial) {
|
||||
this.saving = true;
|
||||
try {
|
||||
const { data } = await api.put(`/posts/${id}`, partial);
|
||||
const i = this.posts.findIndex(p => p._id === id);
|
||||
if (i >= 0) this.posts[i] = data;
|
||||
return data;
|
||||
} finally { this.saving = false; }
|
||||
},
|
||||
async deletePost(id) {
|
||||
this.saving = true;
|
||||
try {
|
||||
await api.delete(`/posts/${id}`);
|
||||
this.posts = this.posts.filter(p => p._id !== id);
|
||||
} finally { this.saving = false; }
|
||||
}
|
||||
}
|
||||
});
|
||||
130
src/stores/home.store.ts
Normal file
130
src/stores/home.store.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import api from 'src/services/api';
|
||||
import type { HomeCMS, EventItem, PostItem } from 'src/types/home';
|
||||
|
||||
type State = {
|
||||
data: HomeCMS | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// sottostati per sezioni dinamiche
|
||||
loadingEvents: boolean;
|
||||
errorEvents: string | null;
|
||||
|
||||
loadingPosts: boolean;
|
||||
errorPosts: string | null;
|
||||
|
||||
_cacheAt?: number;
|
||||
_eventsAt?: number;
|
||||
_postsAt?: number;
|
||||
};
|
||||
|
||||
const FIVE_MIN = 5 * 60 * 1000;
|
||||
|
||||
export const useHomeStore = defineStore('home', {
|
||||
state: (): State => ({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
loadingEvents: false,
|
||||
errorEvents: null,
|
||||
|
||||
loadingPosts: false,
|
||||
errorPosts: null,
|
||||
|
||||
_cacheAt: undefined,
|
||||
_eventsAt: undefined,
|
||||
_postsAt: undefined
|
||||
}),
|
||||
|
||||
getters: {
|
||||
nextEvents(state): EventItem[] {
|
||||
const now = new Date();
|
||||
const list = (state.data?.events || []).filter(e => new Date(e.start) >= now);
|
||||
list.sort((a, b) => +new Date(a.start) - +new Date(b.start));
|
||||
return list.slice(0, 4);
|
||||
},
|
||||
latestPosts(state): PostItem[] {
|
||||
const list = (state.data?.posts || []).slice().sort((a, b) => +new Date(b.date) - +new Date(a.date));
|
||||
return list.slice(0, 3);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchHome(prefetched?: HomeCMS) {
|
||||
if (prefetched) {
|
||||
this.data = prefetched;
|
||||
this._cacheAt = Date.now();
|
||||
return;
|
||||
}
|
||||
const fresh = !this._cacheAt || (Date.now() - this._cacheAt) > FIVE_MIN;
|
||||
if (!fresh && this.data) return;
|
||||
|
||||
this.loading = true; this.error = null;
|
||||
try {
|
||||
const { data } = await api.get<HomeCMS>('/home');
|
||||
this.data = data;
|
||||
this._cacheAt = Date.now();
|
||||
} catch (e: any) {
|
||||
this.error = e?.message || 'Impossibile caricare la home, uso mock locale.';
|
||||
// Fallback ai mock
|
||||
const mock = await import('src/mocks/home.sample.json');
|
||||
this.data = mock.default as HomeCMS;
|
||||
this._cacheAt = Date.now();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchEvents() {
|
||||
const fresh = !this._eventsAt || (Date.now() - this._eventsAt) > FIVE_MIN;
|
||||
if (!fresh && (this.data?.events?.length || 0) > 0) return;
|
||||
|
||||
this.loadingEvents = true; this.errorEvents = null;
|
||||
try {
|
||||
const { data } = await api.get<EventItem[]>('/events', { params: { limit: 6 } });
|
||||
this.data = this.data || ({} as HomeCMS);
|
||||
this.data.events = data;
|
||||
this._eventsAt = Date.now();
|
||||
} catch (e: any) {
|
||||
this.errorEvents = e?.message || 'Eventi non disponibili (mock)';
|
||||
if (!this.data?.events?.length) {
|
||||
const mock = await import('src/mocks/home.sample.json');
|
||||
this.data = this.data || ({} as HomeCMS);
|
||||
this.data.events = (mock.default as HomeCMS).events;
|
||||
}
|
||||
} finally {
|
||||
this.loadingEvents = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchPosts() {
|
||||
const fresh = !this._postsAt || (Date.now() - this._postsAt) > FIVE_MIN;
|
||||
if (!fresh && (this.data?.posts?.length || 0) > 0) return;
|
||||
|
||||
this.loadingPosts = true; this.errorPosts = null;
|
||||
try {
|
||||
const { data } = await api.get<PostItem[]>('/posts', { params: { limit: 3 } });
|
||||
this.data = this.data || ({} as HomeCMS);
|
||||
this.data.posts = data;
|
||||
this._postsAt = Date.now();
|
||||
} catch (e: any) {
|
||||
this.errorPosts = e?.message || 'Post non disponibili (mock)';
|
||||
if (!this.data?.posts?.length) {
|
||||
const mock = await import('src/mocks/home.sample.json');
|
||||
this.data = this.data || ({} as HomeCMS);
|
||||
this.data.posts = (mock.default as HomeCMS).posts;
|
||||
}
|
||||
} finally {
|
||||
this.loadingPosts = false;
|
||||
}
|
||||
},
|
||||
|
||||
async subscribeNewsletter(email: string) {
|
||||
if (!/.+@.+\..+/.test(email)) throw new Error('Email non valida');
|
||||
await api.post('/newsletter/subscribe', { email });
|
||||
// niente stato locale: demandiamo a backend
|
||||
}
|
||||
}
|
||||
});
|
||||
25
src/types/home.ts
Normal file
25
src/types/home.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export interface HeroBlock {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
badge?: string;
|
||||
mediaUrl: string;
|
||||
ctas: { label: string; to?: string; href?: string }[];
|
||||
}
|
||||
export interface Pillar { id: string; icon: string; title: string; excerpt: string; to?: string }
|
||||
export interface EventItem { id: string; title: string; start: string; end?: string; place?: string; teaser?: string; cover?: string; to?: string }
|
||||
export interface Testimonial { id: string; quote: string; author: string; role?: string; avatar?: string }
|
||||
export interface GalleryItem { id: string; src: string; alt?: string }
|
||||
export interface FaqItem { q: string; a: string }
|
||||
export interface PostItem { id: string; title: string; date: string; category?: string; teaser?: string; cover?: string; to?: string }
|
||||
export interface Partner { id: string; name: string; logo: string; href?: string }
|
||||
|
||||
export interface HomeCMS {
|
||||
hero: HeroBlock;
|
||||
pillars: Pillar[];
|
||||
events: EventItem[];
|
||||
testimonials: Testimonial[];
|
||||
gallery: GalleryItem[];
|
||||
faq: FaqItem[];
|
||||
posts: PostItem[];
|
||||
partners: Partner[];
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { LandingFooter } from '@src/components/LandingFooter';
|
||||
import { useUserStore } from '@store/UserStore';
|
||||
import MixinUsers from '@src/mixins/mixin-users';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { tools } from '@tools';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Regok',
|
||||
@@ -15,6 +16,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
t,
|
||||
tools,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user