Compare commits
4 Commits
89a8d10eae
...
product_in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1a66ef4ea | ||
|
|
7c1946debe | ||
|
|
7aeced4232 | ||
|
|
6d78f82099 |
28
migrate-repos.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Salva come migrate-repos.sh
|
||||||
|
|
||||||
|
GITEA_URL="http://95.216.147.38:3000"
|
||||||
|
USERNAME="surya"
|
||||||
|
TOKEN="8ed0622aac269414f4d333d0c89e22b1c42dd4d1" # Crea su Gitea: Settings → Applications → Generate Token
|
||||||
|
SEARCH_PATH="$HOME/myproject"
|
||||||
|
|
||||||
|
# Trova tutti i repository
|
||||||
|
find "$SEARCH_PATH" -name ".git" -type d 2>/dev/null | while read gitdir; do
|
||||||
|
REPO_PATH=$(dirname "$gitdir")
|
||||||
|
REPO_NAME=$(basename "$REPO_PATH")
|
||||||
|
|
||||||
|
echo "Processing: $REPO_NAME"
|
||||||
|
|
||||||
|
# Crea repository su Gitea via API
|
||||||
|
curl -X POST "$GITEA_URL/api/v1/user/repos" \
|
||||||
|
-H "Authorization: token $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"name\":\"$REPO_NAME\",\"private\":false}"
|
||||||
|
|
||||||
|
# Push
|
||||||
|
cd "$REPO_PATH"
|
||||||
|
git remote remove origin 2>/dev/null
|
||||||
|
git remote add origin "$GITEA_URL/$USERNAME/$REPO_NAME.git"
|
||||||
|
git push -u origin --all
|
||||||
|
git push -u origin --tags
|
||||||
|
done
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 424 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
BIN
public/images/riso_quadrato.jpg
Normal file
|
After Width: | Height: | Size: 140 KiB |
@@ -35,7 +35,6 @@ export default defineConfig((ctx) => {
|
|||||||
node: 'node20',
|
node: 'node20',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ✅ AGGIUNTO: Importa automaticamente variables.scss ovunque
|
|
||||||
sassVariables: 'src/css/variables.scss',
|
sassVariables: 'src/css/variables.scss',
|
||||||
|
|
||||||
vueRouterMode: 'history',
|
vueRouterMode: 'history',
|
||||||
|
|||||||
@@ -147,7 +147,6 @@ export const shared_consts = {
|
|||||||
DASHBOARD: 140,
|
DASHBOARD: 140,
|
||||||
DASHGROUP: 145,
|
DASHGROUP: 145,
|
||||||
MOVEMENTS: 148,
|
MOVEMENTS: 148,
|
||||||
CSENDRISTO: 150,
|
|
||||||
STATUSREG: 160,
|
STATUSREG: 160,
|
||||||
CHECKIFISLOGGED: 170,
|
CHECKIFISLOGGED: 170,
|
||||||
INFO_VERSION: 180,
|
INFO_VERSION: 180,
|
||||||
@@ -191,8 +190,10 @@ export const shared_consts = {
|
|||||||
PROFILE_COMPLETITION: 1510,
|
PROFILE_COMPLETITION: 1510,
|
||||||
RISOHOME: 1600,
|
RISOHOME: 1600,
|
||||||
RISOHOME_MODERN: 1610,
|
RISOHOME_MODERN: 1610,
|
||||||
|
RISOHOME_PAGFINALE: 1615,
|
||||||
PAGERIS: 1620,
|
PAGERIS: 1620,
|
||||||
CMYCIRCUITS: 1630,
|
CMYCIRCUITS: 1630,
|
||||||
|
CREA_VOLANTINO: 1700,
|
||||||
},
|
},
|
||||||
|
|
||||||
QUERYTYPE_MYGROUP: 1,
|
QUERYTYPE_MYGROUP: 1,
|
||||||
@@ -2009,11 +2010,6 @@ export const shared_consts = {
|
|||||||
label: 'Lista Movimenti',
|
label: 'Lista Movimenti',
|
||||||
icon: 'fas fa-list',
|
icon: 'fas fa-list',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: 150, // CSENDRISTO
|
|
||||||
label: 'Bott (Invia/Ricevi RIS)',
|
|
||||||
icon: 'fas fa-wallet',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: 280,
|
value: 280,
|
||||||
label: 'Tutorial',
|
label: 'Tutorial',
|
||||||
@@ -2057,6 +2053,11 @@ export const shared_consts = {
|
|||||||
label: 'Check Email',
|
label: 'Check Email',
|
||||||
icon: 'fas fa-envelope',
|
icon: 'fas fa-envelope',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 1700, // CREA_VOLANTINO
|
||||||
|
label: 'Genera Volantini',
|
||||||
|
icon: 'fas fa-user-tie',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 120,
|
value: 120,
|
||||||
label: 'OpenStreetMap',
|
label: 'OpenStreetMap',
|
||||||
@@ -2122,6 +2123,11 @@ export const shared_consts = {
|
|||||||
label: 'RISO Home Modern',
|
label: 'RISO Home Modern',
|
||||||
icon: 'fas fa-home',
|
icon: 'fas fa-home',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 1615, // RISOHOME_PAGFINALE
|
||||||
|
label: 'RISO Home (Parte Finale))',
|
||||||
|
icon: 'fas fa-home',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 1620, // PAGERIS
|
value: 1620, // PAGERIS
|
||||||
label: 'Pagina RIS',
|
label: 'Pagina RIS',
|
||||||
@@ -2481,6 +2487,7 @@ export const shared_consts = {
|
|||||||
link_group: 1,
|
link_group: 1,
|
||||||
totCircolante: 1,
|
totCircolante: 1,
|
||||||
totTransato: 1,
|
totTransato: 1,
|
||||||
|
numTransazioni: 1,
|
||||||
systemUserId: 1,
|
systemUserId: 1,
|
||||||
createdBy: 1,
|
createdBy: 1,
|
||||||
date_created: 1,
|
date_created: 1,
|
||||||
|
|||||||
749
src/components/AIImageGenerator/AIImageGenerator.vue
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
<template>
|
||||||
|
<q-card class="ai-generator-dialog">
|
||||||
|
<!-- Header -->
|
||||||
|
<q-card-section class="dialog-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<q-icon name="auto_awesome" size="32px" color="amber" />
|
||||||
|
<div>
|
||||||
|
<h2>Genera Immagine con AI</h2>
|
||||||
|
<p>{{ assetTypeLabel }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-btn flat round icon="close" @click="$emit('close')" />
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<q-card-section class="dialog-body">
|
||||||
|
<div class="generator-layout">
|
||||||
|
<!-- Left: Form -->
|
||||||
|
<div class="form-panel">
|
||||||
|
<!-- Provider Selection -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">Provider AI</label>
|
||||||
|
<div class="provider-options">
|
||||||
|
<div
|
||||||
|
v-for="provider in providers"
|
||||||
|
:key="provider.value"
|
||||||
|
class="provider-option"
|
||||||
|
:class="{ 'is-selected': selectedProvider === provider.value }"
|
||||||
|
@click="selectedProvider = provider.value"
|
||||||
|
>
|
||||||
|
<q-icon :name="provider.icon" size="24px" />
|
||||||
|
<div class="provider-info">
|
||||||
|
<span class="provider-name">{{ provider.label }}</span>
|
||||||
|
<q-badge
|
||||||
|
v-if="provider.free"
|
||||||
|
color="green"
|
||||||
|
text-color="white"
|
||||||
|
label="Gratis"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prompt -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">
|
||||||
|
Descrivi l'immagine
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
icon="help_outline"
|
||||||
|
@click="showPromptTips = true"
|
||||||
|
>
|
||||||
|
<q-tooltip>Suggerimenti per prompt efficaci</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</label>
|
||||||
|
<q-input
|
||||||
|
v-model="prompt"
|
||||||
|
filled
|
||||||
|
type="textarea"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Descrivi l'immagine che vuoi generare..."
|
||||||
|
counter
|
||||||
|
maxlength="1000"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Quick prompts -->
|
||||||
|
<div class="quick-prompts">
|
||||||
|
<span class="quick-label">Suggerimenti rapidi:</span>
|
||||||
|
<q-chip
|
||||||
|
v-for="(suggestion, idx) in promptSuggestions"
|
||||||
|
:key="idx"
|
||||||
|
clickable
|
||||||
|
size="sm"
|
||||||
|
@click="appendToPrompt(suggestion)"
|
||||||
|
>
|
||||||
|
{{ suggestion }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Negative Prompt -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">
|
||||||
|
Prompt Negativo
|
||||||
|
<span class="optional">(opzionale)</span>
|
||||||
|
</label>
|
||||||
|
<q-input
|
||||||
|
v-model="negativePrompt"
|
||||||
|
filled
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Cosa NON vuoi vedere nell'immagine..."
|
||||||
|
/>
|
||||||
|
<div class="negative-presets">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
label="Usa preset standard"
|
||||||
|
@click="useStandardNegative"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aspect Ratio -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">Formato</label>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="aspectRatio"
|
||||||
|
:options="aspectRatioOptions"
|
||||||
|
spread
|
||||||
|
no-caps
|
||||||
|
toggle-color="primary"
|
||||||
|
class="full-width"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Options -->
|
||||||
|
<q-expansion-item
|
||||||
|
icon="tune"
|
||||||
|
label="Opzioni Avanzate"
|
||||||
|
header-class="advanced-header"
|
||||||
|
>
|
||||||
|
<div class="advanced-options">
|
||||||
|
<div class="options-row">
|
||||||
|
<q-input
|
||||||
|
v-model.number="seed"
|
||||||
|
type="number"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="Seed"
|
||||||
|
hint="Lascia vuoto per casuale"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model.number="steps"
|
||||||
|
type="number"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="Steps"
|
||||||
|
:min="10"
|
||||||
|
:max="50"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model.number="cfg"
|
||||||
|
type="number"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="CFG Scale"
|
||||||
|
:min="1"
|
||||||
|
:max="20"
|
||||||
|
step="0.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<!-- Generate Button -->
|
||||||
|
<div class="generate-actions">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="auto_awesome"
|
||||||
|
label="Genera Immagine"
|
||||||
|
size="lg"
|
||||||
|
:loading="isGenerating"
|
||||||
|
:disable="!prompt.trim()"
|
||||||
|
class="full-width"
|
||||||
|
@click="generateImage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Preview -->
|
||||||
|
<div class="preview-panel">
|
||||||
|
<div class="preview-container" :class="{ 'has-image': !!generatedImage }">
|
||||||
|
<template v-if="isGenerating">
|
||||||
|
<div class="generating-state">
|
||||||
|
<q-spinner-orbit size="80px" color="primary" />
|
||||||
|
<p>Generazione in corso...</p>
|
||||||
|
<span class="time-estimate">Tempo stimato: ~15-30 secondi</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="generatedImage">
|
||||||
|
<img :src="generatedImage" alt="Generated image" class="preview-image" />
|
||||||
|
<div class="preview-actions">
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
color="white"
|
||||||
|
text-color="primary"
|
||||||
|
icon="refresh"
|
||||||
|
@click="generateImage"
|
||||||
|
>
|
||||||
|
<q-tooltip>Rigenera</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
color="green"
|
||||||
|
icon="check"
|
||||||
|
@click="confirmImage"
|
||||||
|
>
|
||||||
|
<q-tooltip>Usa questa immagine</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="empty-preview">
|
||||||
|
<q-icon name="image" size="80px" color="grey-4" />
|
||||||
|
<p>L'immagine generata apparirà qui</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generation History -->
|
||||||
|
<div v-if="history.length > 0" class="generation-history">
|
||||||
|
<h4>Generazioni recenti</h4>
|
||||||
|
<div class="history-grid">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in history"
|
||||||
|
:key="idx"
|
||||||
|
class="history-item"
|
||||||
|
@click="selectFromHistory(item)"
|
||||||
|
>
|
||||||
|
<img :src="item.url" :alt="`Generation ${idx + 1}`" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- Prompt Tips Dialog -->
|
||||||
|
<q-dialog v-model="showPromptTips">
|
||||||
|
<q-card style="max-width: 500px">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">💡 Suggerimenti per Prompt Efficaci</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<ul class="tips-list">
|
||||||
|
<li><strong>Sii specifico:</strong> Descrivi dettagli come colori, stile, atmosfera</li>
|
||||||
|
<li><strong>Indica lo stile:</strong> "fotorealistico", "illustrazione", "acquerello"</li>
|
||||||
|
<li><strong>Specifica la qualità:</strong> "high quality", "4k", "detailed"</li>
|
||||||
|
<li><strong>Evita il testo:</strong> Aggiungi sempre "no text, no letters"</li>
|
||||||
|
<li><strong>Composizione:</strong> Indica "central composition", "clean layout"</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="example-prompt q-mt-md">
|
||||||
|
<strong>Esempio:</strong>
|
||||||
|
<p class="q-mt-sm">"Mystical autumn forest at golden hour, morning mist between oak trees, photorealistic, cinematic lighting, warm colors, high quality, no text, no letters"</p>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="Capito!" color="primary" v-close-popup />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { Api } from 'src/store/Api';
|
||||||
|
import { PROVIDER_OPTIONS, ASPECT_RATIO_OPTIONS } from '../../types/poster.types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
assetType: 'backgroundImage' | 'mainImage';
|
||||||
|
template: any;
|
||||||
|
initialPrompt?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'generated', result: { url: string; aiParams: any }): void;
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const selectedProvider = ref('hf');
|
||||||
|
const prompt = ref('');
|
||||||
|
const negativePrompt = ref('');
|
||||||
|
const aspectRatio = ref('9:16');
|
||||||
|
const seed = ref<number | null>(null);
|
||||||
|
const steps = ref(28);
|
||||||
|
const cfg = ref(7.5);
|
||||||
|
const isGenerating = ref(false);
|
||||||
|
const generatedImage = ref<string | null>(null);
|
||||||
|
const history = ref<{ url: string; prompt: string }[]>([]);
|
||||||
|
const showPromptTips = ref(false);
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const assetTypeLabel = computed(() => {
|
||||||
|
return props.assetType === 'backgroundImage' ? 'Immagine di sfondo' : 'Immagine principale';
|
||||||
|
});
|
||||||
|
|
||||||
|
const providers = computed(() => PROVIDER_OPTIONS);
|
||||||
|
|
||||||
|
const aspectRatioOptions = computed(() =>
|
||||||
|
ASPECT_RATIO_OPTIONS.map(opt => ({
|
||||||
|
label: opt.label,
|
||||||
|
value: opt.value
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const promptSuggestions = computed(() => {
|
||||||
|
const suggestions = [
|
||||||
|
'high quality, 4k',
|
||||||
|
'cinematic lighting',
|
||||||
|
'no text, no letters',
|
||||||
|
'photorealistic',
|
||||||
|
'warm colors',
|
||||||
|
'dramatic atmosphere'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add template-specific suggestions
|
||||||
|
if (props.template?.defaultAiPromptHints?.[props.assetType]) {
|
||||||
|
const hint = props.template.defaultAiPromptHints[props.assetType];
|
||||||
|
const words = hint.split(',').slice(0, 3).map((w: string) => w.trim());
|
||||||
|
return [...words, ...suggestions.slice(0, 3)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const appendToPrompt = (text: string) => {
|
||||||
|
if (prompt.value) {
|
||||||
|
prompt.value += ', ' + text;
|
||||||
|
} else {
|
||||||
|
prompt.value = text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStandardNegative = () => {
|
||||||
|
negativePrompt.value = 'text, letters, words, watermark, signature, blurry, low quality, distorted, ugly, bad anatomy, disfigured';
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateImage = async () => {
|
||||||
|
if (!prompt.value.trim()) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Inserisci una descrizione per l\'immagine'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isGenerating.value = true;
|
||||||
|
generatedImage.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await Api.SendReq('/api/assets/generate-ai', 'POST', {
|
||||||
|
prompt: prompt.value,
|
||||||
|
negativePrompt: negativePrompt.value,
|
||||||
|
provider: selectedProvider.value,
|
||||||
|
aspectRatio: aspectRatio.value,
|
||||||
|
category: props.assetType === 'backgroundImage' ? 'background' : 'main',
|
||||||
|
seed: seed.value,
|
||||||
|
steps: steps.value,
|
||||||
|
cfg: cfg.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.data?.success) {
|
||||||
|
generatedImage.value = res.data.data.file.url;
|
||||||
|
|
||||||
|
// Add to history
|
||||||
|
history.value.unshift({
|
||||||
|
url: res.data.data.file.url,
|
||||||
|
prompt: prompt.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 6
|
||||||
|
if (history.value.length > 6) {
|
||||||
|
history.value = history.value.slice(0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Immagine generata con successo!',
|
||||||
|
icon: 'auto_awesome'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(res?.data?.error || 'Errore durante la generazione');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.message || 'Errore durante la generazione',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isGenerating.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmImage = () => {
|
||||||
|
if (!generatedImage.value) return;
|
||||||
|
|
||||||
|
emit('generated', {
|
||||||
|
url: generatedImage.value,
|
||||||
|
aiParams: {
|
||||||
|
prompt: prompt.value,
|
||||||
|
negativePrompt: negativePrompt.value,
|
||||||
|
provider: selectedProvider.value,
|
||||||
|
aspectRatio: aspectRatio.value,
|
||||||
|
seed: seed.value,
|
||||||
|
steps: steps.value,
|
||||||
|
cfg: cfg.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectFromHistory = (item: { url: string; prompt: string }) => {
|
||||||
|
generatedImage.value = item.url;
|
||||||
|
prompt.value = item.prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize with hint
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.initialPrompt) {
|
||||||
|
prompt.value = props.initialPrompt;
|
||||||
|
}
|
||||||
|
useStandardNegative();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ai-generator-dialog {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generator-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form Panel
|
||||||
|
.form-panel {
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
.optional {
|
||||||
|
font-weight: 400;
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider Options
|
||||||
|
.provider-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 140px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-name {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick Prompts
|
||||||
|
.quick-prompts {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
|
||||||
|
.quick-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.negative-presets {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-header {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-options {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-actions {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview Panel
|
||||||
|
.preview-panel {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 400px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.has-image {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.generating-state,
|
||||||
|
.empty-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #888;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-estimate {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-actions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// History
|
||||||
|
.generation-history {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tips Dialog
|
||||||
|
.tips-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-prompt {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.form-panel {
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-option {
|
||||||
|
border-color: #444;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.is-selected {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
background: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-header {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-prompt {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -31,6 +31,11 @@ export default defineComponent({
|
|||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
circuitSel: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
causalDest: {
|
causalDest: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
:to_user="myuser"
|
:to_user="myuser"
|
||||||
:sendRIS="sendRIS"
|
:sendRIS="sendRIS"
|
||||||
:causalDest="causalDest"
|
:causalDest="causalDest"
|
||||||
|
:circuitname="circuitSel"
|
||||||
@close="
|
@close="
|
||||||
showsendCoinTo = false;
|
showsendCoinTo = false;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|||||||
@@ -1,9 +1,215 @@
|
|||||||
.my-custom-container {
|
// Variables
|
||||||
max-width: 100%; /* Imposta la larghezza massima per il contenitore */
|
$border-radius-sm: 8px;
|
||||||
|
$border-radius-md: 12px;
|
||||||
|
$transition-fast: 0.2s ease;
|
||||||
|
|
||||||
|
.copy-share-container {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.small-variant {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-only {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapword {
|
// ═══════════════════════════════════════════
|
||||||
overflow: hidden; /* Nasconde il contenuto in eccesso */
|
// Link Display Wrapper
|
||||||
white-space: nowrap; /* Impedisce il wrapping del testo */
|
// ═══════════════════════════════════════════
|
||||||
text-overflow: ellipsis; /* Mostra "..." se il testo è troppo lungo */
|
.link-display-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link Preview Box
|
||||||
|
.link-preview-box {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: $border-radius-sm;
|
||||||
|
transition: all $transition-fast;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all $transition-fast;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Action Buttons
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.q-btn {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-btn,
|
||||||
|
.whatsapp-btn,
|
||||||
|
.telegram-btn {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 160px;
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.whatsapp-btn {
|
||||||
|
&:not(.q-btn--flat) {
|
||||||
|
background: linear-gradient(135deg, #25d366 0%, #128c7e 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegram-btn {
|
||||||
|
&:not(.q-btn--flat) {
|
||||||
|
background: linear-gradient(135deg, #0088cc 0%, #0077b5 100%) !important;
|
||||||
|
border: none !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Default Layout
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
.default-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.text-preview {
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-main-btn {
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
|
||||||
|
transition: all $transition-fast;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Copied State Animation
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
.copied-state {
|
||||||
|
animation: pulse-success 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-success {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Dark Mode
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
.body--dark {
|
||||||
|
.link-preview-box {
|
||||||
|
background: #1e293b;
|
||||||
|
border-color: #334155;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #475569;
|
||||||
|
background: #273449;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-text {
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-preview {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Responsive
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.action-buttons:not(.compact) {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.share-btn,
|
||||||
|
.whatsapp-btn,
|
||||||
|
.telegram-btn {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,55 +1,208 @@
|
|||||||
import { tools } from '../../store/Modules/tools'
|
import { tools } from '../../store/Modules/tools'
|
||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useUserStore } from '@store/UserStore'
|
import { defineComponent, ref, computed } from 'vue'
|
||||||
import { useGlobalStore } from '@store/globalStore'
|
|
||||||
import { defineComponent } from 'vue'
|
|
||||||
import { shared_consts } from '@/common/shared_vuejs'
|
import { shared_consts } from '@/common/shared_vuejs'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'CCopyBtnSmall',
|
name: 'CCopyBtnSmall',
|
||||||
props: {
|
props: {
|
||||||
|
// Testo/link da copiare
|
||||||
texttocopy: {
|
texttocopy: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
// Titolo per la condivisione
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
default: 'Condividi questo link',
|
||||||
|
},
|
||||||
|
// Messaggio personalizzato per la condivisione
|
||||||
|
shareMessage: {
|
||||||
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
// Lunghezza massima del testo visualizzato
|
||||||
|
maxLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 400,
|
||||||
|
},
|
||||||
|
// Dimensione compatta
|
||||||
small: {
|
small: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
|
||||||
default: false,
|
default: false,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
components: {},
|
// Mostra solo il pulsante (senza link preview)
|
||||||
setup(props) {
|
btn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// Mostra solo icona (senza label)
|
||||||
|
iconOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// Stile flat
|
||||||
|
flat: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// Stile outline
|
||||||
|
outline: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// Colore del pulsante
|
||||||
|
btnColor: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary',
|
||||||
|
},
|
||||||
|
// Label personalizzata
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: 'Copia link',
|
||||||
|
},
|
||||||
|
// Mostra il link preview
|
||||||
|
showLink: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// Mostra i pulsanti di azione
|
||||||
|
showActions: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// Mostra pulsante Share nativo
|
||||||
|
showShareBtn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// Mostra pulsante WhatsApp
|
||||||
|
showWhatsApp: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
// Mostra pulsante Telegram
|
||||||
|
showTelegram: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['copied', 'shared'],
|
||||||
|
|
||||||
|
setup(props, { emit }) {
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
async function copytoclipandsend() {
|
// State
|
||||||
tools.copyStringToClipboard($q, props.texttocopy, true)
|
const copied = ref(false)
|
||||||
|
let copyTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
let msg = 'Questo è il link che puoi condividere per farti inviare i RIS:<br><br>👉🏻 ' + props.texttocopy
|
// Computed
|
||||||
|
const truncatedText = computed(() => {
|
||||||
|
if (!props.texttocopy) return ''
|
||||||
|
if (props.texttocopy.length <= props.maxLength) return props.texttocopy
|
||||||
|
return props.texttocopy.substring(0, props.maxLength) + '...'
|
||||||
|
})
|
||||||
|
|
||||||
|
const containerClasses = computed(() => ({
|
||||||
|
'small-variant': props.small,
|
||||||
|
'btn-only': props.btn && !props.showLink,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const shareText = computed(() => {
|
||||||
|
if (props.shareMessage) return props.shareMessage
|
||||||
|
return `${props.title}\n\n👉 ${props.texttocopy}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
async function handleCopy() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(props.texttocopy)
|
||||||
|
|
||||||
|
copied.value = true
|
||||||
|
emit('copied', props.texttocopy)
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Link copiato negli appunti!',
|
||||||
|
icon: 'check',
|
||||||
|
timeout: 2000,
|
||||||
|
position: 'bottom',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset stato dopo 2 secondi
|
||||||
|
if (copyTimeout) clearTimeout(copyTimeout)
|
||||||
|
copyTimeout = setTimeout(() => {
|
||||||
|
copied.value = false
|
||||||
|
}, 2000)
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback per browser più vecchi
|
||||||
|
tools.copyStringToClipboard($q, props.texttocopy, true)
|
||||||
|
copied.value = true
|
||||||
|
|
||||||
|
if (copyTimeout) clearTimeout(copyTimeout)
|
||||||
|
copyTimeout = setTimeout(() => {
|
||||||
|
copied.value = false
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleShare() {
|
||||||
|
// Prima copia negli appunti
|
||||||
|
await handleCopy()
|
||||||
|
|
||||||
|
// Prova Web Share API
|
||||||
|
if (navigator.share) {
|
||||||
|
try {
|
||||||
|
await navigator.share({
|
||||||
|
title: props.title,
|
||||||
|
text: props.shareMessage || 'Ecco il link:',
|
||||||
|
url: props.texttocopy,
|
||||||
|
})
|
||||||
|
emit('shared', 'native')
|
||||||
|
} catch (err) {
|
||||||
|
// L'utente ha annullato o errore
|
||||||
|
console.log('Share cancelled or failed')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: usa Telegram come default
|
||||||
|
handleTelegram()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWhatsApp() {
|
||||||
|
const text = encodeURIComponent(shareText.value)
|
||||||
|
window.open(`https://wa.me/?text=${text}`, '_blank')
|
||||||
|
emit('shared', 'whatsapp')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTelegram() {
|
||||||
|
const msg = props.shareMessage ||
|
||||||
|
`Questo è il link che puoi condividere:\n\n👉 ${props.texttocopy}`
|
||||||
|
|
||||||
tools.sendMsgTelegramCmd($q, t, shared_consts.MsgTeleg.SHARE_TEXT, false, msg)
|
tools.sendMsgTelegramCmd($q, t, shared_consts.MsgTeleg.SHARE_TEXT, false, msg)
|
||||||
|
emit('shared', 'telegram')
|
||||||
}
|
|
||||||
|
|
||||||
function getclass() {
|
|
||||||
if (props.small) {
|
|
||||||
return 'text-h7'
|
|
||||||
} else {
|
|
||||||
return 'text-h5'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
copytoclipandsend,
|
// State
|
||||||
|
copied,
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
truncatedText,
|
||||||
|
containerClasses,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
handleCopy,
|
||||||
|
handleShare,
|
||||||
|
handleWhatsApp,
|
||||||
|
handleTelegram,
|
||||||
|
|
||||||
|
// Utils
|
||||||
tools,
|
tools,
|
||||||
getclass,
|
|
||||||
t,
|
t,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,128 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="" style="width: 100%; overflow: hidden;">
|
<div class="copy-share-container" :class="containerClasses">
|
||||||
<div class="row justify-center">
|
<!-- Variante: Solo pulsante -->
|
||||||
{{ tools.firstchars(texttocopy, 80) }}
|
<template v-if="btn && !showLink">
|
||||||
|
<q-btn
|
||||||
|
:size="small ? 'sm' : 'md'"
|
||||||
|
:round="iconOnly"
|
||||||
|
:rounded="!iconOnly"
|
||||||
|
:flat="flat"
|
||||||
|
:outline="outline"
|
||||||
|
:unelevated="!flat && !outline"
|
||||||
|
:color="btnColor"
|
||||||
|
:icon="copied ? 'check' : 'content_copy'"
|
||||||
|
:label="iconOnly ? undefined : (copied ? 'Copiato!' : label)"
|
||||||
|
:class="{ 'copied-state': copied }"
|
||||||
|
@click="handleCopy"
|
||||||
|
>
|
||||||
|
<q-tooltip v-if="iconOnly">{{ copied ? 'Copiato!' : 'Copia link' }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Variante: Link + Azioni -->
|
||||||
|
<template v-else-if="showLink">
|
||||||
|
<div class="link-display-wrapper">
|
||||||
|
<!-- Link Preview -->
|
||||||
|
<div class="link-preview-box" :class="{ 'compact': small }">
|
||||||
|
<q-icon name="link" :size="small ? '18px' : '20px'" color="primary" class="link-icon" />
|
||||||
|
<span class="link-text" :title="texttocopy">
|
||||||
|
{{ truncatedText }}
|
||||||
|
</span>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
:size="small ? 'sm' : 'md'"
|
||||||
|
:icon="copied ? 'check' : 'content_copy'"
|
||||||
|
:color="copied ? 'positive' : 'grey-7'"
|
||||||
|
class="copy-icon-btn"
|
||||||
|
@click="handleCopy"
|
||||||
|
>
|
||||||
|
<q-tooltip>{{ copied ? 'Copiato!' : 'Copia' }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-center q-mt-sm">
|
|
||||||
<q-btn rounded label="Condividi link" color="primary" @click="copytoclipandsend" />
|
<!-- Action Buttons -->
|
||||||
|
<div v-if="showActions" class="action-buttons" :class="{ 'compact': small }">
|
||||||
|
<q-btn
|
||||||
|
v-if="showShareBtn"
|
||||||
|
:outline="!small"
|
||||||
|
:flat="small"
|
||||||
|
:rounded="!small"
|
||||||
|
:round="small"
|
||||||
|
:size="small ? 'sm' : 'md'"
|
||||||
|
color="primary"
|
||||||
|
:icon="small ? 'share' : undefined"
|
||||||
|
:label="small ? undefined : 'Condividi'"
|
||||||
|
class="share-btn"
|
||||||
|
@click="handleShare"
|
||||||
|
>
|
||||||
|
<template v-if="!small" v-slot:prepend>
|
||||||
|
<q-icon name="share" />
|
||||||
|
</template>
|
||||||
|
<q-tooltip v-if="small">Condividi</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-if="showWhatsApp"
|
||||||
|
:outline="!small"
|
||||||
|
:flat="small"
|
||||||
|
:rounded="!small"
|
||||||
|
:round="small"
|
||||||
|
:size="small ? 'sm' : 'md'"
|
||||||
|
color="green"
|
||||||
|
:icon="small ? 'fab fa-whatsapp' : undefined"
|
||||||
|
:label="small ? undefined : 'WhatsApp'"
|
||||||
|
class="whatsapp-btn"
|
||||||
|
@click="handleWhatsApp"
|
||||||
|
>
|
||||||
|
<template v-if="!small" v-slot:prepend>
|
||||||
|
<q-icon name="fab fa-whatsapp" />
|
||||||
|
</template>
|
||||||
|
<q-tooltip v-if="small">WhatsApp</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-if="showTelegram"
|
||||||
|
:outline="!small"
|
||||||
|
:flat="small"
|
||||||
|
:rounded="!small"
|
||||||
|
:round="small"
|
||||||
|
:size="small ? 'sm' : 'md'"
|
||||||
|
color="light-blue"
|
||||||
|
:icon="small ? 'fab fa-telegram' : undefined"
|
||||||
|
:label="small ? undefined : 'Telegram'"
|
||||||
|
class="telegram-btn"
|
||||||
|
@click="handleTelegram"
|
||||||
|
>
|
||||||
|
<template v-if="!small" v-slot:prepend>
|
||||||
|
<q-icon name="fab fa-telegram" />
|
||||||
|
</template>
|
||||||
|
<q-tooltip v-if="small">Telegram</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Variante: Default (testo + bottone) -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="default-layout">
|
||||||
|
<div class="text-preview" :class="{ 'small': small }">
|
||||||
|
{{ truncatedText }}
|
||||||
|
</div>
|
||||||
|
<q-btn
|
||||||
|
rounded
|
||||||
|
unelevated
|
||||||
|
:size="small ? 'sm' : 'md'"
|
||||||
|
:label="copied ? 'Copiato!' : 'Condividi link'"
|
||||||
|
:color="copied ? 'positive' : 'primary'"
|
||||||
|
:icon="copied ? 'check' : 'share'"
|
||||||
|
class="share-main-btn"
|
||||||
|
@click="handleShare"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" src="./CCopyBtnSmall.ts"></script>
|
<script lang="ts" src="./CCopyBtnSmall.ts"></script>
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ export default defineComponent({
|
|||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
prop_compatto: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
finder: {
|
finder: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
labelElemFind="trovati"
|
labelElemFind="trovati"
|
||||||
:choose_visutype="visuType"
|
:choose_visutype="visuType"
|
||||||
:butt_modif_new="prop_modif"
|
:butt_modif_new="prop_modif"
|
||||||
|
:compatto="prop_compatto"
|
||||||
:noresultLabel="
|
:noresultLabel="
|
||||||
t('grid.nosearchfound') + ' ' + (showMap ? t('grid.intheareamap') : '')
|
t('grid.nosearchfound') + ' ' + (showMap ? t('grid.intheareamap') : '')
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// ========================================
|
||||||
|
// SHADOW VARIABLES
|
||||||
|
// ========================================
|
||||||
$shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
|
$shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||||
$shadow-md: 0 2px 6px rgba(0, 0, 0, 0.08);
|
$shadow-md: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
$shadow-hover: 0 4px 12px rgba(25, 118, 210, 0.15);
|
$shadow-hover: 0 4px 12px rgba(25, 118, 210, 0.15);
|
||||||
@@ -20,22 +23,23 @@ $shadow-hover: 0 4px 12px rgba(25, 118, 210, 0.15);
|
|||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-table td {
|
.q-table {
|
||||||
padding-left: 1px;
|
td {
|
||||||
padding-right: 2px;
|
padding: 0 2px 0 1px;
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-table {
|
|
||||||
&__col {
|
&__col {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__top {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.newrec_fields {
|
.newrec_fields {
|
||||||
@@ -51,10 +55,6 @@ $shadow-hover: 0 4px 12px rgba(25, 118, 210, 0.15);
|
|||||||
padding: 3px 6px !important;
|
padding: 3px 6px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-table__top {
|
|
||||||
padding-top: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// DIALOG & LAYOUT
|
// DIALOG & LAYOUT
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -88,6 +88,7 @@ $shadow-hover: 0 4px 12px rgba(25, 118, 210, 0.15);
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: calc(100vh - 120px);
|
max-height: calc(100vh - 120px);
|
||||||
|
|
||||||
|
// Scrollbar styling
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
@@ -148,15 +149,20 @@ $shadow-hover: 0 4px 12px rgba(25, 118, 210, 0.15);
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// INFINITE SCROLL
|
// GRID LAYOUT PER CARD (INFINITE SCROLL)
|
||||||
// ========================================
|
|
||||||
// ========================================
|
|
||||||
// GRID LAYOUT PER CARD
|
|
||||||
// ========================================
|
// ========================================
|
||||||
.q-infinite-scroll {
|
.q-infinite-scroll {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
|
// Mobile: colonna singola
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
// Desktop: layout a griglia 2 colonne
|
// Desktop: layout a griglia 2 colonne
|
||||||
@media (min-width: $mobile-breakpoint) {
|
@media (min-width: $mobile-breakpoint) {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -165,19 +171,11 @@ $shadow-hover: 0 4px 12px rgba(25, 118, 210, 0.15);
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desktop large: 3 colonne per schermi molto larghi
|
// Desktop large: mantiene 2 colonne con gap maggiore
|
||||||
@media (min-width: 1400px) {
|
@media (min-width: 1400px) {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mobile: colonna singola
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 5px;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -209,6 +207,10 @@ $shadow-hover: 0 4px 12px rgba(25, 118, 210, 0.15);
|
|||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// GRID CARD ITEM
|
||||||
|
// ========================================
|
||||||
.grid-card-item {
|
.grid-card-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: rgba(255, 255, 255, 0.7);
|
background: rgba(255, 255, 255, 0.7);
|
||||||
@@ -216,14 +218,16 @@ $shadow-hover: 0 4px 12px rgba(25, 118, 210, 0.15);
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
padding: 8px; // Spazio tra carousel e card
|
padding: 8px;
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
width: 100%;
|
padding: 4px;
|
||||||
padding: 4px; // Ridotto su mobile
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// UTILITY CLASSES
|
||||||
|
// ========================================
|
||||||
.fill-all-width {
|
.fill-all-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,7 @@ import { CMyUser } from '../CMyUser';
|
|||||||
import { CMyGroups } from '../CMyGroups';
|
import { CMyGroups } from '../CMyGroups';
|
||||||
import { CMyFieldDb } from '../CMyFieldDb';
|
import { CMyFieldDb } from '../CMyFieldDb';
|
||||||
import { CMyRecCard } from '../CMyRecCard';
|
import { CMyRecCard } from '../CMyRecCard';
|
||||||
|
import { CMyRecEventi } from '../CMyRecEventi';
|
||||||
import { CMyRecCatalog } from '../CMyRecCatalog';
|
import { CMyRecCatalog } from '../CMyRecCatalog';
|
||||||
import { CMyRecRaccoltaCataloghi } from '../CMyRecRaccoltaCataloghi';
|
import { CMyRecRaccoltaCataloghi } from '../CMyRecRaccoltaCataloghi';
|
||||||
import { CMapByTable } from '../CMapByTable';
|
import { CMapByTable } from '../CMapByTable';
|
||||||
@@ -176,6 +177,11 @@ export default defineComponent({
|
|||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
compatto: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
actionType: {
|
actionType: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -381,6 +387,7 @@ export default defineComponent({
|
|||||||
CMyGroups,
|
CMyGroups,
|
||||||
CMyUser,
|
CMyUser,
|
||||||
CMyRecCard,
|
CMyRecCard,
|
||||||
|
CMyRecEventi,
|
||||||
CMyRecCatalog,
|
CMyRecCatalog,
|
||||||
CMyCardPopup,
|
CMyCardPopup,
|
||||||
CMyRecGrpCard,
|
CMyRecGrpCard,
|
||||||
@@ -1341,14 +1348,20 @@ export default defineComponent({
|
|||||||
obj2.idSkill = idSkill;
|
obj2.idSkill = idSkill;
|
||||||
filtersearch2.push(obj2);
|
filtersearch2.push(obj2);
|
||||||
}
|
}
|
||||||
} else if (item.table === toolsext.TABGOODS && item.value === costanti.FILTER_TUTTI) {
|
} else if (
|
||||||
|
item.table === toolsext.TABGOODS &&
|
||||||
|
item.value === costanti.FILTER_TUTTI
|
||||||
|
) {
|
||||||
const obj2: any = {};
|
const obj2: any = {};
|
||||||
if (idSectorGood > 0) {
|
if (idSectorGood > 0) {
|
||||||
// idSectorGood
|
// idSectorGood
|
||||||
obj2['sectorGood._id'] = idSectorGood;
|
obj2['sectorGood._id'] = idSectorGood;
|
||||||
filtersearch2.push(obj2);
|
filtersearch2.push(obj2);
|
||||||
}
|
}
|
||||||
} else if (item.table === toolsext.TABBACHECAS && item.value === costanti.FILTER_TUTTI) {
|
} else if (
|
||||||
|
item.table === toolsext.TABBACHECAS &&
|
||||||
|
item.value === costanti.FILTER_TUTTI
|
||||||
|
) {
|
||||||
const obj2: any = {};
|
const obj2: any = {};
|
||||||
if (idSectorBacheca > 0) {
|
if (idSectorBacheca > 0) {
|
||||||
// idSectorBacheca
|
// idSectorBacheca
|
||||||
|
|||||||
@@ -89,6 +89,8 @@
|
|||||||
>
|
>
|
||||||
</CTitleBanner>
|
</CTitleBanner>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div v-if="shared_consts.VERTIC_SHOW_GRID.includes(myvertical)">
|
<div v-if="shared_consts.VERTIC_SHOW_GRID.includes(myvertical)">
|
||||||
<div
|
<div
|
||||||
v-if="(prop_search || canEdit) && finder"
|
v-if="(prop_search || canEdit) && finder"
|
||||||
@@ -444,7 +446,29 @@
|
|||||||
:style="heightcarousel ? `height: ${heightcarousel}` : ''"
|
:style="heightcarousel ? `height: ${heightcarousel}` : ''"
|
||||||
v-intersection="onIntersection"
|
v-intersection="onIntersection"
|
||||||
>
|
>
|
||||||
|
<div v-if="tablesel === shared_consts.TABLES_MYBACHECAS && compatto">
|
||||||
|
<div
|
||||||
|
v-for="(row, indexrow) in serverData.slice(0, 5)"
|
||||||
|
:key="row._id || indexrow"
|
||||||
|
:class="{
|
||||||
|
row: opt.rowclass,
|
||||||
|
'items-stretch': opt.rowclass,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<CMyRecEventi
|
||||||
|
:table="tablesel"
|
||||||
|
:prop_myrec="row"
|
||||||
|
@cmdext="cmdExt"
|
||||||
|
:editOn="editOn"
|
||||||
|
:margin_right="margin_right"
|
||||||
|
:compatto="compatto"
|
||||||
|
>
|
||||||
|
</CMyRecEventi>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-carousel
|
<q-carousel
|
||||||
|
v-else
|
||||||
swipeable
|
swipeable
|
||||||
animated
|
animated
|
||||||
:autoplay="autoplay"
|
:autoplay="autoplay"
|
||||||
@@ -507,6 +531,7 @@
|
|||||||
@cmdext="cmdExt"
|
@cmdext="cmdExt"
|
||||||
:editOn="editOn"
|
:editOn="editOn"
|
||||||
:margin_right="margin_right"
|
:margin_right="margin_right"
|
||||||
|
:compatto="compatto"
|
||||||
>
|
>
|
||||||
</CMyRecCard>
|
</CMyRecCard>
|
||||||
</q-carousel-slide>
|
</q-carousel-slide>
|
||||||
@@ -523,10 +548,7 @@
|
|||||||
|
|
||||||
<q-infinite-scroll
|
<q-infinite-scroll
|
||||||
ref="myinfscroll"
|
ref="myinfscroll"
|
||||||
v-else-if="
|
v-else-if="shared_consts.VERTIC_SHOW_GRID.includes(myvertical) && alreadymounting"
|
||||||
shared_consts.VERTIC_SHOW_GRID.includes(myvertical) &&
|
|
||||||
alreadymounting
|
|
||||||
"
|
|
||||||
:initial-index="0"
|
:initial-index="0"
|
||||||
@load="onLoadScroll"
|
@load="onLoadScroll"
|
||||||
:offset="350"
|
:offset="350"
|
||||||
@@ -631,6 +653,7 @@
|
|||||||
@cmdext="cmdExt"
|
@cmdext="cmdExt"
|
||||||
:editOn="editOn"
|
:editOn="editOn"
|
||||||
:margin_right="margin_right"
|
:margin_right="margin_right"
|
||||||
|
:compatto="compatto"
|
||||||
>
|
>
|
||||||
</CMyRecCard>
|
</CMyRecCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -699,9 +722,7 @@
|
|||||||
</q-infinite-scroll>
|
</q-infinite-scroll>
|
||||||
<q-table
|
<q-table
|
||||||
v-else-if="
|
v-else-if="
|
||||||
!shared_consts.VERTIC_SHOW_GRID.includes(myvertical) &&
|
!shared_consts.VERTIC_SHOW_GRID.includes(myvertical) && serverData && mycolumns
|
||||||
serverData &&
|
|
||||||
mycolumns
|
|
||||||
"
|
"
|
||||||
:grid="shared_consts.VERTIC_SHOW_GRID.includes(myvertical)"
|
:grid="shared_consts.VERTIC_SHOW_GRID.includes(myvertical)"
|
||||||
:grid-header="
|
:grid-header="
|
||||||
@@ -728,55 +749,38 @@
|
|||||||
selection="single"
|
selection="single"
|
||||||
v-model:selected="selected"
|
v-model:selected="selected"
|
||||||
>
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th />
|
||||||
|
<q-th
|
||||||
|
v-for="(col, index) in (props.cols || []).filter(
|
||||||
|
(c) => c !== undefined && c !== null
|
||||||
|
)"
|
||||||
|
:key="col.name || index"
|
||||||
|
:props="props"
|
||||||
|
class="text-italic text-weight-bold"
|
||||||
|
>
|
||||||
|
<span v-if="col">
|
||||||
<template
|
<template
|
||||||
v-if="
|
v-if="
|
||||||
!(
|
!(
|
||||||
myvertical === costanti.VISUTABLE_SCHEDA_USER ||
|
myvertical === costanti.VISUTABLE_SCHEDA_USER ||
|
||||||
myvertical === 2 ||
|
myvertical === 2 ||
|
||||||
myvertical === costanti.VISUTABLE_SCHEDA_GROUP
|
myvertical === costanti.VISUTABLE_SCHEDA_GROUP
|
||||||
)
|
) &&
|
||||||
|
showColCheck &&
|
||||||
|
showColCheck(col, tools.TIPOVIS_SHOW_RECORD, true)
|
||||||
"
|
"
|
||||||
v-slot:header="props"
|
|
||||||
>
|
>
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th> </q-th>
|
|
||||||
<q-th
|
|
||||||
v-for="col in props.cols"
|
|
||||||
:key="col.name"
|
|
||||||
:props="props"
|
|
||||||
class="text-italic text-weight-bold"
|
|
||||||
>
|
|
||||||
<span v-if="col && showColCheck(col, tools.TIPOVIS_SHOW_RECORD, true)">
|
|
||||||
{{ col.label }}
|
{{ col.label }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="col.sortable !== undefined">
|
||||||
|
{{ col.label }}
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</q-th>
|
</q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
<template
|
|
||||||
v-else
|
|
||||||
v-slot:header="props"
|
|
||||||
>
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th> </q-th>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-for="col in props.cols"
|
|
||||||
:key="col.name"
|
|
||||||
>
|
|
||||||
<q-th
|
|
||||||
v-if="col.sortable"
|
|
||||||
:key="col.name"
|
|
||||||
:props="props"
|
|
||||||
class="text-italic text-weight-bold"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{{ col.label }}
|
|
||||||
</span>
|
|
||||||
</q-th>
|
|
||||||
</span>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-slot:top-right
|
v-slot:top-right
|
||||||
v-if="tablesList || arrfilters || enableExport"
|
v-if="tablesList || arrfilters || enableExport"
|
||||||
|
|||||||
@@ -219,7 +219,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<q-item-section>{{
|
<q-item-section>{{
|
||||||
circuit.askManagerToEnter ? t('circuit.ask') : t('circuit.enter')
|
circuit.askManagerToEnter ? t('circuit.ask') : t('circuit.Iscriviti')
|
||||||
}}</q-item-section>
|
}}</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
@@ -436,7 +436,7 @@
|
|||||||
"
|
"
|
||||||
icon="fas fa-user-plus"
|
icon="fas fa-user-plus"
|
||||||
color="primary"
|
color="primary"
|
||||||
:label="circuit.askManagerToEnter ? t('circuit.ask') : t('circuit.enter')"
|
:label="circuit.askManagerToEnter ? t('circuit.ask') : t('circuit.Iscriviti')"
|
||||||
rounded
|
rounded
|
||||||
size="lg"
|
size="lg"
|
||||||
@click="
|
@click="
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ import { tools } from '@tools'
|
|||||||
import { CUserNonVerif } from '@/components/CUserNonVerif'
|
import { CUserNonVerif } from '@/components/CUserNonVerif'
|
||||||
import { CTitleBanner } from '@/components/CTitleBanner'
|
import { CTitleBanner } from '@/components/CTitleBanner'
|
||||||
import { CMovements } from '@/components/CMovements'
|
import { CMovements } from '@/components/CMovements'
|
||||||
import { CSendRISTo } from '@/components/CSendRISTo'
|
|
||||||
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'CMyCircuits',
|
name: 'CMyCircuits',
|
||||||
components: { CMyCircuit, CUserNonVerif, CTitleBanner, CMovements, CSendRISTo },
|
components: { CMyCircuit, CUserNonVerif, CTitleBanner, CMovements },
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
<div v-if="tools.isUserOk() && finishloading">
|
<div v-if="tools.isUserOk() && finishloading">
|
||||||
<div v-if="finder && showfinder" class="q-gutter-sm q-pa-sm q-pb-sm">
|
<div v-if="finder && showfinder" class="q-gutter-sm q-pa-sm q-pb-sm">
|
||||||
<div class="q-mt-md">
|
<div class="q-mt-md">
|
||||||
<CSendRISTo></CSendRISTo>
|
|
||||||
|
|
||||||
<q-tabs
|
<q-tabs
|
||||||
v-model="mytab"
|
v-model="mytab"
|
||||||
|
|||||||
@@ -1539,6 +1539,41 @@
|
|||||||
label="Bottone Modifica"
|
label="Bottone Modifica"
|
||||||
@update:model-value="modifElem"
|
@update:model-value="modifElem"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
|
<q-toggle
|
||||||
|
v-model="myel.parambool5"
|
||||||
|
color="positive"
|
||||||
|
label="Compatto"
|
||||||
|
@update:model-value="modifElem"
|
||||||
|
></q-toggle>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
label="Titolo Principale"
|
||||||
|
@update:model-value="modifElem"
|
||||||
|
v-model="myel.stiletit_str"
|
||||||
|
filled
|
||||||
|
v-on:keyup.enter="saveElem"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
label="Icona Titolo Normale:"
|
||||||
|
@update:model-value="modifElem"
|
||||||
|
v-model="myel.stiletit_icon"
|
||||||
|
filled
|
||||||
|
v-on:keyup.enter="saveElem"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
label="Link Principale: /..."
|
||||||
|
@update:model-value="modifElem"
|
||||||
|
v-model="myel.container2"
|
||||||
|
filled
|
||||||
|
v-on:keyup.enter="saveElem"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="myel.type === shared_consts.ELEMTYPE.RACCOLTE_CATALOGHI"></div>
|
<div v-else-if="myel.type === shared_consts.ELEMTYPE.RACCOLTE_CATALOGHI"></div>
|
||||||
|
|||||||
@@ -1,44 +1,241 @@
|
|||||||
|
// ========================================
|
||||||
|
// COLOR VARIABLES
|
||||||
|
// ========================================
|
||||||
$grayshadow: #555;
|
$grayshadow: #555;
|
||||||
|
|
||||||
$textcol: blue;
|
$textcol: blue;
|
||||||
$textcol_scuro: darkblue;
|
$textcol_scuro: darkblue;
|
||||||
|
|
||||||
|
// Dark mode variants
|
||||||
|
$grayshadow-dark: #222;
|
||||||
|
$textcol-dark: #64b5f6;
|
||||||
|
$textcol_scuro-dark: #90caf9;
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// TYPOGRAPHY BASE
|
||||||
|
// ========================================
|
||||||
p {
|
p {
|
||||||
margin: 0 0 1.25rem;
|
margin: 0 0 1.25rem;
|
||||||
//text-shadow: .125rem .125rem .25rem $grayshadow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-shadow: 0.25rem 0.25rem 0.5rem $grayshadow;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
text-shadow: 0.25rem 0.25rem 0.5rem $grayshadow-dark;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// HEADING STYLES
|
||||||
|
// ========================================
|
||||||
|
.text-h1,
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 3rem;
|
||||||
|
letter-spacing: -0.01562em;
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-h2 {
|
||||||
|
font-size: 3.75rem;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 3.75rem;
|
||||||
|
letter-spacing: -0.00833em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-weight-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-vers {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.75rem;
|
||||||
|
letter-spacing: 0.00937em;
|
||||||
|
text-shadow: 0.25rem 0.25rem 0.5rem $grayshadow;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
text-shadow: 0.25rem 0.25rem 0.5rem $grayshadow-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CARD & UTILITY
|
||||||
|
// ========================================
|
||||||
.mycard {
|
.mycard {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
text-shadow: 0.125rem 0.125rem 0.25rem $grayshadow;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
text-shadow: 0.125rem 0.125rem 0.25rem $grayshadow-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// LANDING PAGE - BACKGROUND & LAYOUT
|
||||||
|
// ========================================
|
||||||
.landing_background {
|
.landing_background {
|
||||||
background: #000 url(/images/foto1.jpg) no-repeat 50% fixed;
|
background: #000 url("/images/foto1.jpg") no-repeat 50% fixed;
|
||||||
background-size: cover
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing>section {
|
.landing > section {
|
||||||
display: -webkit-box;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
-webkit-box-pack: center;
|
|
||||||
-ms-flex-pack: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
//padding: 0 16px
|
|
||||||
|
> div {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.padding {
|
||||||
|
padding: 5.62rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.padding_testo {
|
||||||
|
padding-top: 1.25rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.padding_gallery {
|
||||||
|
padding-top: 3.125rem;
|
||||||
|
padding-bottom: 5.625rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// LANDING TOOLBAR
|
||||||
|
// ========================================
|
||||||
|
.landing__toolbar {
|
||||||
|
background: linear-gradient(180deg, #000, transparent);
|
||||||
|
padding: 0 !important;
|
||||||
|
|
||||||
|
.q-btn {
|
||||||
|
border-radius: 0 0 0.315rem 0.315rem;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// LANDING HERO
|
||||||
|
// ========================================
|
||||||
|
.landing__hero {
|
||||||
|
min-height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing__header {
|
||||||
|
height: 18vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing__arrow {
|
||||||
|
bottom: 1.5rem;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing__front {
|
||||||
|
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.6) 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing__logo {
|
||||||
|
width: 9.4rem;
|
||||||
|
height: 9.4rem;
|
||||||
|
margin-top: 1.315rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// LANDING FEATURES
|
||||||
|
// ========================================
|
||||||
|
.landing__features {
|
||||||
|
.q-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4,
|
||||||
|
h6 {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feat-descr {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transition: opacity 0.5s ease-in-out;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// LANDING FOOTER
|
||||||
|
// ========================================
|
||||||
|
.landing__footer {
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, 0.8) 95%, #fff);
|
||||||
|
padding: 4.5rem 1.25rem !important;
|
||||||
|
color: #9f9f9f;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, 0.9) 95%, #1a1a1a);
|
||||||
|
color: #b0b0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-link {
|
||||||
|
color: $textcol;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: $textcol-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing__footer-icons {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin: 0 0.5rem 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: 0;
|
||||||
|
color: $textcol;
|
||||||
|
transition: color 0.28s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $textcol_scuro;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: $textcol-dark;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $textcol_scuro-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// INTRO SECTION
|
||||||
|
// ========================================
|
||||||
.intro {
|
.intro {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
/* flex-flow: row nowrap; */
|
padding: 1.25rem 0;
|
||||||
|
margin: 0.125rem;
|
||||||
padding: 1.25rem 0 1.25rem 0;
|
|
||||||
margin: .125rem;
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -47,10 +244,7 @@ h4 {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__associazione {
|
&__associazione,
|
||||||
min-width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__comeassociarsi {
|
&__comeassociarsi {
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
}
|
}
|
||||||
@@ -64,160 +258,20 @@ h4 {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing>section.padding {
|
// ========================================
|
||||||
padding: 5.62rem 1rem;
|
// LAYOUT UTILITIES
|
||||||
}
|
// ========================================
|
||||||
|
|
||||||
.landing>section.padding_testo {
|
|
||||||
padding-top: 1.25rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing>section.padding_gallery {
|
|
||||||
padding-top: 3.125rem;
|
|
||||||
padding-bottom: 5.625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing>section>div {
|
|
||||||
position: relative;
|
|
||||||
width: 100%
|
|
||||||
}
|
|
||||||
|
|
||||||
.maxwidth1200 {
|
.maxwidth1200 {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing__toolbar {
|
|
||||||
background: -webkit-gradient(linear, left top, left bottom, from(#000), to(transparent));
|
|
||||||
background: linear-gradient(180deg, #000, transparent);
|
|
||||||
padding: 0 !important
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__toolbar .q-btn {
|
|
||||||
border-radius: 0 0 .315rem .315rem;
|
|
||||||
-ms-flex-item-align: stretch;
|
|
||||||
align-self: stretch
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__hero {
|
|
||||||
min-height: 50vh
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__header {
|
|
||||||
height: 18vh
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__arrow {
|
|
||||||
bottom: 1.5rem;
|
|
||||||
opacity: .4
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__front {
|
|
||||||
background: -webkit-gradient(linear, left top, left bottom, from(transparent), color-stop(15%, rgba(0, 0, 0, .6)));
|
|
||||||
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, .6) 15%)
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__logo {
|
|
||||||
width: 9.40rem;
|
|
||||||
height: 9.40rem;
|
|
||||||
margin-top: 1.315rem;
|
|
||||||
//-webkit-animation: logo-rotate 240s linear infinite;
|
|
||||||
//animation: logo-rotate 240s linear infinite
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__features .q-icon {
|
|
||||||
font-size: 4rem
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
line-height: 1.5;
|
|
||||||
text-shadow: .25rem .25rem .5rem $grayshadow;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__features h4,
|
|
||||||
.landing__features h6 {
|
|
||||||
margin: 1rem 0
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__features p {
|
|
||||||
opacity: .7;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__footer {
|
|
||||||
//background: -webkit-gradient(linear, left top, left bottom, color-stop(65%, rgba(0, 0, 0, .1)), to(#000));
|
|
||||||
background: linear-gradient(180deg, rgba(0, 0, 0, .8) 95%, #FFF);
|
|
||||||
padding-top: 4.5rem !important;
|
|
||||||
padding-bottom: 4.5rem !important;
|
|
||||||
padding-left: 1.25rem;
|
|
||||||
padding-right: 1.25rem;
|
|
||||||
color: #9f9f9f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon_contact:hover {
|
|
||||||
color: blue;
|
|
||||||
border-color: white;
|
|
||||||
border-width: .0625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__footer .doc-link {
|
|
||||||
color: $textcol;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__footer .doc-link:hover {
|
|
||||||
opacity: .8
|
|
||||||
}
|
|
||||||
|
|
||||||
.feat-descr {
|
|
||||||
font-size: 1.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feat-descr:hover {
|
|
||||||
transition: opacity 0.5s ease-in-out;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-col-gutter-sm {
|
.q-col-gutter-sm {
|
||||||
padding: 3.125rem 3.125rem;
|
padding: 3.125rem;
|
||||||
//margin-left: -48px
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.mobile .landing:before {
|
|
||||||
content: "";
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
height: 100vh;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: -1;
|
|
||||||
//background: #000 url(/images/cover.jpg) 50%;
|
|
||||||
|
|
||||||
background-size: cover
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
@-webkit-keyframes logo-rotate {
|
|
||||||
to {
|
|
||||||
-webkit-transform: rotate(-1turn);
|
|
||||||
transform: rotate(-1turn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-rotate {
|
|
||||||
to {
|
|
||||||
-webkit-transform: rotate(-1turn);
|
|
||||||
transform: rotate(-1turn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.home {
|
.home {
|
||||||
//background-color: rgb(250, 250, 250);
|
|
||||||
padding: 3.125rem;
|
padding: 3.125rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
//flex-wrap: nowrap;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -227,86 +281,291 @@ body.mobile .landing:before {
|
|||||||
margin: 3.125rem;
|
margin: 3.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow {
|
// ========================================
|
||||||
//color: white;
|
// MOBILE BACKGROUND
|
||||||
text-shadow: 0.125rem 0.125rem 0.25rem $grayshadow;
|
// ========================================
|
||||||
}
|
body.mobile .landing::before {
|
||||||
|
content: "";
|
||||||
.text-h1,
|
position: fixed;
|
||||||
h1 {
|
top: 0;
|
||||||
font-size: 3rem;
|
height: 100vh;
|
||||||
font-weight: bold;
|
left: 0;
|
||||||
line-height: 3rem;
|
right: 0;
|
||||||
letter-spacing: -.01562em;
|
bottom: 0;
|
||||||
margin-bottom: 8px !important;
|
z-index: -1;
|
||||||
}
|
background-size: cover;
|
||||||
|
|
||||||
.text-h2 {
|
|
||||||
font-size: 3.75rem;
|
|
||||||
font-weight: 300;
|
|
||||||
line-height: 3.75rem;
|
|
||||||
letter-spacing: -.00833em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-weight-bold {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-vers {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
letter-spacing: .00937em;
|
|
||||||
text-shadow: .25rem .25rem .5rem $grayshadow;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.landing__footer-icons {
|
|
||||||
font-size: 1.75rem
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__footer-icons a {
|
|
||||||
margin: 0 .5rem .5rem;
|
|
||||||
text-decoration: none;
|
|
||||||
outline: 0;
|
|
||||||
color: $textcol;
|
|
||||||
transition: color .28s
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing__footer-icons a:hover {
|
|
||||||
color: $textcol_scuro;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mylist {
|
|
||||||
background: #3fdaff;
|
|
||||||
padding-left: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clgutter {
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
padding: .62rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// GALLERY & CAROUSEL
|
||||||
|
// ========================================
|
||||||
.carousel_img_3 {
|
.carousel_img_3 {
|
||||||
//background-image: url(/images/cibo_sano.jpg);
|
|
||||||
background-size: cover !important;
|
background-size: cover !important;
|
||||||
background-position: 50% center !important;
|
background-position: 50% center !important;
|
||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 718px) {
|
.custom-caption {
|
||||||
// PER VERSIONE MOBILE
|
text-align: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: $textcol;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: $textcol-dark;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sfondo-grigio {
|
||||||
|
padding: 1rem;
|
||||||
|
color: $textcol;
|
||||||
|
background-color: rgba(0, 0, 0, 0.35);
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: $textcol-dark;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CONTACTS
|
||||||
|
// ========================================
|
||||||
|
.mycontacts {
|
||||||
|
color: gray;
|
||||||
|
letter-spacing: 0.078rem;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycontacts_title {
|
||||||
|
text-shadow: 0.125rem 0.125rem 0.125rem #555;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #999;
|
||||||
|
letter-spacing: 0.125rem;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
text-shadow: 0.125rem 0.125rem 0.125rem #222;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mycontacts_text {
|
||||||
|
color: #999;
|
||||||
|
letter-spacing: 0.093rem;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon_contact {
|
||||||
|
&:hover {
|
||||||
|
color: blue;
|
||||||
|
border-color: white;
|
||||||
|
border-width: 0.0625rem;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: $textcol-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// GUTTER & SPACING
|
||||||
|
// ========================================
|
||||||
|
.clgutter {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding: 0.62rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mylist {
|
||||||
|
background: #3fdaff;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
background: #1a8a9e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// IMAGE
|
||||||
|
// ========================================
|
||||||
|
.doc-img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// EDITOR STYLES
|
||||||
|
// ========================================
|
||||||
|
.clEditDiv {
|
||||||
|
border: 2px solid #c8c9cb;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clEditNotActive {
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clEdit {
|
||||||
|
border: 2px solid #f69f09;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #11f609;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedElem {
|
||||||
|
border: 3px solid #b91111 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elemEdit {
|
||||||
|
margin: 3px;
|
||||||
|
padding: 3px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 2px solid #11f609;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.myElemBase {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// ALIGNMENT UTILITIES
|
||||||
|
// ========================================
|
||||||
|
.align_center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align_right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align_left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// FLEX UTILITIES
|
||||||
|
// ========================================
|
||||||
|
.flex3 {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: green;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
background-color: #1b5e20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CONTENT SECTION
|
||||||
|
// ========================================
|
||||||
|
.content-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: $r-lg;
|
||||||
|
padding: $s-sm;
|
||||||
|
margin-bottom: $s-sm;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
background: #2d2d2d;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: $s-sm;
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// SECTION TITLE
|
||||||
|
// ========================================
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $s-sm;
|
||||||
|
background: $gradient-primary;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
|
||||||
|
.q-icon {
|
||||||
|
background: $gradient-primary;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
background: $gradient-primary;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
|
||||||
|
.q-icon {
|
||||||
|
background: $gradient-primary;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MOBILE RESPONSIVE
|
||||||
|
// ========================================
|
||||||
|
@media (max-width: 718px) {
|
||||||
.landing__hero {
|
.landing__hero {
|
||||||
text-align: center
|
text-align: center;
|
||||||
|
|
||||||
|
.text-h1,
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 2.05rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing__header {
|
.landing__header {
|
||||||
height: 7vh
|
height: 7vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clgutter {
|
.clgutter {
|
||||||
@@ -314,43 +573,42 @@ h1 {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing__hero .text-h1,
|
.landing > section {
|
||||||
h1 {
|
&.padding {
|
||||||
font-size: 2rem;
|
|
||||||
line-height: 2.05rem;
|
|
||||||
margin-bottom: 1.25rem
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing>section.padding {
|
|
||||||
padding: 2.5rem 1rem;
|
padding: 2.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing>section.padding_testo {
|
&.padding_testo {
|
||||||
padding-top: 1.25rem;
|
padding-top: 1.25rem;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing>section.padding_gallery {
|
&.padding_gallery {
|
||||||
padding-top: 3.125rem;
|
padding-top: 3.125rem;
|
||||||
padding-bottom: 5.625rem;
|
padding-bottom: 5.625rem;
|
||||||
|
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
|
||||||
|
|
||||||
.landing>section.padding_gallery>div {
|
> div {
|
||||||
padding-top: 3.125rem;
|
padding-top: 3.125rem;
|
||||||
padding-bottom: 5.625rem;
|
padding-bottom: 5.625rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing__features h4,
|
.landing__features {
|
||||||
.landing__features h6 {
|
h4,
|
||||||
margin: 1.25rem 0
|
h6 {
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
text-shadow: 0.25rem 0.25rem 0.5rem $grayshadow;
|
text-shadow: 0.25rem 0.25rem 0.5rem $grayshadow;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
text-shadow: 0.25rem 0.25rem 0.5rem $grayshadow-dark;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.landing .feature-item {
|
.landing .feature-item {
|
||||||
@@ -367,13 +625,11 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.landing__hero-btns {
|
.landing__hero-btns {
|
||||||
-webkit-box-pack: center;
|
justify-content: center;
|
||||||
-ms-flex-pack: center;
|
|
||||||
justify-content: center
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-col-gutter-sm {
|
.q-col-gutter-sm {
|
||||||
padding: .625rem .315rem;
|
padding: 0.625rem 0.315rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-subtitle1 {
|
.text-subtitle1 {
|
||||||
@@ -383,91 +639,4 @@ h1 {
|
|||||||
.text-vers {
|
.text-vers {
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-caption {
|
|
||||||
text-align: center;
|
|
||||||
padding: .75rem;
|
|
||||||
color: $textcol;
|
|
||||||
background-color: rgba(0, 0, 0, .3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sfondo-grigio {
|
|
||||||
padding: 1rem;
|
|
||||||
color: $textcol;
|
|
||||||
background-color: rgba(0, 0, 0, .35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mycontacts {
|
|
||||||
color: gray;
|
|
||||||
letter-spacing: 0.078rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mycontacts_title {
|
|
||||||
text-shadow: 0.125rem 0.125rem 0.125rem #555;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #999;
|
|
||||||
letter-spacing: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mycontacts_text {
|
|
||||||
color: #999;
|
|
||||||
letter-spacing: 0.093rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clEditDiv {
|
|
||||||
border: #c8c9cb solid 2px;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clEditNotActive {
|
|
||||||
background-color: #e6e6e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clEdit {
|
|
||||||
border: #f69f09 solid 2px;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clEdit:hover {
|
|
||||||
border: #11f609 solid 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selectedElem {
|
|
||||||
border: #b91111 solid 3px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align_center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align_right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align_left {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex3 {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
background-color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.elemEdit {
|
|
||||||
margin: 3px;
|
|
||||||
padding: 3px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.elemEdit:hover {
|
|
||||||
border: #11f609 solid 2px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.myElemBase{
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
@@ -32,11 +32,13 @@ import { shared_consts } from '@/common/shared_vuejs';
|
|||||||
import { LandingFooter } from '@/components/LandingFooter';
|
import { LandingFooter } from '@/components/LandingFooter';
|
||||||
import { CMyActivities } from '@/components/CMyActivities';
|
import { CMyActivities } from '@/components/CMyActivities';
|
||||||
import { CECommerce } from '@/components/CECommerce';
|
import { CECommerce } from '@/components/CECommerce';
|
||||||
|
import { EventPosterGenerator } from '@/components/EventPosterGenerator';
|
||||||
import { CheckEmail } from '@/components/CheckEmail';
|
import { CheckEmail } from '@/components/CheckEmail';
|
||||||
import { HomeRiso } from '@/components/HomeRiso';
|
import { HomeRiso } from '@/components/HomeRiso';
|
||||||
import mycircuits from '@/views/user/mycircuits/mycircuits.vue';
|
import mycircuits from '@/views/user/mycircuits/mycircuits.vue';
|
||||||
import PageRis from '@/components/pageris/pageris.vue';
|
import PageRis from '@/components/pageris/pageris.vue';
|
||||||
import { Riso_Home_Modern } from '@/components/Riso_Home_Modern';
|
import { Riso_Home_Modern } from '@/components/Riso_Home_Modern';
|
||||||
|
import { Riso_Home_ParteFinale } from '@/components/Riso_Home_ParteFinale';
|
||||||
import { InvitaAmico } from '@/components/InvitaAmico';
|
import { InvitaAmico } from '@/components/InvitaAmico';
|
||||||
import { CMyVideoYoutube } from '@/components/CMyVideoYoutube';
|
import { CMyVideoYoutube } from '@/components/CMyVideoYoutube';
|
||||||
import { editprofile } from '@/components/editprofile';
|
import { editprofile } from '@/components/editprofile';
|
||||||
@@ -63,7 +65,6 @@ import { CMyFieldRec } from '@/components/CMyFieldRec';
|
|||||||
import { CSelectColor } from '@/components/CSelectColor';
|
import { CSelectColor } from '@/components/CSelectColor';
|
||||||
import { CMainView } from '@/components/CMainView';
|
import { CMainView } from '@/components/CMainView';
|
||||||
import { CMyProfileTutorial } from '@/components/CMyProfileTutorial';
|
import { CMyProfileTutorial } from '@/components/CMyProfileTutorial';
|
||||||
import { CSendRISTo } from '@/components/CSendRISTo';
|
|
||||||
import { CDashboard } from '@/components/CDashboard';
|
import { CDashboard } from '@/components/CDashboard';
|
||||||
import { CDashGroup } from '@/components/CDashGroup';
|
import { CDashGroup } from '@/components/CDashGroup';
|
||||||
import { CMovements } from '@/components/CMovements';
|
import { CMovements } from '@/components/CMovements';
|
||||||
@@ -103,12 +104,14 @@ export default defineComponent({
|
|||||||
CEventsCalendar,
|
CEventsCalendar,
|
||||||
CCardCarousel,
|
CCardCarousel,
|
||||||
CProfileCompletitionBanner,
|
CProfileCompletitionBanner,
|
||||||
|
EventPosterGenerator,
|
||||||
COpenStreetMap,
|
COpenStreetMap,
|
||||||
CMyPage,
|
CMyPage,
|
||||||
CMyPageIntro,
|
CMyPageIntro,
|
||||||
InvitaAmico,
|
InvitaAmico,
|
||||||
HomeRiso,
|
HomeRiso,
|
||||||
Riso_Home_Modern,
|
Riso_Home_Modern,
|
||||||
|
Riso_Home_ParteFinale,
|
||||||
CMyEditor,
|
CMyEditor,
|
||||||
mycircuits,
|
mycircuits,
|
||||||
editprofile,
|
editprofile,
|
||||||
@@ -126,7 +129,6 @@ export default defineComponent({
|
|||||||
CCardCarouselComp,
|
CCardCarouselComp,
|
||||||
CMyActivities,
|
CMyActivities,
|
||||||
CMyProfileTutorial,
|
CMyProfileTutorial,
|
||||||
CSendRISTo,
|
|
||||||
CTitleBanner,
|
CTitleBanner,
|
||||||
CShareSocial,
|
CShareSocial,
|
||||||
CCheckAppRunning,
|
CCheckAppRunning,
|
||||||
|
|||||||
@@ -109,6 +109,12 @@
|
|||||||
>
|
>
|
||||||
<Riso_Home_Modern />
|
<Riso_Home_Modern />
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="myel.type === shared_consts.ELEMTYPE.RISOHOME_PAGFINALE"
|
||||||
|
class="myElemBase"
|
||||||
|
>
|
||||||
|
<Riso_Home_ParteFinale />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="myel.type === shared_consts.ELEMTYPE.IMGTITLE"
|
v-else-if="myel.type === shared_consts.ELEMTYPE.IMGTITLE"
|
||||||
class="myElemBase"
|
class="myElemBase"
|
||||||
@@ -134,6 +140,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="myel.type === shared_consts.ELEMTYPE.CREA_VOLANTINO"
|
||||||
|
class="myElemBase"
|
||||||
|
>
|
||||||
|
˚
|
||||||
|
<div
|
||||||
|
v-if="editOn"
|
||||||
|
class="elemEdit"
|
||||||
|
>
|
||||||
|
CREA POSTER VOLANTINI:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EventPosterGenerator></EventPosterGenerator>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="myel.type === shared_consts.ELEMTYPE.IMGPOSTER"
|
v-else-if="myel.type === shared_consts.ELEMTYPE.IMGPOSTER"
|
||||||
class="myElemBase"
|
class="myElemBase"
|
||||||
@@ -607,22 +627,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<CMovements :showbuttolastmov="true"></CMovements>
|
<CMovements :showbuttolastmov="true"></CMovements>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-else-if="myel.type === shared_consts.ELEMTYPE.CSENDRISTO"
|
|
||||||
class="myElemBase"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="editOn"
|
|
||||||
class="elemEdit"
|
|
||||||
>
|
|
||||||
Bottoni (Invia/Ricevi RIS) CSendRISTo
|
|
||||||
</div>
|
|
||||||
<CSendRISTo></CSendRISTo>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-else-if="myel.type === shared_consts.ELEMTYPE.GRID_ORIZ"
|
v-else-if="myel.type === shared_consts.ELEMTYPE.GRID_ORIZ"
|
||||||
class="myElemBase"
|
|
||||||
>
|
>
|
||||||
|
<section
|
||||||
|
class="content-section"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="myel.stiletit_str"
|
||||||
|
class="section-header"
|
||||||
|
>
|
||||||
|
<h2 class="section-title">
|
||||||
|
<q-icon
|
||||||
|
v-if="myel.stiletit_icon"
|
||||||
|
:name="myel.stiletit_icon || 'event'"
|
||||||
|
/>
|
||||||
|
{{ myel.stiletit_str }}
|
||||||
|
</h2>
|
||||||
|
<q-btn
|
||||||
|
v-if="myel.container2"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
round
|
||||||
|
icon="arrow_forward"
|
||||||
|
@click="naviga(myel.container2)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="editOn"
|
v-if="editOn"
|
||||||
class="elemEdit"
|
class="elemEdit"
|
||||||
@@ -637,7 +668,9 @@
|
|||||||
:showMap="myel.parambool3"
|
:showMap="myel.parambool3"
|
||||||
:heightcarousel="myel.heightcarousel"
|
:heightcarousel="myel.heightcarousel"
|
||||||
:prop_modif="myel.parambool4"
|
:prop_modif="myel.parambool4"
|
||||||
|
:prop_compatto="myel.parambool5"
|
||||||
></CGridOriz>
|
></CGridOriz>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="myel.type === shared_consts.ELEMTYPE.SEARCHPRODUCT"
|
v-else-if="myel.type === shared_consts.ELEMTYPE.SEARCHPRODUCT"
|
||||||
@@ -700,6 +733,7 @@
|
|||||||
:showMap="false"
|
:showMap="false"
|
||||||
:prop_modif="tools.isAdmin()"
|
:prop_modif="tools.isAdmin()"
|
||||||
:enableExport="true"
|
:enableExport="true"
|
||||||
|
:prop_compatto="myel.number === -1"
|
||||||
></CGridOriz>
|
></CGridOriz>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
@@ -783,6 +817,7 @@
|
|||||||
:showMap="false"
|
:showMap="false"
|
||||||
:prop_modif="tools.isAdmin()"
|
:prop_modif="tools.isAdmin()"
|
||||||
:enableExport="true"
|
:enableExport="true"
|
||||||
|
:prop_compatto="myel.number === -1"
|
||||||
></CGridOriz>
|
></CGridOriz>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
.rec-card-wrapper {
|
.rec-card-wrapper {
|
||||||
margin: 3px auto;
|
margin: 3px auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%; // Aggiungi questa riga
|
width: 100%;
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
margin: 2px auto;
|
margin: 2px auto;
|
||||||
@@ -12,19 +12,55 @@
|
|||||||
|
|
||||||
&.is-even {
|
&.is-even {
|
||||||
.modern-rec-card {
|
.modern-rec-card {
|
||||||
background: linear-gradient(135deg, rgba(49, 153, 239, 0.249) 0%, rgba(25, 118, 210, 0.03) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(49, 153, 239, 0.15) 0%,
|
||||||
|
rgba(25, 118, 210, 0.03) 100%
|
||||||
|
);
|
||||||
border-color: rgba(66, 165, 245, 0.12);
|
border-color: rgba(66, 165, 245, 0.12);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-odd {
|
&.is-odd {
|
||||||
.modern-rec-card {
|
.modern-rec-card {
|
||||||
background: linear-gradient(135deg, rgba(38, 197, 218, 0.344) 0%, rgba(0, 150, 136, 0.03) 100%);
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(38, 197, 218, 0.2) 0%,
|
||||||
|
rgba(0, 150, 136, 0.03) 100%
|
||||||
|
);
|
||||||
border-color: rgba(38, 198, 218, 0.12);
|
border-color: rgba(38, 198, 218, 0.12);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dark mode alternating colors
|
||||||
|
body.body--dark,
|
||||||
|
.body--dark {
|
||||||
|
.rec-card-wrapper {
|
||||||
|
&.is-even {
|
||||||
|
.modern-rec-card {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(49, 153, 239, 0.2) 0%,
|
||||||
|
rgba(25, 118, 210, 0.08) 100%
|
||||||
|
);
|
||||||
|
border-color: rgba(66, 165, 245, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-odd {
|
||||||
|
.modern-rec-card {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(38, 197, 218, 0.25) 0%,
|
||||||
|
rgba(0, 150, 136, 0.08) 100%
|
||||||
|
);
|
||||||
|
border-color: rgba(38, 198, 218, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// CARD EVENTO (Calendar View)
|
// CARD EVENTO (Calendar View)
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -32,14 +68,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
background: white;
|
background: var(--app-bg-card);
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: $shadow-md;
|
box-shadow: var(--app-shadow-md);
|
||||||
transition: box-shadow $transition-speed ease;
|
transition: box-shadow $transition-speed ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: $shadow-hover;
|
box-shadow: var(--app-shadow-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
@@ -135,7 +171,7 @@
|
|||||||
.user-avatar {
|
.user-avatar {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
box-shadow: $shadow-sm;
|
box-shadow: var(--app-shadow-sm);
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
@@ -148,8 +184,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
// L'altezza viene gestita dinamicamente dal Vue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-image {
|
.event-image {
|
||||||
@@ -171,10 +205,10 @@
|
|||||||
// CARD RECORD PRINCIPALE
|
// CARD RECORD PRINCIPALE
|
||||||
// ========================================
|
// ========================================
|
||||||
.modern-rec-card {
|
.modern-rec-card {
|
||||||
background: white;
|
background: var(--app-bg-card);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
border: 1px solid var(--app-border-color);
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
box-shadow: $shadow-sm;
|
box-shadow: var(--app-shadow-sm);
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
transition: all $transition-speed ease;
|
transition: all $transition-speed ease;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
@@ -182,7 +216,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: $shadow-md;
|
box-shadow: var(--app-shadow-md);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,8 +256,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.record-avatar {
|
.record-avatar {
|
||||||
box-shadow: $shadow-sm;
|
box-shadow: var(--app-shadow-sm);
|
||||||
border: 2px solid rgba(0, 0, 0, 0.05);
|
border: 2px solid var(--app-border-color);
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
width: 48px !important;
|
width: 48px !important;
|
||||||
@@ -269,22 +303,20 @@
|
|||||||
.tag-chip {
|
.tag-chip {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 0 px;
|
padding: 0 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
// Categoria principale - più scura e intensa
|
|
||||||
&.sector {
|
&.sector {
|
||||||
background: linear-gradient(135deg, $primary-color, #1976d2);
|
background: linear-gradient(135deg, $primary-color, #1976d2);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sottocategoria - stessa base ma più chiara
|
|
||||||
&.subsector {
|
&.subsector {
|
||||||
background: linear-gradient(135deg, color.adjust($primary-color, $lightness: 10%), #42a5f5);
|
background: linear-gradient(135deg, $primary-light, #42a5f5);
|
||||||
color: white;
|
color: white;
|
||||||
opacity: 0.9; // Opzionale: leggera trasparenza
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
@@ -315,7 +347,7 @@
|
|||||||
|
|
||||||
.description-text {
|
.description-text {
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
color: #2c3e50;
|
color: var(--app-text-primary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin: 3px 0;
|
margin: 3px 0;
|
||||||
@@ -323,7 +355,6 @@
|
|||||||
|
|
||||||
&.event-title {
|
&.event-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1a1a1a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
@@ -333,7 +364,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// PREFERENCES ROW (dopo descrizione)
|
// PREFERENCES ROW
|
||||||
// ========================================
|
// ========================================
|
||||||
.preferences-row {
|
.preferences-row {
|
||||||
margin: 8px 0 6px 0;
|
margin: 8px 0 6px 0;
|
||||||
@@ -357,11 +388,11 @@
|
|||||||
.preference-icon-avatar {
|
.preference-icon-avatar {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--app-shadow-sm);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-2px) scale(1.08);
|
transform: translateY(-2px) scale(1.08);
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25);
|
box-shadow: var(--app-shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
@@ -384,11 +415,11 @@
|
|||||||
.user-name {
|
.user-name {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: $mainColor;
|
color: var(--app-text-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
||||||
@media (max-width: $mobile) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,8 +429,9 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
color: var(--app-text-secondary);
|
||||||
|
|
||||||
@media (max-width: $mobile) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
@@ -434,8 +466,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dark mode attending badge
|
||||||
|
body.body--dark .attending-badge,
|
||||||
|
.body--dark .attending-badge {
|
||||||
|
background: rgba(33, 186, 69, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.cities-text {
|
.cities-text {
|
||||||
color: rgba(44, 62, 80, 0.7); // Grigio bluastro più caldo del grigio puro
|
color: var(--app-text-secondary);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -443,12 +481,13 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
color: rgba($mainColor, 0.6); // Icona location con colore primario attenuato
|
color: var(--app-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cities-text-bold {
|
.cities-text-bold {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
color: var(--app-text-primary);
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -484,11 +523,11 @@
|
|||||||
.action-menu-btn {
|
.action-menu-btn {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
color: $grey-color;
|
color: var(--app-text-muted);
|
||||||
transition: all $transition-speed ease;
|
transition: all $transition-speed ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: var(--app-border-color);
|
||||||
color: $primary-color;
|
color: $primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,15 +550,15 @@
|
|||||||
white-space: wrap;
|
white-space: wrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
color: $grey-color;
|
color: var(--app-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardrec {
|
.cardrec {
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
border: 1px solid var(--app-border-color);
|
||||||
box-shadow: $shadow-sm;
|
box-shadow: var(--app-shadow-sm);
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
background-color: white;
|
background-color: var(--app-bg-card);
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
|
|
||||||
@media (min-width: 500px) {
|
@media (min-width: 500px) {
|
||||||
@@ -532,9 +571,16 @@
|
|||||||
color: $primary-color;
|
color: $primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dark mode text_title
|
||||||
|
body.body--dark .text_title,
|
||||||
|
.body--dark .text_title {
|
||||||
|
color: $primary-light;
|
||||||
|
}
|
||||||
|
|
||||||
.event_date {
|
.event_date {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
|
color: var(--app-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -543,12 +589,16 @@
|
|||||||
.q-separator {
|
.q-separator {
|
||||||
margin: 3px 0;
|
margin: 3px 0;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
background: var(--app-divider-color);
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CATEGORY HIERARCHY
|
||||||
|
// ========================================
|
||||||
.category-hierarchy {
|
.category-hierarchy {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -558,23 +608,28 @@
|
|||||||
.hierarchy-arrow {
|
.hierarchy-arrow {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
color: var(--app-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $mobile-breakpoint) {
|
@media (max-width: $mobile-breakpoint) {
|
||||||
gap: 0px;
|
gap: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CATEGORIES DIALOG
|
||||||
|
// ========================================
|
||||||
.categories-dialog {
|
.categories-dialog {
|
||||||
.dialog-header {
|
.dialog-header {
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
|
background: var(--app-bg-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-content {
|
.dialog-content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
background: var(--app-bg-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
.categories-grid {
|
.categories-grid {
|
||||||
@@ -590,9 +645,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// ANNUNCIO LOCATION
|
||||||
|
// ========================================
|
||||||
.annuncio-location {
|
.annuncio-location {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: #718096;
|
color: var(--app-text-secondary);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
||||||
@@ -601,35 +659,148 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Il contenitore padre (q-item o card) deve avere position relative
|
// ========================================
|
||||||
|
// OVERLAY BUTTONS
|
||||||
|
// ========================================
|
||||||
.q-item,
|
.q-item,
|
||||||
.event-card {
|
.event-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottone overlay fisso a destra
|
|
||||||
.action-menu-btn-overlay {
|
.action-menu-btn-overlay {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
background: var(--app-bg-card) !important;
|
||||||
// Sfondo semi-trasparente per visibilità
|
box-shadow: var(--app-shadow-sm);
|
||||||
background: rgba(255, 255, 255, 0.8) !important;
|
transition: all $transition-speed ease;
|
||||||
|
|
||||||
// Ombra leggera
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 1) !important;
|
background: var(--app-bg-card-hover) !important;
|
||||||
|
box-shadow: var(--app-shadow-md);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alternativa: in alto a destra
|
|
||||||
.action-menu-btn-overlay--top {
|
.action-menu-btn-overlay--top {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
background: var(--app-bg-card) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// DARK MODE SPECIFIC OVERRIDES
|
||||||
|
// ========================================
|
||||||
|
body.body--dark,
|
||||||
|
.body--dark {
|
||||||
|
// Event date column - leggermente più scuro in dark mode
|
||||||
|
.event-date-column {
|
||||||
|
background: linear-gradient(135deg, #1565c0, #1976d2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag chips
|
||||||
|
.tag-chip {
|
||||||
|
&.sector {
|
||||||
|
background: linear-gradient(135deg, #1565c0, #1976d2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.subsector {
|
||||||
|
background: linear-gradient(135deg, #1976d2, #2196f3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time badges nel calendar
|
||||||
|
.time-start,
|
||||||
|
.time-end {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User avatar border
|
||||||
|
.user-avatar {
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record avatar
|
||||||
|
.record-avatar {
|
||||||
|
border-color: var(--app-border-color-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preference icon avatar
|
||||||
|
.preference-icon-avatar {
|
||||||
|
&:hover {
|
||||||
|
box-shadow: var(--app-shadow-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action menu button hover
|
||||||
|
.action-menu-btn {
|
||||||
|
&:hover {
|
||||||
|
background: var(--app-border-color-strong);
|
||||||
|
color: $primary-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// EVENT CARD - Theme Aware
|
||||||
|
// ========================================
|
||||||
|
.event-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $s-md;
|
||||||
|
|
||||||
|
// Background con gradiente viola - usa opacità che funziona in entrambi i temi
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(102, 126, 234, 0.08) 0%,
|
||||||
|
rgba(118, 75, 162, 0.08) 100%
|
||||||
|
);
|
||||||
|
border: 2px solid rgba(102, 126, 234, 0.2);
|
||||||
|
border-radius: $r-md;
|
||||||
|
padding: $s-md;
|
||||||
|
min-width: 280px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2);
|
||||||
|
border-color: rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// EVENT DATE BADGE
|
||||||
|
// ========================================
|
||||||
|
.event-date {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: $gradient-primary;
|
||||||
|
color: white;
|
||||||
|
border-radius: $r-sm;
|
||||||
|
padding: $s-sm;
|
||||||
|
min-width: 50px;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
// Posizionamento assoluto (dalla seconda definizione)
|
||||||
|
position: absolute;
|
||||||
|
top: $s-sm;
|
||||||
|
left: $s-sm;
|
||||||
|
|
||||||
|
.event-day {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-month {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,11 @@ export default defineComponent({
|
|||||||
required: false,
|
required: false,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
compatto: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
|
|||||||
@@ -65,7 +65,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card Record Principale -->
|
<!-- Card Record Principale -->
|
||||||
<q-item
|
<q-item
|
||||||
v-if="myrec"
|
v-if="myrec"
|
||||||
@@ -343,7 +342,11 @@
|
|||||||
class="annuncio-location"
|
class="annuncio-location"
|
||||||
>
|
>
|
||||||
<span v-if="ind > 0"></span>
|
<span v-if="ind > 0"></span>
|
||||||
<span v-if="table === shared_consts.TABLES_MYHOSPS" class="cities-text-bold">{{ rec.comune }} ({{ rec.prov }})</span>
|
<span
|
||||||
|
v-if="table === shared_consts.TABLES_MYHOSPS"
|
||||||
|
class="cities-text-bold"
|
||||||
|
>{{ rec.comune }} ({{ rec.prov }})</span
|
||||||
|
>
|
||||||
<span v-else>{{ rec.comune }} ({{ rec.prov }})</span>
|
<span v-else>{{ rec.comune }} ({{ rec.prov }})</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
759
src/components/CMyRecEventi/CMyRecEventi.scss
Normal file
@@ -0,0 +1,759 @@
|
|||||||
|
// ========================================
|
||||||
|
// WRAPPER CARD - ALTERNATING COLORS
|
||||||
|
// ========================================
|
||||||
|
.rec-card-wrapper {
|
||||||
|
margin: 3px auto;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%; // Aggiungi questa riga
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
margin: 2px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-even {
|
||||||
|
.modern-rec-card {
|
||||||
|
background: linear-gradient(135deg, rgba(49, 153, 239, 0.249) 0%, rgba(25, 118, 210, 0.03) 100%);
|
||||||
|
border-color: rgba(66, 165, 245, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-odd {
|
||||||
|
.modern-rec-card {
|
||||||
|
background: linear-gradient(135deg, rgba(38, 197, 218, 0.344) 0%, rgba(0, 150, 136, 0.03) 100%);
|
||||||
|
border-color: rgba(38, 198, 218, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CARD EVENTO (Calendar View)
|
||||||
|
// ========================================
|
||||||
|
.event-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
background: white;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: $shadow-md;
|
||||||
|
transition: box-shadow $transition-speed ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: $shadow-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-date-column {
|
||||||
|
flex: 0 0 70px;
|
||||||
|
background: linear-gradient(135deg, $primary-color, #42a5f5);
|
||||||
|
padding: 6px 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
flex: 0 0 65px;
|
||||||
|
padding: 5px 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-of-week {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
opacity: 0.9;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
font-size: 0.5625rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
margin: 3px 0;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
font-size: 1.625rem;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-name {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-start,
|
||||||
|
.time-end {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 2px 5px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
margin: 3px 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
margin-top: 6px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
margin-top: 5px;
|
||||||
|
width: 36px !important;
|
||||||
|
height: 36px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-image-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// L'altezza viene gestita dinamicamente dal Vue
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform $transition-speed ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.q-img__image) {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CARD RECORD PRINCIPALE
|
||||||
|
// ========================================
|
||||||
|
.modern-rec-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: $border-radius;
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
padding: 6px;
|
||||||
|
transition: all $transition-speed ease;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: $shadow-md;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
padding: 5px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// AVATAR SECTION
|
||||||
|
// ========================================
|
||||||
|
.avatar-section {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin-right: 6px;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
left: -3px;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: 0.625rem;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
font-size: 0.5625rem;
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-avatar {
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
width: 48px !important;
|
||||||
|
height: 48px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.q-img) {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CONTENT SECTION
|
||||||
|
// ========================================
|
||||||
|
.content-section {
|
||||||
|
padding: 0 6px 0 0 !important;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
padding: 0 5px 0 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 3px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
gap: $s-xs;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
gap: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip {
|
||||||
|
height: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0 px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: none;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
// Categoria principale - più scura e intensa
|
||||||
|
&.sector {
|
||||||
|
background: linear-gradient(135deg, $primary-color, #1976d2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sottocategoria - stessa base ma più chiara
|
||||||
|
&.subsector {
|
||||||
|
background: linear-gradient(135deg, color.adjust($primary-color, $lightness: 10%), #42a5f5);
|
||||||
|
color: white;
|
||||||
|
opacity: 0.9; // Opzionale: leggera trasparenza
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
height: 19px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.q-chip__content) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
padding: 2px 4px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: $text-secondary; // Era #2c3e50
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 3px 0;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: $text-secondary-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.event-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-primary; // Era #1a1a1a
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: $text-primary-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ========================================
|
||||||
|
// PREFERENCES ROW (dopo descrizione)
|
||||||
|
// ========================================
|
||||||
|
.preferences-row {
|
||||||
|
margin: 8px 0 6px 0;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
margin: 6px 0 4px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences-icons-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preference-icon-avatar {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px) scale(1.08);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
width: 28px !important;
|
||||||
|
height: 28px !important;
|
||||||
|
|
||||||
|
.q-icon {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: $s-sm;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
color: $mainColor;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
@media (max-width: $mobile) {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-container {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
|
||||||
|
@media (max-width: $mobile) {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
.q-icon {
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attending-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
color: $positive-color;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
margin: 3px 0;
|
||||||
|
padding: 3px 6px;
|
||||||
|
background: rgba(33, 186, 69, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 2px 5px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cities-text {
|
||||||
|
color: rgba(44, 62, 80, 0.7); // Grigio bluastro più caldo del grigio puro
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
flex-flow: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: rgba($mainColor, 0.6); // Icona location con colore primario attenuato
|
||||||
|
}
|
||||||
|
|
||||||
|
.cities-text-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cities-contrib-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// ACTIONS SECTION
|
||||||
|
// ========================================
|
||||||
|
.actions-section {
|
||||||
|
padding: 0 !important;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-left: 3px;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-menu-btn {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
color: $grey-color;
|
||||||
|
transition: all $transition-speed ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// LEGACY SUPPORT
|
||||||
|
// ========================================
|
||||||
|
.myflex {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text_user_city {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: wrap;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: $grey-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardrec {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
box-shadow: $shadow-sm;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: white;
|
||||||
|
margin: 3px;
|
||||||
|
|
||||||
|
@media (min-width: 500px) {
|
||||||
|
margin: 2px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text_title {
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event_date {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// SEPARATOR
|
||||||
|
// ========================================
|
||||||
|
.q-separator {
|
||||||
|
margin: 3px 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-hierarchy {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
.hierarchy-arrow {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-breakpoint) {
|
||||||
|
gap: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dialog {
|
||||||
|
.dialog-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-chip {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.annuncio-location {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #718096;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '📍';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Il contenitore padre (q-item o card) deve avere position relative
|
||||||
|
.q-item,
|
||||||
|
.event-card {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottone overlay fisso a destra
|
||||||
|
.action-menu-btn-overlay {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 8px;
|
||||||
|
right: 0px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
// Sfondo semi-trasparente per visibilità
|
||||||
|
background: rgba(255, 255, 255, 0.8) !important;
|
||||||
|
|
||||||
|
// Ombra leggera
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternativa: in alto a destra
|
||||||
|
.action-menu-btn-overlay--top {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// EVENT CARD
|
||||||
|
// ==========================================
|
||||||
|
.event-card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $s-md;
|
||||||
|
background: $gradient-hover;
|
||||||
|
border: 2px solid rgba($primary-color, 0.2);
|
||||||
|
border-radius: $r-md;
|
||||||
|
padding: $s-md;
|
||||||
|
min-width: 280px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
background: $gradient-hover-dark;
|
||||||
|
border-color: rgba($primary-color-dark, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover state
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 16px rgba($primary-color, 0.2);
|
||||||
|
border-color: rgba($primary-color, 0.4);
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
box-shadow: 0 4px 16px rgba($primary-color-dark, 0.3);
|
||||||
|
border-color: rgba($primary-color-dark, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== EVENT DATE BADGE ==========
|
||||||
|
.event-date {
|
||||||
|
position: absolute;
|
||||||
|
top: $s-sm;
|
||||||
|
left: $s-sm;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: $gradient-primary;
|
||||||
|
color: $text-on-primary;
|
||||||
|
border-radius: $r-sm;
|
||||||
|
padding: $s-sm;
|
||||||
|
min-width: 50px;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
background: $gradient-primary-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-day {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-month {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== EVENT INFO ==========
|
||||||
|
.event-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-primary;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: $text-primary-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-location {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: $text-muted;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
color: $text-muted-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== EVENT IMAGE ==========
|
||||||
|
.event-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: $r-sm;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-image-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: $gradient-placeholder;
|
||||||
|
|
||||||
|
.body--dark & {
|
||||||
|
background: $gradient-placeholder-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
src/components/CMyRecEventi/CMyRecEventi.ts
Executable file
@@ -0,0 +1,198 @@
|
|||||||
|
import type { PropType } from 'vue';
|
||||||
|
import { defineComponent, onMounted, ref, watch, computed } from 'vue';
|
||||||
|
import { useUserStore } from '@store/UserStore';
|
||||||
|
import type { IUserFields } from 'model';
|
||||||
|
import { IImgGallery, IUserProfile } from 'model';
|
||||||
|
import { costanti } from '@costanti';
|
||||||
|
import { shared_consts } from '@/common/shared_vuejs';
|
||||||
|
import { fieldsTable } from '@store/Modules/fieldsTable';
|
||||||
|
import { tools } from '@tools';
|
||||||
|
import { toolsext } from '@store/Modules/toolsext';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { CMyCardPopup } from '@/components/CMyCardPopup';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useCalendarStore } from '@/store/CalendarStore';
|
||||||
|
import { useGlobalStore } from '@/store/globalStore';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CMyRecEventi',
|
||||||
|
components: { CMyCardPopup },
|
||||||
|
emits: ['setCmd', 'cmdext'],
|
||||||
|
props: {
|
||||||
|
table: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
prop_myrec: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
indexRow: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
editOn: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
margin_right: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
compatto: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const calendarStore = useCalendarStore();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const $q = useQuasar();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const $router = useRouter();
|
||||||
|
|
||||||
|
const myrec = ref(<any>null);
|
||||||
|
const showPreferencesDialog = ref(false);
|
||||||
|
|
||||||
|
const visupage = ref(false);
|
||||||
|
const disabilita = computed(() => {
|
||||||
|
return props.table === shared_consts.TABLES_MYBACHECAS;
|
||||||
|
});
|
||||||
|
const arrSubSector = computed(() => {
|
||||||
|
return tools.getArrSubSector(props.table, myrec.value);
|
||||||
|
});
|
||||||
|
const arrSector = computed(() => {
|
||||||
|
return tools.getArrSector(props.table, myrec.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getColorSubSector = computed(() => {
|
||||||
|
return arrSector.value && arrSector.value.length == 1 ? arrSector.value[0].color : 'primary'
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.prop_myrec,
|
||||||
|
(newval, oldval) => {
|
||||||
|
mounted();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function mounted() {
|
||||||
|
if (props.prop_myrec) {
|
||||||
|
myrec.value = props.prop_myrec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBadge() {
|
||||||
|
if (shared_consts.TABLES_SHOW_ADTYPE.includes(props.table)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImgUser(profile: IUserFields) {
|
||||||
|
return userStore.getImgByProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function naviga(path: string) {
|
||||||
|
$router.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCmd(
|
||||||
|
$q: any,
|
||||||
|
cmd: number,
|
||||||
|
myusername: string,
|
||||||
|
value: any,
|
||||||
|
groupname: string
|
||||||
|
) {
|
||||||
|
emit('setCmd', $q, cmd, myusername, value, groupname);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmdExt(cmd: any, val1: any, val2: any) {
|
||||||
|
emit('cmdext', cmd, val1, val2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigaExt(obj: any) {
|
||||||
|
cmdExt(costanti.CMD_SHOW_PAGE, null, obj);
|
||||||
|
//let link = shared_consts.getDirectoryByTable(props.table) + '/' + obj._id
|
||||||
|
//console.log('link', link)
|
||||||
|
//$router.push(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNameToShow(user: IUserFields, col = null) {
|
||||||
|
if (myrec.value.groupname) return myrec.value.groupname;
|
||||||
|
else return userStore.getNameToShow(user, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPartecipero() {
|
||||||
|
return (
|
||||||
|
props.table === shared_consts.TABLES_MYBACHECAS &&
|
||||||
|
calendarStore.isPartecipero(myrec.value._id, props.table)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computedWidth() {
|
||||||
|
//const width = tools.getwidth($q) - 20;
|
||||||
|
//return `${Math.min(width, 600)}px`; // Limita la larghezza massima a 600px
|
||||||
|
|
||||||
|
const width = tools.getwidth($q);
|
||||||
|
// Limita la larghezza per evitare overflow
|
||||||
|
return `${Math.min(width - 40, 600)}px`;
|
||||||
|
|
||||||
|
//return '100%'; // Rimuovi la limitazione a 600px
|
||||||
|
}
|
||||||
|
|
||||||
|
function computedEventImageHeight() {
|
||||||
|
const width = tools.getwidth($q);
|
||||||
|
const isMobile = width < 768;
|
||||||
|
const cardWidth = Math.min(width - 20, 600);
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
// Mobile: aspect ratio 16:9, minimo 200px
|
||||||
|
const calculatedHeight = Math.round((cardWidth - 70) * 0.5625);
|
||||||
|
return `${Math.max(calculatedHeight, 200)}px`;
|
||||||
|
} else {
|
||||||
|
// Desktop: altezza fissa ottimale 220px
|
||||||
|
return '220px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(mounted);
|
||||||
|
|
||||||
|
return {
|
||||||
|
t,
|
||||||
|
myrec,
|
||||||
|
costanti,
|
||||||
|
getImgUser,
|
||||||
|
naviga,
|
||||||
|
navigaExt,
|
||||||
|
setCmd,
|
||||||
|
shared_consts,
|
||||||
|
userStore,
|
||||||
|
tools,
|
||||||
|
toolsext,
|
||||||
|
fieldsTable,
|
||||||
|
cmdExt,
|
||||||
|
visupage,
|
||||||
|
showBadge,
|
||||||
|
getNameToShow,
|
||||||
|
isPartecipero,
|
||||||
|
calendarStore,
|
||||||
|
disabilita,
|
||||||
|
globalStore,
|
||||||
|
computedWidth,
|
||||||
|
computedEventImageHeight,
|
||||||
|
arrSubSector,
|
||||||
|
arrSector,
|
||||||
|
getColorSubSector,
|
||||||
|
showPreferencesDialog,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
104
src/components/CMyRecEventi/CMyRecEventi.vue
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="myrec"
|
||||||
|
class="event-card"
|
||||||
|
@click="navigaExt(myrec)"
|
||||||
|
>
|
||||||
|
<q-img
|
||||||
|
:src="
|
||||||
|
myrec.photos
|
||||||
|
? tools.getFullFileName(myrec.photos, table, myrec.username, '')
|
||||||
|
: '/images/event-placeholder.png'
|
||||||
|
"
|
||||||
|
:alt="myrec.descr"
|
||||||
|
class="event-image"
|
||||||
|
@click="cmdExt(costanti.CMD_SHOW_PAGE, null, myrec)"
|
||||||
|
>
|
||||||
|
<template v-slot:error>
|
||||||
|
<div class="event-image-placeholder">
|
||||||
|
<q-icon
|
||||||
|
name="event"
|
||||||
|
size="2rem"
|
||||||
|
color="grey-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-img>
|
||||||
|
<div class="event-date">
|
||||||
|
<span class="event-day">{{ tools.getstrDay(myrec.dateTimeStart) }}</span>
|
||||||
|
<span class="event-month">{{
|
||||||
|
tools.getstrMonth3Letters(myrec.dateTimeStart)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-info">
|
||||||
|
<span class="event-title">{{ myrec.descr }}</span>
|
||||||
|
<span class="event-location">
|
||||||
|
<div class="cities-text">
|
||||||
|
<span
|
||||||
|
v-for="(rec, ind) of myrec.mycities"
|
||||||
|
:key="ind"
|
||||||
|
class="annuncio-location"
|
||||||
|
>
|
||||||
|
<span v-if="ind > 0"></span>
|
||||||
|
<span
|
||||||
|
v-if="table === shared_consts.TABLES_MYHOSPS"
|
||||||
|
class="cities-text-bold"
|
||||||
|
>{{ rec.comune }} ({{ rec.prov }})</span
|
||||||
|
>
|
||||||
|
<span v-else>{{ rec.comune }} ({{ rec.prov }})</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<q-icon name="chevron_right" size="lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Categories Dialog -->
|
||||||
|
<q-dialog v-model="showPreferencesDialog">
|
||||||
|
<q-card
|
||||||
|
class="categories-dialog"
|
||||||
|
:style="{ minWidth: $q.screen.lt.sm ? '90vw' : '400px' }"
|
||||||
|
>
|
||||||
|
<q-card-section class="dialog-header">
|
||||||
|
<div class="text-h6">Categorie</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-section class="dialog-content">
|
||||||
|
<div class="categories-grid">
|
||||||
|
<q-chip
|
||||||
|
v-for="(rec, ind) of myrec.preferences"
|
||||||
|
:key="ind"
|
||||||
|
class="category-chip shadow-3"
|
||||||
|
:style="`background-color: ${tools.getPreferenceById(rec).color}; opacity: 0.85; color: white;`"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="tools.getPreferenceById(rec).icon"
|
||||||
|
left
|
||||||
|
size="18px"
|
||||||
|
/>
|
||||||
|
{{ tools.getPreferenceById(rec).label }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
label="Chiudi"
|
||||||
|
color="primary"
|
||||||
|
v-close-popup
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" src="./CMyRecEventi.ts"></script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import './CMyRecEventi.scss';
|
||||||
|
</style>
|
||||||
1
src/components/CMyRecEventi/index.ts
Executable file
@@ -0,0 +1 @@
|
|||||||
|
export { default as CMyRecEventi } from './CMyRecEventi.vue'
|
||||||
@@ -367,7 +367,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
console.log('update', props.value, props);
|
|
||||||
// console.log(' #### mounted myselect', props.options, 'arrvalue', myarrvalue.value)
|
// console.log(' #### mounted myselect', props.options, 'arrvalue', myarrvalue.value)
|
||||||
let rec: any;
|
let rec: any;
|
||||||
if (optionsreal.value) {
|
if (optionsreal.value) {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const username = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import 'src/css/variables.scss';
|
|
||||||
|
|
||||||
.user-not-found-container {
|
.user-not-found-container {
|
||||||
min-height: calc(100vh - #{$headerHeight});
|
min-height: calc(100vh - #{$headerHeight});
|
||||||
|
|||||||
@@ -1,17 +1,357 @@
|
|||||||
|
.qrcode-container {
|
||||||
.button_download{
|
width: 100%;
|
||||||
display: inline-block !important;
|
display: flex;
|
||||||
padding: 10px 20px !important;
|
flex-direction: column;
|
||||||
font-size: 16px !important;
|
align-items: center;
|
||||||
font-weight: 500;
|
padding: 16px;
|
||||||
color: #ffffff !important;
|
}
|
||||||
background-color: #027be3 !important;
|
|
||||||
/* Colore primario di Quasar */
|
// ============================================
|
||||||
border: none !important;
|
// QR READER SECTION
|
||||||
border-radius: 4px !important;
|
// ============================================
|
||||||
cursor: pointer;
|
.qr-reader-section {
|
||||||
text-align: center;
|
width: 100%;
|
||||||
text-decoration: none;
|
max-width: 400px;
|
||||||
transition: background-color 0.3s, box-shadow 0.3s;
|
}
|
||||||
|
|
||||||
|
.stream-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-stream {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
:deep(video) {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-frame {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.corner {
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-color: #4caf50;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
|
||||||
|
&.top-left {
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
border-top-width: 4px;
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top-right {
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
border-top-width: 4px;
|
||||||
|
border-right-width: 4px;
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom-left {
|
||||||
|
bottom: -2px;
|
||||||
|
left: -2px;
|
||||||
|
border-bottom-width: 4px;
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-bottom-left-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom-right {
|
||||||
|
bottom: -2px;
|
||||||
|
right: -2px;
|
||||||
|
border-bottom-width: 4px;
|
||||||
|
border-right-width: 4px;
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animazione scan line
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
transparent,
|
||||||
|
#4caf50,
|
||||||
|
transparent);
|
||||||
|
animation: scan 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scan {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
top: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
top: 100%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
.upload-input {
|
||||||
|
:deep(.q-field__control) {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-result {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.result-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// QR GENERATOR SECTION
|
||||||
|
// ============================================
|
||||||
|
.qr-generator-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-info {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
.q-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #027be3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-display {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px rgba(0, 0, 0, 0.05),
|
||||||
|
0 10px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
:deep(canvas) {
|
||||||
|
display: block;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-logo {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-section {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: none;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
:deep(.q-btn__content) {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-section {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.share-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: none;
|
||||||
|
border-width: 2px;
|
||||||
|
|
||||||
|
:deep(.q-btn__content) {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// LOADING STATE
|
||||||
|
// ============================================
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48px;
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ANIMATIONS
|
||||||
|
// ============================================
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// RESPONSIVE
|
||||||
|
// ============================================
|
||||||
|
@media (max-width: 599px) {
|
||||||
|
.qrcode-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-display {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-info {
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-link {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// DARK MODE (opzionale)
|
||||||
|
// ============================================
|
||||||
|
.body--dark {
|
||||||
|
.qrcode-container {
|
||||||
|
.link-info {
|
||||||
|
background: #1e1e1e;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-link {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-display {
|
||||||
|
background: #fff; // QR sempre su sfondo bianco per leggibilità
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
background: linear-gradient(135deg, #1b5e20 0%, #2e7d32 100%);
|
||||||
|
|
||||||
|
.result-text {
|
||||||
|
.result-label {
|
||||||
|
color: #c8e6c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-value {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
background: #2d2d2d;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,38 @@
|
|||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
provide, defineComponent, onBeforeMount, onBeforeUnmount, onMounted, ref, toRef, toRefs, watch, reactive
|
defineComponent,
|
||||||
} from 'vue'
|
onMounted,
|
||||||
|
ref,
|
||||||
|
toRefs,
|
||||||
|
reactive,
|
||||||
|
nextTick,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import { tools } from '@tools'
|
import { tools } from '@tools';
|
||||||
import { costanti } from '@costanti'
|
import { costanti } from '@costanti';
|
||||||
import { useGlobalStore } from '@store/globalStore'
|
import { useGlobalStore } from '@store/globalStore';
|
||||||
import { useUserStore } from '@store/UserStore'
|
import { useUserStore } from '@store/UserStore';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { toolsext } from '@store/Modules/toolsext';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { shared_consts } from '@/common/shared_vuejs'
|
// Import per lettura QR
|
||||||
import { useI18n } from 'vue-i18n'
|
import { QrStream, QrCapture, QrDropzone } from 'vue3-qr-reader';
|
||||||
import { toolsext } from '@store/Modules/toolsext'
|
|
||||||
import { useQuasar } from 'quasar'
|
|
||||||
|
|
||||||
import { QrStream, QrCapture, QrDropzone } from 'vue3-qr-reader'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
|
// Import per generazione QR
|
||||||
|
import QRCodeVue3 from 'qrcode-vue3';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'CQRCode',
|
name: 'CQRCode',
|
||||||
emits: [''],
|
|
||||||
|
components: {
|
||||||
|
QrStream,
|
||||||
|
QrCapture,
|
||||||
|
QrDropzone,
|
||||||
|
QRCodeVue3,
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
link: {
|
link: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -41,51 +54,361 @@ export default defineComponent({
|
|||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 250,
|
||||||
|
},
|
||||||
|
primaryColor: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '#26249a',
|
||||||
},
|
},
|
||||||
components: {
|
|
||||||
QrStream,
|
|
||||||
QrCapture,
|
|
||||||
QrDropzone
|
|
||||||
},
|
},
|
||||||
setup(props, { attrs, slots, emit }) {
|
|
||||||
const { t } = useI18n()
|
|
||||||
const $q = useQuasar()
|
|
||||||
const globalStore = useGlobalStore()
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const $router = useRouter()
|
|
||||||
|
|
||||||
|
emits: ['decoded', 'error'],
|
||||||
|
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const $q = useQuasar();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const $router = useRouter();
|
||||||
|
|
||||||
|
const qrDisplayRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
// State
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
data: null
|
data: null as string | null,
|
||||||
})
|
isDownloading: false,
|
||||||
|
uploadedFile: null as File | null,
|
||||||
|
});
|
||||||
|
|
||||||
function onDecode(data: any) {
|
// Computed
|
||||||
if (data)
|
const qrSize = computed(() => {
|
||||||
state.data = data
|
// Responsive size
|
||||||
|
if ($q.screen.lt.sm) {
|
||||||
|
return Math.min(props.size, window.innerWidth - 80);
|
||||||
|
}
|
||||||
|
return props.size;
|
||||||
|
});
|
||||||
|
|
||||||
|
const logoImage = computed(() => {
|
||||||
|
return props.imglogo || tools.getimglogo();
|
||||||
|
});
|
||||||
|
|
||||||
|
const dotsOptions = computed(() => ({
|
||||||
|
type: 'rounded' as const,
|
||||||
|
color: props.primaryColor,
|
||||||
|
gradient: {
|
||||||
|
type: 'linear' as const,
|
||||||
|
rotation: 0,
|
||||||
|
colorStops: [
|
||||||
|
{ offset: 0, color: props.primaryColor },
|
||||||
|
{ offset: 1, color: lightenColor(props.primaryColor, 20) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const canShare = computed(() => {
|
||||||
|
return !!navigator.share;
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadFilename = computed(() => {
|
||||||
|
const username = userStore.my?.username || 'user';
|
||||||
|
const timestamp = Date.now();
|
||||||
|
return `qrcode-${username}-${timestamp}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function onDecode(data: string) {
|
||||||
|
if (data) {
|
||||||
|
state.data = data;
|
||||||
|
emit('decoded', data);
|
||||||
|
|
||||||
|
// Vibrazione feedback su mobile
|
||||||
|
if (navigator.vibrate) {
|
||||||
|
navigator.vibrate(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = ref('');
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'QR Code rilevato!',
|
||||||
|
position: 'top',
|
||||||
|
timeout: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileUpload(file: File | null) {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Qui potresti usare una libreria per decodificare QR da immagine
|
||||||
|
// Per esempio: jsQR
|
||||||
|
$q.notify({
|
||||||
|
type: 'info',
|
||||||
|
message: 'Analisi immagine in corso...',
|
||||||
|
position: 'top',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findCanvas(maxAttempts = 10): Promise<HTMLCanvasElement | null> {
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
// Cerca in vari modi
|
||||||
|
let canvas: HTMLCanvasElement | null = null;
|
||||||
|
|
||||||
|
// Metodo 1: cerca nel ref
|
||||||
|
if (qrDisplayRef.value) {
|
||||||
|
canvas = qrDisplayRef.value.querySelector('canvas');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metodo 2: cerca con classe specifica
|
||||||
|
if (!canvas) {
|
||||||
|
const qrCode = document.querySelector('.qr-code');
|
||||||
|
canvas = qrCode?.querySelector('canvas') || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metodo 3: cerca nel container .qr-display
|
||||||
|
if (!canvas) {
|
||||||
|
const container = document.querySelector('.qr-display');
|
||||||
|
canvas = container?.querySelector('canvas') || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvas && canvas.width > 0 && canvas.height > 0) {
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aspetta prima del prossimo tentativo
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nuova funzione downloadQR
|
||||||
|
async function downloadQR() {
|
||||||
|
state.isDownloading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await nextTick();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
let dataUrl: string | null = null;
|
||||||
|
|
||||||
|
// Metodo 1: Cerca l'immagine (QRCodeVue3 genera un <img> con base64)
|
||||||
|
const qrDisplay = qrDisplayRef.value || document.querySelector('.qr-display');
|
||||||
|
|
||||||
|
if (qrDisplay) {
|
||||||
|
const img = qrDisplay.querySelector('img') as HTMLImageElement;
|
||||||
|
|
||||||
|
if (img && img.src && img.src.startsWith('data:image')) {
|
||||||
|
console.log('✅ Immagine base64 trovata!');
|
||||||
|
dataUrl = img.src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metodo 2: Se non trova img, cerca canvas (fallback)
|
||||||
|
if (!dataUrl) {
|
||||||
|
const canvas = document.querySelector(
|
||||||
|
'.qr-display canvas'
|
||||||
|
) as HTMLCanvasElement;
|
||||||
|
if (canvas && canvas.width > 0) {
|
||||||
|
console.log('✅ Canvas trovato come fallback');
|
||||||
|
dataUrl = canvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dataUrl) {
|
||||||
|
debugCanvas();
|
||||||
|
throw new Error('QR Code non trovato. Riprova.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.download = `${downloadFilename.value}.png`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'QR Code scaricato!',
|
||||||
|
icon: 'download_done',
|
||||||
|
position: 'top',
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Errore download QR:', error);
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.message || 'Errore durante il download',
|
||||||
|
position: 'top',
|
||||||
|
});
|
||||||
|
emit('error', error);
|
||||||
|
} finally {
|
||||||
|
state.isDownloading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function shareQR() {
|
||||||
|
try {
|
||||||
|
// Cerca l'immagine base64
|
||||||
|
const qrDisplay = qrDisplayRef.value || document.querySelector('.qr-display');
|
||||||
|
const img = qrDisplay?.querySelector('img') as HTMLImageElement;
|
||||||
|
|
||||||
|
if (img && img.src && img.src.startsWith('data:image')) {
|
||||||
|
// Converti base64 in blob
|
||||||
|
const response = await fetch(img.src);
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
const file = new File([blob], `${downloadFilename.value}.png`, {
|
||||||
|
type: 'image/png',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
||||||
|
await navigator.share({
|
||||||
|
title: props.textlink || 'QR Code',
|
||||||
|
files: [file],
|
||||||
|
});
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Condivisione avviata!',
|
||||||
|
position: 'top',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: condividi solo il link
|
||||||
|
await navigator.share({
|
||||||
|
title: props.textlink || 'QR Code',
|
||||||
|
url: props.link,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.name !== 'AbortError') {
|
||||||
|
console.error('Errore condivisione:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function copyToClipboard(text: string) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(text)
|
||||||
|
.then(() => {
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Copiato negli appunti!',
|
||||||
|
icon: 'content_copy',
|
||||||
|
position: 'top',
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Errore copia:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidUrl(text: string): boolean {
|
||||||
|
try {
|
||||||
|
new URL(text);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateUrl(url: string, maxLength: number = 400): string {
|
||||||
|
if (!url || url.length <= maxLength) return url;
|
||||||
|
return url.substring(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
function lightenColor(color: string, percent: number): string {
|
||||||
|
const num = parseInt(color.replace('#', ''), 16);
|
||||||
|
const amt = Math.round(2.55 * percent);
|
||||||
|
const R = (num >> 16) + amt;
|
||||||
|
const G = ((num >> 8) & 0x00ff) + amt;
|
||||||
|
const B = (num & 0x0000ff) + amt;
|
||||||
|
return (
|
||||||
|
'#' +
|
||||||
|
(
|
||||||
|
0x1000000 +
|
||||||
|
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
||||||
|
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
||||||
|
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
||||||
|
)
|
||||||
|
.toString(16)
|
||||||
|
.slice(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function naviga(path: string) {
|
function naviga(path: string) {
|
||||||
$router.push(path)
|
$router.push(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(mounted)
|
function debugCanvas() {
|
||||||
|
console.log('=== DEBUG QR ===');
|
||||||
|
console.log('1. qrDisplayRef.value:', qrDisplayRef.value);
|
||||||
|
|
||||||
function mounted() {
|
const qrDisplay = qrDisplayRef.value || document.querySelector('.qr-display');
|
||||||
// ...
|
|
||||||
|
if (qrDisplay) {
|
||||||
|
// Cerca immagini
|
||||||
|
const imgs = qrDisplay.querySelectorAll('img');
|
||||||
|
console.log('2. Immagini trovate:', imgs.length);
|
||||||
|
imgs.forEach((img, i) => {
|
||||||
|
const imgEl = img as HTMLImageElement;
|
||||||
|
console.log(
|
||||||
|
` Img ${i}: src starts with data:image = ${imgEl.src?.startsWith('data:image')}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cerca canvas
|
||||||
|
const canvases = qrDisplay.querySelectorAll('canvas');
|
||||||
|
console.log('3. Canvas trovati:', canvases.length);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'4. innerHTML (primi 200 char):',
|
||||||
|
qrDisplay.innerHTML.substring(0, 200)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(() => {
|
||||||
|
// Inizializzazione se necessaria
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// Stores
|
||||||
|
globalStore,
|
||||||
|
userStore,
|
||||||
|
|
||||||
|
// Utils
|
||||||
t,
|
t,
|
||||||
tools,
|
tools,
|
||||||
costanti,
|
costanti,
|
||||||
toolsext,
|
toolsext,
|
||||||
text,
|
|
||||||
userStore,
|
// State
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
qrSize,
|
||||||
|
logoImage,
|
||||||
|
dotsOptions,
|
||||||
|
canShare,
|
||||||
|
downloadFilename,
|
||||||
|
primaryColor: props.primaryColor,
|
||||||
|
|
||||||
|
// Methods
|
||||||
onDecode,
|
onDecode,
|
||||||
|
handleFileUpload,
|
||||||
|
qrDisplayRef,
|
||||||
|
downloadQR,
|
||||||
|
shareQR,
|
||||||
|
copyToClipboard,
|
||||||
|
isValidUrl,
|
||||||
|
truncateUrl,
|
||||||
naviga,
|
naviga,
|
||||||
globalStore,
|
};
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,71 +1,215 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="globalStore.finishLoading">
|
<div
|
||||||
<div v-if="read">
|
v-if="globalStore.finishLoading"
|
||||||
<div class="stream">
|
class="qrcode-container"
|
||||||
<qr-stream @decode="onDecode" class="mb">
|
|
||||||
<div style="color: red" class="frame"></div>
|
|
||||||
</qr-stream>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<qr-capture @decode="onDecode" class="mb"></qr-capture>
|
|
||||||
</div>
|
|
||||||
<div class="row justify-center q-ma-sm">
|
|
||||||
<q-btn
|
|
||||||
v-if="data && data.startsWith('http')"
|
|
||||||
class="q-ma-sm"
|
|
||||||
dense
|
|
||||||
color="positive"
|
|
||||||
@click="tools.openUrl(data)"
|
|
||||||
label="APRI PAGINA"
|
|
||||||
>
|
>
|
||||||
|
<!-- Modalità Lettura QR -->
|
||||||
|
<div
|
||||||
|
v-if="read"
|
||||||
|
class="qr-reader-section"
|
||||||
|
>
|
||||||
|
<div class="stream-container">
|
||||||
|
<qr-stream
|
||||||
|
@decode="onDecode"
|
||||||
|
class="qr-stream"
|
||||||
|
>
|
||||||
|
<div class="scan-frame">
|
||||||
|
<div class="corner top-left"></div>
|
||||||
|
<div class="corner top-right"></div>
|
||||||
|
<div class="corner bottom-left"></div>
|
||||||
|
<div class="corner bottom-right"></div>
|
||||||
|
</div>
|
||||||
|
</qr-stream>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fallback per upload immagine -->
|
||||||
|
<div class="upload-section q-mt-md">
|
||||||
|
<q-file
|
||||||
|
v-model="uploadedFile"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
label="Oppure carica un'immagine QR"
|
||||||
|
accept="image/*"
|
||||||
|
class="upload-input"
|
||||||
|
@update:model-value="handleFileUpload"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon
|
||||||
|
name="image"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-file>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Risultato scansione -->
|
||||||
|
<transition
|
||||||
|
name="fade"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="data"
|
||||||
|
class="scan-result q-mt-md"
|
||||||
|
>
|
||||||
|
<q-card class="result-card">
|
||||||
|
<q-card-section class="result-content">
|
||||||
|
<q-icon
|
||||||
|
name="check_circle"
|
||||||
|
color="positive"
|
||||||
|
size="32px"
|
||||||
|
class="q-mr-sm"
|
||||||
|
/>
|
||||||
|
<div class="result-text">
|
||||||
|
<span class="result-label">QR Code rilevato:</span>
|
||||||
|
<span class="result-value">{{ truncateUrl(data) }}</span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions
|
||||||
|
v-if="isValidUrl(data)"
|
||||||
|
class="result-actions"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="open_in_new"
|
||||||
|
label="Apri Link"
|
||||||
|
class="full-width"
|
||||||
|
@click="tools.openUrl(data)"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
|
||||||
|
<q-card-actions
|
||||||
|
v-else
|
||||||
|
class="result-actions"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
color="grey-7"
|
||||||
|
icon="content_copy"
|
||||||
|
label="Copia testo"
|
||||||
|
class="full-width"
|
||||||
|
@click="copyToClipboard(data)"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modalità Generazione QR -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="qr-generator-section"
|
||||||
|
>
|
||||||
|
<!-- Info link -->
|
||||||
|
<div
|
||||||
|
v-if="textlink || link"
|
||||||
|
class="link-info q-mb-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="textlink"
|
||||||
|
class="info-label"
|
||||||
|
>
|
||||||
|
{{ textlink }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="link"
|
||||||
|
class="info-link"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="link"
|
||||||
|
size="16px"
|
||||||
|
class="q-mr-xs"
|
||||||
|
/>
|
||||||
|
<span>{{ truncateUrl(link) }}</span>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
icon="content_copy"
|
||||||
|
@click="copyToClipboard(link)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Copia link</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<br />
|
|
||||||
<div v-if="data && data.startsWith('http')" class="result">
|
|
||||||
Link: {{ data }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div v-else>
|
<!-- QR Code Display -->
|
||||||
<div class="q-ma-sm">
|
<div class="qr-display" ref="qrDisplayRef">
|
||||||
{{ textlink }}<br />
|
<QRCodeVue3
|
||||||
{{ link }}<br />
|
|
||||||
Logo: {{imglogo}}<br />
|
|
||||||
</div>
|
|
||||||
<qrcode-vue
|
|
||||||
:width="250"
|
|
||||||
:height="250"
|
|
||||||
:qrOptions="{ typeNumber: 0, mode: 'Byte', errorCorrectionLevel: 'H' }"
|
|
||||||
:imageOptions="{ hideBackgroundDots: true, imageSize: 0.4, margin: 0 }"
|
|
||||||
:dotsOptions="{
|
|
||||||
type: 'dots',
|
|
||||||
color: '#26249a',
|
|
||||||
gradient: {
|
|
||||||
type: 'linear',
|
|
||||||
rotation: 0,
|
|
||||||
colorStops: [
|
|
||||||
{ offset: 0, color: '#26249a' },
|
|
||||||
{ offset: 1, color: '#26249a' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
:image="imglogo ? imglogo : tools.getimglogo()"
|
|
||||||
:cornersSquareOptions="{ type: 'dot', color: '#000000' }"
|
|
||||||
:cornersDotOptions="{ type: undefined, color: '#000000' }"
|
|
||||||
fileExt="png"
|
|
||||||
:download="true"
|
|
||||||
:value="link"
|
:value="link"
|
||||||
downloadButton="button_download"
|
:width="qrSize"
|
||||||
:downloadOptions="{
|
:height="qrSize"
|
||||||
name: 'qrcode-riso-' + userStore.my.username,
|
:qr-options="{
|
||||||
extension: 'png',
|
typeNumber: 0,
|
||||||
|
mode: 'Byte',
|
||||||
|
errorCorrectionLevel: 'H',
|
||||||
}"
|
}"
|
||||||
|
:image-options="{
|
||||||
|
hideBackgroundDots: true,
|
||||||
|
imageSize: 0.4,
|
||||||
|
margin: 10,
|
||||||
|
}"
|
||||||
|
:dots-options="dotsOptions"
|
||||||
|
:corners-square-options="{
|
||||||
|
type: 'extra-rounded',
|
||||||
|
color: primaryColor,
|
||||||
|
}"
|
||||||
|
:corners-dot-options="{
|
||||||
|
type: 'dot',
|
||||||
|
color: primaryColor,
|
||||||
|
}"
|
||||||
|
:background-options="{
|
||||||
|
color: '#ffffff',
|
||||||
|
}"
|
||||||
|
:image="logoImage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Download B utton -->
|
||||||
|
<div class="download-section q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="download"
|
||||||
|
label="Scarica QR Code"
|
||||||
|
class="download-btn"
|
||||||
|
:loading="isDownloading"
|
||||||
|
@click="downloadQR"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Share Options -->
|
||||||
|
<div
|
||||||
|
v-if="canShare"
|
||||||
|
class="share-section q-mt-md"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="primary"
|
||||||
|
icon="share"
|
||||||
|
label="Condividi"
|
||||||
|
class="share-btn"
|
||||||
|
@click="shareQR"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="loading-container"
|
||||||
|
>
|
||||||
|
<q-spinner-dots
|
||||||
|
color="primary"
|
||||||
|
size="40px"
|
||||||
|
/>
|
||||||
|
<span class="loading-text q-mt-sm">Caricamento...</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" src="./CQRCode.ts">
|
<script lang="ts" src="./CQRCode.ts"></script>
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import './CQRCode.scss';
|
@import './CQRCode.scss';
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ $r-md: 10px;
|
|||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
border-radius: $r-md;
|
border-radius: $r-md;
|
||||||
padding: $s-md;
|
padding: $s-sm;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ $r-md: 10px;
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: $s-md;
|
margin-bottom: $s-lg;
|
||||||
|
|
||||||
.balance-label {
|
.balance-label {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -56,7 +56,7 @@ $r-md: 10px;
|
|||||||
// Container progressione
|
// Container progressione
|
||||||
.progress-container {
|
.progress-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: $s-lg;
|
margin-bottom: $s-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track di sfondo
|
// Track di sfondo
|
||||||
@@ -160,6 +160,7 @@ $r-md: 10px;
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +170,7 @@ $r-md: 10px;
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: $s-sm;
|
gap: $s-sm;
|
||||||
padding-top: $s-md;
|
padding-top: $s-sm;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
// Layout inline quando !small
|
// Layout inline quando !small
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineComponent, computed } from 'vue';
|
import { defineComponent, computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'CRISBalanceBar',
|
name: 'CRISBalanceBar',
|
||||||
@@ -24,14 +25,24 @@ export default defineComponent({
|
|||||||
// Label opzionale
|
// Label opzionale
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Range disponibile',
|
default: '',
|
||||||
},
|
},
|
||||||
small: {
|
small: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
// Range totale
|
// Range totale
|
||||||
const totalRange = computed(() => {
|
const totalRange = computed(() => {
|
||||||
return Math.abs(props.minLimit) + props.maxLimit;
|
return Math.abs(props.minLimit) + props.maxLimit;
|
||||||
@@ -86,6 +97,7 @@ export default defineComponent({
|
|||||||
canGive,
|
canGive,
|
||||||
canReceive,
|
canReceive,
|
||||||
zeroPosition,
|
zeroPosition,
|
||||||
|
t,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ris-balance-bar">
|
<div
|
||||||
|
class="ris-balance-bar"
|
||||||
|
:style="color ? { color } : {}"
|
||||||
|
>
|
||||||
<!-- Label e valore corrente -->
|
<!-- Label e valore corrente -->
|
||||||
<div class="balance-header">
|
<div class="balance-header">
|
||||||
<span class="balance-label">{{ label }}</span>
|
<span
|
||||||
|
v-if="label"
|
||||||
|
class="balance-label text-white text-bold"
|
||||||
|
>{{ label }}</span
|
||||||
|
>
|
||||||
|
|
||||||
<span :class="['balance-current', balanceClass]">
|
<span :class="['balance-current', balanceClass]">
|
||||||
<span
|
<span
|
||||||
@@ -15,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Barra di progressione -->
|
<!-- Barra di progressione -->
|
||||||
<div class="progress-container">
|
<div class="progress-container q-pt-xs">
|
||||||
<!-- Linea di sfondo -->
|
<!-- Linea di sfondo -->
|
||||||
<div class="progress-track">
|
<div class="progress-track">
|
||||||
<!-- Zona negativa (rossa) -->
|
<!-- Zona negativa (rossa) -->
|
||||||
@@ -47,28 +54,31 @@
|
|||||||
:style="{ '--zero-position': zeroPosition + '%' }"
|
:style="{ '--zero-position': zeroPosition + '%' }"
|
||||||
>
|
>
|
||||||
<div class="marker min-marker">
|
<div class="marker min-marker">
|
||||||
<span class="marker-value">{{ minLimit.toFixed(2) }}</span>
|
<span class="marker-value">{{ minLimit }}</span>
|
||||||
<span class="marker-label">Fido</span>
|
<span class="marker-label">FIDO</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="marker zero-marker-label">
|
<div class="marker zero-marker-label">
|
||||||
<span class="marker-value">0</span>
|
<span class="marker-value">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="marker max-marker">
|
<div class="marker max-marker">
|
||||||
<span class="marker-value">+{{ maxLimit.toFixed(2) }}</span>
|
<span class="marker-value">+{{ maxLimit }}</span>
|
||||||
<span class="marker-label">Max</span>
|
<span class="marker-label">Max</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info disponibilità -->
|
<!-- Info disponibilità -->
|
||||||
<div :class="['availability-info', { 'inline-layout': !small }]">
|
<div
|
||||||
|
v-if="info"
|
||||||
|
:class="['availability-info', { 'inline-layout': !small }]"
|
||||||
|
>
|
||||||
<div class="availability-item">
|
<div class="availability-item">
|
||||||
<q-icon
|
<q-icon
|
||||||
name="arrow_downward"
|
name="arrow_downward"
|
||||||
size="xs"
|
size="xs"
|
||||||
color="negative"
|
color="negative"
|
||||||
/>
|
/>
|
||||||
<span class="availability-text">
|
<span class="availability-text text-white">
|
||||||
Puoi dare ancora: <strong>{{ canGive.toFixed(2) }} RIS</strong>
|
Puoi dare ancora: <strong>{{ canGive.toFixed(2) }} RIS</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +88,7 @@
|
|||||||
size="xs"
|
size="xs"
|
||||||
color="positive"
|
color="positive"
|
||||||
/>
|
/>
|
||||||
<span class="availability-text">
|
<span class="availability-text text-white">
|
||||||
Puoi ricevere: <strong>{{ canReceive.toFixed(2) }} RIS</strong>
|
Puoi ricevere: <strong>{{ canReceive.toFixed(2) }} RIS</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Variables
|
// Variables
|
||||||
$primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
$primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
$receive-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
$orange-gradient: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
$orange-gradient: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||||
$ris-color: #ff5500;
|
$ris-color: #ff5500;
|
||||||
$border-radius-lg: 16px;
|
$border-radius-lg: 16px;
|
||||||
@@ -8,36 +9,15 @@ $border-radius-sm: 8px;
|
|||||||
$shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
|
$shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
$shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
$shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
// Main Dialog
|
// Header Gradients
|
||||||
.send-coins-dialog {
|
|
||||||
border-radius: $border-radius-lg $border-radius-lg 0 0;
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: 420px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
|
||||||
border-radius: $border-radius-lg;
|
|
||||||
max-height: 90vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-fullheight {
|
|
||||||
height: 100vh;
|
|
||||||
max-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header
|
|
||||||
.dialog-header {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-gradient {
|
.header-gradient {
|
||||||
background: $primary-gradient;
|
background: $primary-gradient;
|
||||||
padding: 12px 16px 14px;
|
padding: 12px 16px 14px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&.receive-gradient {
|
||||||
|
background: $receive-gradient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-top-bar {
|
.header-top-bar {
|
||||||
@@ -62,6 +42,33 @@ $shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mode Icons
|
||||||
|
.mode-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&.send-icon {
|
||||||
|
background: $orange-gradient;
|
||||||
|
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.receive-icon {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ris-logo {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ris-coin-icon {
|
.ris-coin-icon {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@@ -145,17 +152,203 @@ $shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content
|
// ═══════════════════════════════════════════
|
||||||
.dialog-content {
|
// Receive Tabs
|
||||||
padding: 14px 16px;
|
// ═══════════════════════════════════════════
|
||||||
flex: 1;
|
.receive-tabs {
|
||||||
overflow-y: auto;
|
margin-top: 8px;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
.receive-tabs-inner {
|
||||||
display: none;
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: $border-radius-sm;
|
||||||
|
|
||||||
|
:deep(.q-tab) {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 12px;
|
||||||
|
min-height: 40px;
|
||||||
|
|
||||||
|
&.q-tab--active {
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
-ms-overflow-style: none;
|
}
|
||||||
scrollbar-width: none;
|
|
||||||
|
:deep(.q-tab__indicator) {
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Receive Panels
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
.receive-panels {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
:deep(.q-tab-panel) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-panel,
|
||||||
|
.qrcode-panel,
|
||||||
|
.showonlist-panel {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fef3c7;
|
||||||
|
border-radius: $border-radius-sm;
|
||||||
|
color: #92400e;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request Form
|
||||||
|
.request-form {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-input-wrapper {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receive-amount {
|
||||||
|
:deep(.q-field__control) {
|
||||||
|
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
|
||||||
|
border: none;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.q-field__native) {
|
||||||
|
color: white !important;
|
||||||
|
font-size: 22px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated Link Section
|
||||||
|
.generated-link-section {
|
||||||
|
padding: 16px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: $border-radius-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: white;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: $border-radius-sm;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.share-btn,
|
||||||
|
.whatsapp-btn {
|
||||||
|
flex: 1;
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR Code Panel
|
||||||
|
.qrcode-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-description {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-wrapper {
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
:deep(canvas),
|
||||||
|
:deep(img) {
|
||||||
|
border-radius: $border-radius-md;
|
||||||
|
box-shadow: $shadow-md;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick Amounts
|
||||||
|
.quick-amounts {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.q-chip {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Showonlist Panel
|
||||||
|
.showonlist-panel {
|
||||||
|
padding: 48px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info Banner
|
||||||
|
.info-banner {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1e40af;
|
||||||
|
border-radius: $border-radius-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty State
|
||||||
|
.empty-state,
|
||||||
|
.empty-state-circuit {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48px 24px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section Block - Più compatto
|
// Section Block - Più compatto
|
||||||
@@ -185,10 +378,6 @@ $shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.q-field__control-container) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.q-field--focused .q-field__control) {
|
:deep(.q-field--focused .q-field__control) {
|
||||||
border-color: #667eea;
|
border-color: #667eea;
|
||||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15);
|
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15);
|
||||||
@@ -348,25 +537,7 @@ $shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Action Buttons
|
||||||
.dialog-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: white;
|
|
||||||
border-top: 1px solid #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fixed-bottom-actions {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
|
|
||||||
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
.cancel-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
@@ -403,6 +574,15 @@ $shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.receive-btn {
|
||||||
|
background: $receive-gradient;
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||||
|
|
||||||
|
&:hover:not(.btn-disabled) {
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.send-btn-text {
|
.send-btn-text {
|
||||||
@@ -479,7 +659,51 @@ $shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|||||||
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important;
|
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Dialog Layout
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
.send-coins-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 100vh;
|
||||||
|
|
||||||
|
&.mobile-fullheight {
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
background: white;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-block:last-child {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
// Dark mode support
|
// Dark mode support
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
.body--dark {
|
.body--dark {
|
||||||
.send-coins-dialog {
|
.send-coins-dialog {
|
||||||
background: #1f2937;
|
background: #1f2937;
|
||||||
@@ -526,10 +750,6 @@ $shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|||||||
border-top-color: #374151;
|
border-top-color: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixed-bottom-actions {
|
|
||||||
background: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
.cancel-btn {
|
||||||
background: #374151;
|
background: #374151;
|
||||||
color: #d1d5db;
|
color: #d1d5db;
|
||||||
@@ -547,9 +767,34 @@ $shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|||||||
.keyboard-title {
|
.keyboard-title {
|
||||||
color: #d1d5db;
|
color: #d1d5db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.generated-link-section {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-preview {
|
||||||
|
background: #1f2937;
|
||||||
|
border-color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-text span {
|
||||||
|
color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-description {
|
||||||
|
background: #78350f;
|
||||||
|
color: #fef3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-banner {
|
||||||
|
background: #1e3a5f;
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responsive - Ultra compatto per mobile piccoli
|
// ═══════════════════════════════════════════
|
||||||
|
// Responsive
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
@media (max-width: 360px) {
|
@media (max-width: 360px) {
|
||||||
.header-gradient {
|
.header-gradient {
|
||||||
padding: 10px 12px 12px;
|
padding: 10px 12px 12px;
|
||||||
@@ -572,4 +817,8 @@ $shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|||||||
font-size: 20px !important;
|
font-size: 20px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.share-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,14 +13,24 @@ import { CNumericKeyboard } from '@/components/CNumericKeyboard';
|
|||||||
import { CMyUserOnlyView } from '@/components/CMyUserOnlyView';
|
import { CMyUserOnlyView } from '@/components/CMyUserOnlyView';
|
||||||
import { CMyGroupOnlyView } from '@/components/CMyGroupOnlyView';
|
import { CMyGroupOnlyView } from '@/components/CMyGroupOnlyView';
|
||||||
import { CCheckCircuitsEnabled } from '@/components/CCheckCircuitsEnabled';
|
import { CCheckCircuitsEnabled } from '@/components/CCheckCircuitsEnabled';
|
||||||
|
import { CQRCode } from '@/components/CQRCode';
|
||||||
|
import { CCopyBtnSmall } from '@/components/CCopyBtnSmall';
|
||||||
import { costanti } from '@costanti';
|
import { costanti } from '@costanti';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { shared_consts } from '@/common/shared_vuejs';
|
import { shared_consts } from '@/common/shared_vuejs';
|
||||||
|
|
||||||
|
export type DialogMode = 'send' | 'receive';
|
||||||
|
export type ReceiveTabType = 'link' | 'qrcode' | 'showonlist';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'CSendCoins',
|
name: 'CSendCoins',
|
||||||
emits: ['close', 'showed'],
|
emits: ['close', 'showed'],
|
||||||
props: {
|
props: {
|
||||||
|
// Modalità: 'send' per inviare, 'receive' per ricevere
|
||||||
|
mode: {
|
||||||
|
type: String as PropType<DialogMode>,
|
||||||
|
default: 'send',
|
||||||
|
},
|
||||||
loadprofile: {
|
loadprofile: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -75,6 +85,8 @@ export default defineComponent({
|
|||||||
CMyGroupOnlyView,
|
CMyGroupOnlyView,
|
||||||
CCheckCircuitsEnabled,
|
CCheckCircuitsEnabled,
|
||||||
CNumericKeyboard,
|
CNumericKeyboard,
|
||||||
|
CQRCode,
|
||||||
|
CCopyBtnSmall,
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
@@ -85,23 +97,24 @@ export default defineComponent({
|
|||||||
const circuitStore = useCircuitStore();
|
const circuitStore = useCircuitStore();
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
|
|
||||||
const to_user_real = ref(<IUserFields>{});
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// COMPUTED - Mode
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
const isReceiveMode = computed(() => props.mode === 'receive');
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// REFS - Common
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
const to_user_real = ref(<IUserFields>{});
|
||||||
const from_username = ref(userStore.my.username);
|
const from_username = ref(userStore.my.username);
|
||||||
const from_groupname = ref('');
|
const from_groupname = ref('');
|
||||||
const from_contocom = ref('');
|
const from_contocom = ref('');
|
||||||
const circuitsel = ref('');
|
const circuitsel = ref('');
|
||||||
const qty = ref(<string | number>'');
|
|
||||||
const causal = ref('');
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const visubanner = ref(true);
|
|
||||||
const bothcircuits = ref(<any>[]);
|
const bothcircuits = ref(<any>[]);
|
||||||
|
|
||||||
const showProvinceToSelect = ref(false);
|
const showProvinceToSelect = ref(false);
|
||||||
const showKeyboard = ref(false);
|
|
||||||
|
|
||||||
const groupSel = ref(<IMyGroup | null | undefined>null);
|
const groupSel = ref(<IMyGroup | null | undefined>null);
|
||||||
|
|
||||||
const datasaved = ref(<any>null);
|
const datasaved = ref(<any>null);
|
||||||
const step = ref(0);
|
const step = ref(0);
|
||||||
const sendCoinDialog = ref(null);
|
const sendCoinDialog = ref(null);
|
||||||
@@ -116,19 +129,61 @@ export default defineComponent({
|
|||||||
const numstep = ref(0);
|
const numstep = ref(0);
|
||||||
const arrTypesAccounts = ref(<any>[]);
|
const arrTypesAccounts = ref(<any>[]);
|
||||||
const tipoConto = ref(shared_consts.AccountType.USER);
|
const tipoConto = ref(shared_consts.AccountType.USER);
|
||||||
|
const arrGroupsList = ref(<any[]>[]);
|
||||||
|
const groupsListAdmin = ref(<IMyGroup[]>[]);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// REFS - Send Mode
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
const qty = ref(<string | number>'');
|
||||||
|
const causal = ref('');
|
||||||
|
const showKeyboard = ref(false);
|
||||||
|
const qtyRef = ref(<any>null);
|
||||||
|
const causalRef = ref(<any>null);
|
||||||
|
const visubanner = ref(true);
|
||||||
|
const arrayMarkerLabel = ref(<any>[]);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// REFS - Receive Mode
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
const receiveType = ref<ReceiveTabType>('link');
|
||||||
|
const receiveQty = ref('');
|
||||||
|
const receiveCausal = ref('');
|
||||||
|
const riscallrec = ref('');
|
||||||
|
const showonreclist = ref(false);
|
||||||
|
const quickAmounts = ref([5, 10, 20, 50, 100]);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// COMPUTED
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
const priceLabel = computed(() =>
|
const priceLabel = computed(() =>
|
||||||
circuitloaded.value ? `${qty.value} ` + circuitloaded.value.symbol : ''
|
circuitloaded.value ? `${qty.value} ` + circuitloaded.value.symbol : ''
|
||||||
);
|
);
|
||||||
const arrayMarkerLabel = ref(<any>[]);
|
|
||||||
|
|
||||||
const qtyRef = ref(<any>null);
|
// Circuiti disponibili (diversi per SEND e RECEIVE)
|
||||||
const causalRef = ref(<any>null);
|
const availableCircuits = computed(() => {
|
||||||
|
if (isReceiveMode.value) {
|
||||||
|
// Per RECEIVE: tutti i miei circuiti
|
||||||
|
return userStore.getMyCircuits();
|
||||||
|
} else {
|
||||||
|
// Per SEND: circuiti in comune con il destinatario
|
||||||
|
return bothcircuits.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const groupsListAdmin = ref(<IMyGroup[]>[]);
|
// Link generato per la ricezione
|
||||||
|
const generatedLink = computed(() => {
|
||||||
const arrGroupsList = ref(<any[]>[]);
|
return userStore.getLinkProfileAndRIS(
|
||||||
|
circuitsel.value,
|
||||||
|
'',
|
||||||
|
receiveQty.value,
|
||||||
|
receiveCausal.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// WATCHERS
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
watch(
|
watch(
|
||||||
() => circuitsel.value,
|
() => circuitsel.value,
|
||||||
(newval, oldval) => {
|
(newval, oldval) => {
|
||||||
@@ -144,9 +199,7 @@ export default defineComponent({
|
|||||||
if (arrGroupsList.value.length >= 1)
|
if (arrGroupsList.value.length >= 1)
|
||||||
from_groupname.value = arrGroupsList.value[0].value;
|
from_groupname.value = arrGroupsList.value[0].value;
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.setCookie(tools.COOK_TIPOCONTO, tipoConto.value.toString());
|
tools.setCookie(tools.COOK_TIPOCONTO, tipoConto.value.toString());
|
||||||
|
|
||||||
aggiorna(true);
|
aggiorna(true);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -172,6 +225,17 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Watch per aggiornare il link quando cambiano i valori di ricezione
|
||||||
|
watch(
|
||||||
|
() => receiveQty.value,
|
||||||
|
() => {
|
||||||
|
limitReceiveQuantity();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// METHODS - Common
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
async function aggiorna(load: boolean = false) {
|
async function aggiorna(load: boolean = false) {
|
||||||
if (load) {
|
if (load) {
|
||||||
inizio_caricamento();
|
inizio_caricamento();
|
||||||
@@ -219,7 +283,6 @@ export default defineComponent({
|
|||||||
groupSel.value = userStore.my.profile.manage_mygroups.find(
|
groupSel.value = userStore.my.profile.manage_mygroups.find(
|
||||||
(group: IMyGroup) => from_groupname.value === group.groupname
|
(group: IMyGroup) => from_groupname.value === group.groupname
|
||||||
);
|
);
|
||||||
|
|
||||||
accountloaded.value = groupSel.value ? groupSel.value.account : null;
|
accountloaded.value = groupSel.value ? groupSel.value.account : null;
|
||||||
} else if (tipoConto.value === shared_consts.AccountType.COMMUNITY_ACCOUNT) {
|
} else if (tipoConto.value === shared_consts.AccountType.COMMUNITY_ACCOUNT) {
|
||||||
from_contocom.value = circuitloaded.value.path;
|
from_contocom.value = circuitloaded.value.path;
|
||||||
@@ -237,10 +300,8 @@ export default defineComponent({
|
|||||||
if (groupsListAdmin.value) {
|
if (groupsListAdmin.value) {
|
||||||
for (const group of groupsListAdmin.value) {
|
for (const group of groupsListAdmin.value) {
|
||||||
let aggiungi = true;
|
let aggiungi = true;
|
||||||
|
|
||||||
if (props.to_group && props.to_group.groupname === group.groupname)
|
if (props.to_group && props.to_group.groupname === group.groupname)
|
||||||
aggiungi = false;
|
aggiungi = false;
|
||||||
|
|
||||||
if (aggiungi)
|
if (aggiungi)
|
||||||
arrGroupsList.value.push({
|
arrGroupsList.value.push({
|
||||||
label: group.groupname,
|
label: group.groupname,
|
||||||
@@ -317,6 +378,15 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invio_ris_caricamento() {
|
||||||
|
$q.loading.show({
|
||||||
|
message: 'Invio RIS in corso...',
|
||||||
|
spinnerColor: 'green',
|
||||||
|
backgroundColor: 'primary',
|
||||||
|
messageColor: 'white',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function fine_caricamento() {
|
function fine_caricamento() {
|
||||||
$q.loading.hide();
|
$q.loading.hide();
|
||||||
}
|
}
|
||||||
@@ -324,6 +394,19 @@ export default defineComponent({
|
|||||||
async function mounted() {
|
async function mounted() {
|
||||||
inizio_caricamento();
|
inizio_caricamento();
|
||||||
|
|
||||||
|
if (isReceiveMode.value) {
|
||||||
|
// RECEIVE MODE INIT
|
||||||
|
await initReceiveMode();
|
||||||
|
} else {
|
||||||
|
// SEND MODE INIT
|
||||||
|
await initSendMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
emit('showed');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initSendMode() {
|
||||||
to_user_real.value = props.to_user;
|
to_user_real.value = props.to_user;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
@@ -360,7 +443,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
bothcircuits.value &&
|
bothcircuits.value &&
|
||||||
bothcircuits.value.find((name: any) => name !== circuitsel.value)
|
!bothcircuits.value.some((name: any) => name === circuitsel.value)
|
||||||
) {
|
) {
|
||||||
circuitsel.value = bothcircuits.value[0];
|
circuitsel.value = bothcircuits.value[0];
|
||||||
}
|
}
|
||||||
@@ -414,9 +497,39 @@ export default defineComponent({
|
|||||||
|
|
||||||
showpage.value = true;
|
showpage.value = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = false;
|
async function initReceiveMode() {
|
||||||
emit('showed');
|
// Per RECEIVE mode: mostra tutti i circuiti dell'utente
|
||||||
|
bothcircuits.value = userStore.getMyCircuits();
|
||||||
|
|
||||||
|
// Seleziona il primo circuito o quello salvato
|
||||||
|
if (props.circuitname) {
|
||||||
|
circuitsel.value = props.circuitname;
|
||||||
|
} else {
|
||||||
|
const savedCircuit = tools.getCookie(tools.CIRCUIT_USE, '');
|
||||||
|
if (savedCircuit && bothcircuits.value.includes(savedCircuit)) {
|
||||||
|
circuitsel.value = savedCircuit;
|
||||||
|
} else if (bothcircuits.value.length > 0) {
|
||||||
|
circuitsel.value = bothcircuits.value[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (circuitsel.value) {
|
||||||
|
await aggiorna();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiungi alla lista dei riceventi temporanei
|
||||||
|
await clickAddtoRecList();
|
||||||
|
|
||||||
|
showpage.value = true;
|
||||||
|
fine_caricamento();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDialogShow() {
|
||||||
|
if (!isReceiveMode.value && qtyRef.value) {
|
||||||
|
qtyRef.value.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
@@ -424,7 +537,10 @@ export default defineComponent({
|
|||||||
showpage.value = false;
|
showpage.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendCoin() {
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// METHODS - Send Mode
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
async function sendCoin() {
|
||||||
const ok =
|
const ok =
|
||||||
(to_user_real.value && to_user_real.value.username) ||
|
(to_user_real.value && to_user_real.value.username) ||
|
||||||
(props.to_group && props.to_group.groupname) ||
|
(props.to_group && props.to_group.groupname) ||
|
||||||
@@ -456,12 +572,55 @@ export default defineComponent({
|
|||||||
myrecsendcoin.dest = to_user_real.value ? to_user_real.value.username : '';
|
myrecsendcoin.dest = to_user_real.value ? to_user_real.value.username : '';
|
||||||
|
|
||||||
if (myrecsendcoin) {
|
if (myrecsendcoin) {
|
||||||
tools
|
const orig = myrecsendcoin.grouporig || myrecsendcoin.contoComOrig || '';
|
||||||
.sendCoinsByCircuit($q, $router, circuitloaded.value, myrecsendcoin)
|
const dest =
|
||||||
.then((ris: any) => {
|
myrecsendcoin.groupdest || myrecsendcoin.contoComDest || myrecsendcoin.dest;
|
||||||
|
|
||||||
|
// Seleziona il messaggio appropriato
|
||||||
|
const messageKey = orig
|
||||||
|
? 'circuit.question_sendcoinsto_from'
|
||||||
|
: 'circuit.question_sendcoinsto';
|
||||||
|
|
||||||
|
// Parametri comuni per i messaggi
|
||||||
|
const msgParams = {
|
||||||
|
coin: circuitloaded.value.symbol,
|
||||||
|
dest,
|
||||||
|
qty: myrecsendcoin.qty,
|
||||||
|
circuit: circuitloaded.value.name,
|
||||||
|
...(orig && { from: orig }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mostra dialog di conferma
|
||||||
|
$q.dialog({
|
||||||
|
title: t('db.domanda'),
|
||||||
|
message: t(messageKey, msgParams),
|
||||||
|
ok: {
|
||||||
|
label: t('dialog.yes'),
|
||||||
|
color: 'primary',
|
||||||
|
push: true,
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: t('dialog.cancel'),
|
||||||
|
color: 'secondary',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.onOk(async () => {
|
||||||
|
invio_ris_caricamento();
|
||||||
|
const ris = await tools.sendCoinsByCircuit(
|
||||||
|
$q,
|
||||||
|
circuitloaded.value,
|
||||||
|
myrecsendcoin
|
||||||
|
);
|
||||||
if (ris) {
|
if (ris) {
|
||||||
showpage.value = false;
|
showpage.value = false;
|
||||||
}
|
}
|
||||||
|
fine_caricamento()
|
||||||
|
})
|
||||||
|
.onCancel(() => {
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.onDismiss(() => {
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,7 +660,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
function getQty(): number {
|
function getQty(): number {
|
||||||
let myqty: number | null = null;
|
let myqty: number | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (qty.value) {
|
if (qty.value) {
|
||||||
myqty = parseFloat(String(qty.value));
|
myqty = parseFloat(String(qty.value));
|
||||||
@@ -509,48 +667,106 @@ export default defineComponent({
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return myqty ? myqty : 0;
|
return myqty ? myqty : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTitle(step: number) {
|
|
||||||
if (step === 0) {
|
|
||||||
return 'Circuito';
|
|
||||||
} else if (step === 1) {
|
|
||||||
return 'Quantità';
|
|
||||||
} else if (step === 2) {
|
|
||||||
return 'Causale';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIcon(step: number) {
|
|
||||||
if (step === 0) {
|
|
||||||
return 'circuit';
|
|
||||||
} else if (step === 1) {
|
|
||||||
return 'attach_money';
|
|
||||||
} else if (step === 2) {
|
|
||||||
return 'description';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setQty(value: string | number) {
|
function setQty(value: string | number) {
|
||||||
qty.value = value;
|
qty.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// METHODS - Receive Mode
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
function limitReceiveQuantity() {
|
||||||
|
if (receiveQty.value && receiveQty.value.length > 5) {
|
||||||
|
receiveQty.value = receiveQty.value.substring(0, 5);
|
||||||
|
}
|
||||||
|
if (receiveQty.value) {
|
||||||
|
receiveQty.value = receiveQty.value.replace(',', '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setQuickAmount(amount: number) {
|
||||||
|
receiveQty.value = amount.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateLink(link: string, maxLength: number = 35): string {
|
||||||
|
if (!link || link.length <= maxLength) return link;
|
||||||
|
return link.substring(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shareLink() {
|
||||||
|
if (navigator.share) {
|
||||||
|
try {
|
||||||
|
await navigator.share({
|
||||||
|
title: 'Ricevi pagamento',
|
||||||
|
text: receiveCausal.value || 'Paga con questo link',
|
||||||
|
url: generatedLink.value,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Share cancelled');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: copia negli appunti
|
||||||
|
navigator.clipboard.writeText(generatedLink.value);
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Link copiato!',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareWhatsApp() {
|
||||||
|
const text = encodeURIComponent(
|
||||||
|
`${receiveCausal.value ? receiveCausal.value + '\n' : ''}Paga qui: ${generatedLink.value}`
|
||||||
|
);
|
||||||
|
window.open(`https://wa.me/?text=${text}`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureShowcase() {
|
||||||
|
showpage.value = false;
|
||||||
|
$router.push('/showcase/settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickAddtoRecList() {
|
||||||
|
const risultato = await tools.addToTemporaryReceiverRIS(t);
|
||||||
|
if (risultato) {
|
||||||
|
// riscallrec.value = risultato.msg;
|
||||||
|
showonreclist.value = risultato.ris;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// LIFECYCLE
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
onMounted(mounted);
|
onMounted(mounted);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// Common
|
||||||
t,
|
t,
|
||||||
tools,
|
tools,
|
||||||
showpage,
|
showpage,
|
||||||
bothcircuits,
|
|
||||||
from_username,
|
|
||||||
circuitsel,
|
circuitsel,
|
||||||
circuitloaded,
|
circuitloaded,
|
||||||
accountloaded,
|
accountloaded,
|
||||||
accountdest,
|
accountdest,
|
||||||
qty,
|
|
||||||
hide,
|
hide,
|
||||||
|
loading,
|
||||||
|
circuitStore,
|
||||||
|
costanti,
|
||||||
|
userStore,
|
||||||
|
step,
|
||||||
|
sendCoinDialog,
|
||||||
|
showProvinceToSelect,
|
||||||
|
shared_consts,
|
||||||
|
availableCircuits,
|
||||||
|
isReceiveMode,
|
||||||
|
onDialogShow,
|
||||||
|
|
||||||
|
// Send Mode
|
||||||
|
bothcircuits,
|
||||||
|
from_username,
|
||||||
|
qty,
|
||||||
sendCoin,
|
sendCoin,
|
||||||
causal,
|
causal,
|
||||||
priceLabel,
|
priceLabel,
|
||||||
@@ -559,27 +775,31 @@ export default defineComponent({
|
|||||||
qtyRef,
|
qtyRef,
|
||||||
causalRef,
|
causalRef,
|
||||||
maxsendable,
|
maxsendable,
|
||||||
circuitStore,
|
|
||||||
numstep,
|
numstep,
|
||||||
costanti,
|
|
||||||
userStore,
|
|
||||||
tipoConto,
|
tipoConto,
|
||||||
arrGroupsList,
|
arrGroupsList,
|
||||||
from_groupname,
|
from_groupname,
|
||||||
from_contocom,
|
from_contocom,
|
||||||
arrTypesAccounts,
|
arrTypesAccounts,
|
||||||
loading,
|
|
||||||
showProvinceToSelect,
|
|
||||||
shared_consts,
|
|
||||||
step,
|
|
||||||
ifNextCheck,
|
ifNextCheck,
|
||||||
getTitle,
|
|
||||||
getIcon,
|
|
||||||
sendCoinDialog,
|
|
||||||
visubanner,
|
visubanner,
|
||||||
showKeyboard,
|
showKeyboard,
|
||||||
setQty,
|
setQty,
|
||||||
to_user_real,
|
to_user_real,
|
||||||
|
|
||||||
|
// Receive Mode
|
||||||
|
receiveType,
|
||||||
|
receiveQty,
|
||||||
|
receiveCausal,
|
||||||
|
riscallrec,
|
||||||
|
quickAmounts,
|
||||||
|
generatedLink,
|
||||||
|
limitReceiveQuantity,
|
||||||
|
setQuickAmount,
|
||||||
|
truncateLink,
|
||||||
|
shareLink,
|
||||||
|
shareWhatsApp,
|
||||||
|
configureShowcase,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,30 +4,40 @@
|
|||||||
ref="sendCoinDialog"
|
ref="sendCoinDialog"
|
||||||
:maximized="$q.screen.lt.sm"
|
:maximized="$q.screen.lt.sm"
|
||||||
@hide="hide"
|
@hide="hide"
|
||||||
@show="qtyRef ? qtyRef.focus() : ''"
|
@show="onDialogShow"
|
||||||
transition-show="slide-up"
|
transition-show="slide-up"
|
||||||
transition-hide="slide-down"
|
transition-hide="slide-down"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
class="send-coins-dialog"
|
class="send-coins-dialog column no-wrap"
|
||||||
:class="{ 'mobile-fullheight': $q.screen.lt.sm }"
|
:class="{ 'mobile-fullheight': $q.screen.lt.sm }"
|
||||||
>
|
>
|
||||||
<!-- Header con gradiente -->
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
<q-card-section class="dialog-header q-pa-none">
|
<!-- HEADER -->
|
||||||
<div class="header-gradient">
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<q-card-section class="dialog-header q-pa-none col-shrink">
|
||||||
|
<div class="header-gradient" :class="{ 'receive-gradient': isReceiveMode }">
|
||||||
<!-- Top bar -->
|
<!-- Top bar -->
|
||||||
<div class="header-top-bar">
|
<div class="header-top-bar">
|
||||||
<div class="header-title-wrapper">
|
<div class="header-title-wrapper">
|
||||||
<div class="ris-coin-icon">
|
<!-- Icon based on mode -->
|
||||||
|
<div class="mode-icon" :class="isReceiveMode ? 'receive-icon' : 'send-icon'">
|
||||||
|
<q-icon
|
||||||
|
:name="isReceiveMode ? 'south_west' : 'north_east'"
|
||||||
|
size="20px"
|
||||||
|
v-if="!circuitloaded.symbol || circuitloaded.symbol !== 'RIS'"
|
||||||
|
/>
|
||||||
<img
|
<img
|
||||||
v-if="circuitloaded.symbol === 'RIS'"
|
v-else
|
||||||
src="/images/1ris_rosso_100.png"
|
src="/images/1ris_rosso_100.png"
|
||||||
alt="RIS"
|
alt="RIS"
|
||||||
class="ris-logo"
|
class="ris-logo"
|
||||||
/>
|
/>
|
||||||
<span v-else class="coin-symbol">{{ circuitloaded.symbol }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="header-title">Invia {{ circuitloaded.symbol || 'Crediti' }}</span>
|
<span class="header-title">
|
||||||
|
{{ isReceiveMode ? 'Ricevi' : 'Invia' }}
|
||||||
|
{{ circuitloaded.symbol || 'Crediti' }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
@@ -40,41 +50,61 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Balance Card compatto -->
|
<!-- Balance Card (solo per SEND mode) -->
|
||||||
<div class="balance-card">
|
<div v-if="!isReceiveMode && accountloaded" class="balance-card">
|
||||||
<div class="balance-info">
|
<div class="balance-info">
|
||||||
<div class="balance-main">
|
<div class="balance-main">
|
||||||
<span class="balance-label">Saldo disponibile</span>
|
<span class="balance-label">Saldo disponibile</span>
|
||||||
<span class="balance-value">
|
<span class="balance-value">
|
||||||
{{ accountloaded ? circuitStore.getRemainingCoinsToSend(accountloaded).toFixed(2) : '0.00' }}
|
{{ circuitStore.getRemainingCoinsToSend(accountloaded).toFixed(2) }}
|
||||||
<span class="balance-symbol">{{ circuitloaded.symbol }}</span>
|
<span class="balance-symbol">{{ circuitloaded.symbol }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="balance-fido" v-if="accountloaded?.fidoConcesso > 0">
|
<div class="balance-fido" v-if="accountloaded?.fidoConcesso > 0">
|
||||||
<span class="fido-label">Fido</span>
|
<span class="fido-label">{{ t('circuit.fido_scoperto_default') }}</span>
|
||||||
<span class="fido-value">+{{ accountloaded.fidoConcesso.toFixed(2) }}</span>
|
<span class="fido-value">+{{ accountloaded.fidoConcesso.toFixed(2) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Receive Type Tabs (solo per RECEIVE mode e circuito selezionato) -->
|
||||||
|
<div v-if="isReceiveMode && circuitsel" class="receive-tabs">
|
||||||
|
<q-tabs
|
||||||
|
v-model="receiveType"
|
||||||
|
dense
|
||||||
|
active-color="white"
|
||||||
|
indicator-color="white"
|
||||||
|
align="justify"
|
||||||
|
narrow-indicator
|
||||||
|
class="receive-tabs-inner"
|
||||||
|
>
|
||||||
|
<q-tab name="link" icon="link" label="Link" />
|
||||||
|
<q-tab name="qrcode" icon="qr_code_2" label="QR Code" />
|
||||||
|
<!--<q-tab name="showonlist" icon="storefront" label="Vetrina" />-->
|
||||||
|
</q-tabs>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
<q-card-section
|
<!-- CONTENT -->
|
||||||
class="dialog-content scroll"
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
:style="$q.screen.lt.sm ? 'padding-bottom: 80px;' : ''"
|
<q-card-section class="dialog-content col scroll">
|
||||||
>
|
|
||||||
<!-- Circuit Check -->
|
<!-- Circuit Check -->
|
||||||
<CCheckCircuitsEnabled
|
<CCheckCircuitsEnabled
|
||||||
|
v-if="!isReceiveMode"
|
||||||
:to_user="to_user_real"
|
:to_user="to_user_real"
|
||||||
:to_group="to_group"
|
:to_group="to_group"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Circuit Selector -->
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
<div v-if="circuitloaded.symbol && circuitname === ''" class="section-block">
|
<!-- STEP 1: Circuit Selector (comune a entrambi) -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<div v-if="circuitloaded.symbol" class="section-block">
|
||||||
|
<label class="section-label">Seleziona Circuito</label>
|
||||||
<q-select
|
<q-select
|
||||||
v-model="circuitsel"
|
v-model="circuitsel"
|
||||||
:options="bothcircuits"
|
:options="availableCircuits"
|
||||||
:behavior="$q.platform.is.ios === true ? 'dialog' : 'menu'"
|
:behavior="$q.platform.is.ios === true ? 'dialog' : 'menu'"
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
@@ -85,6 +115,16 @@
|
|||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-icon name="account_balance_wallet" color="primary" />
|
<q-icon name="account_balance_wallet" color="primary" />
|
||||||
</template>
|
</template>
|
||||||
|
<template v-slot:option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="account_balance" color="primary" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ scope.opt }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -100,8 +140,12 @@
|
|||||||
{{ $t('circuit.insertprovince_text') }}
|
{{ $t('circuit.insertprovince_text') }}
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<!-- SEND MODE CONTENT -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<template v-if="!isReceiveMode && circuitsel">
|
||||||
<!-- Sender Section -->
|
<!-- Sender Section -->
|
||||||
<div v-if="circuitsel" class="section-block">
|
<div class="section-block">
|
||||||
<q-select
|
<q-select
|
||||||
v-if="arrTypesAccounts.length > 0"
|
v-if="arrTypesAccounts.length > 0"
|
||||||
v-model="tipoConto"
|
v-model="tipoConto"
|
||||||
@@ -153,11 +197,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recipient Section -->
|
<!-- Recipient Section -->
|
||||||
<div v-if="circuitsel" class="section-block">
|
<div class="section-block">
|
||||||
<label class="section-label">Destinatario</label>
|
<label class="section-label">Destinatario</label>
|
||||||
<div class="recipient-card">
|
<div class="recipient-card">
|
||||||
<div class="recipient-content">
|
<div class="recipient-content">
|
||||||
<!-- User Recipient -->
|
|
||||||
<CMyUserOnlyView
|
<CMyUserOnlyView
|
||||||
v-if="to_user_real"
|
v-if="to_user_real"
|
||||||
:mycontact="to_user_real"
|
:mycontact="to_user_real"
|
||||||
@@ -165,8 +208,6 @@
|
|||||||
@setCmd="tools.setCmd"
|
@setCmd="tools.setCmd"
|
||||||
class="recipient-view"
|
class="recipient-view"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Group Recipient -->
|
|
||||||
<CMyGroupOnlyView
|
<CMyGroupOnlyView
|
||||||
v-if="to_group"
|
v-if="to_group"
|
||||||
:mygrp="to_group"
|
:mygrp="to_group"
|
||||||
@@ -174,8 +215,6 @@
|
|||||||
:circuitname="circuitloaded.name"
|
:circuitname="circuitloaded.name"
|
||||||
class="recipient-view"
|
class="recipient-view"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Community Account Recipient -->
|
|
||||||
<CMyGroupOnlyView
|
<CMyGroupOnlyView
|
||||||
v-if="circuitloaded && !!circuitloaded._id && to_contocom"
|
v-if="circuitloaded && !!circuitloaded._id && to_contocom"
|
||||||
:mygrp="{ groupname: to_contocom }"
|
:mygrp="{ groupname: to_contocom }"
|
||||||
@@ -187,11 +226,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Amount Section - Compatto -->
|
<!-- Amount Section -->
|
||||||
<div v-if="circuitsel" class="section-block">
|
<div class="section-block">
|
||||||
<label class="section-label">Importo</label>
|
<label class="section-label">Importo</label>
|
||||||
|
<div
|
||||||
<div class="amount-input-row" @click="$q.screen.lt.sm ? showKeyboard = true : qtyRef?.focus()">
|
class="amount-input-row"
|
||||||
|
@click="$q.screen.lt.sm ? (showKeyboard = true) : qtyRef?.focus()"
|
||||||
|
>
|
||||||
<q-input
|
<q-input
|
||||||
ref="qtyRef"
|
ref="qtyRef"
|
||||||
v-model="qty"
|
v-model="qty"
|
||||||
@@ -202,7 +243,12 @@
|
|||||||
:readonly="$q.screen.lt.sm"
|
:readonly="$q.screen.lt.sm"
|
||||||
:rules="[
|
:rules="[
|
||||||
(val) => !isNaN(parseFloat(val)) || t('circuit.qta_not_valid'),
|
(val) => !isNaN(parseFloat(val)) || t('circuit.qta_not_valid'),
|
||||||
(val) => parseFloat(val) <= circuitStore.getRemainingCoinsToSend(accountloaded) || t('circuit.qta_remaining_to_send', { maxqta: circuitStore.getRemainingCoinsToSend(accountloaded), symbol: circuitloaded.symbol }),
|
(val) =>
|
||||||
|
parseFloat(val) <= circuitStore.getRemainingCoinsToSend(accountloaded) ||
|
||||||
|
t('circuit.qta_remaining_to_send', {
|
||||||
|
maxqta: circuitStore.getRemainingCoinsToSend(accountloaded),
|
||||||
|
symbol: circuitloaded.symbol,
|
||||||
|
}),
|
||||||
(val) => parseFloat(val) > 0 || t('circuit.qta_not_valid'),
|
(val) => parseFloat(val) > 0 || t('circuit.qta_not_valid'),
|
||||||
]"
|
]"
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
@@ -213,7 +259,10 @@
|
|||||||
<span class="currency-symbol">€</span>
|
<span class="currency-symbol">€</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<div class="coin-badge" :style="`background: ${circuitloaded.color || '#ff5500'}`">
|
<div
|
||||||
|
class="coin-badge"
|
||||||
|
:style="`background: ${circuitloaded.color || '#ff5500'}`"
|
||||||
|
>
|
||||||
{{ circuitloaded.symbol }}
|
{{ circuitloaded.symbol }}
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -243,8 +292,8 @@
|
|||||||
{{ $t('circuit.transactionsEnabled_text') }}
|
{{ $t('circuit.transactionsEnabled_text') }}
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
<!-- Note Section - Compatto -->
|
<!-- Note Section -->
|
||||||
<div v-if="circuitsel" class="section-block">
|
<div class="section-block">
|
||||||
<q-input
|
<q-input
|
||||||
ref="causalRef"
|
ref="causalRef"
|
||||||
v-model="causal"
|
v-model="causal"
|
||||||
@@ -276,20 +325,198 @@
|
|||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<!-- RECEIVE MODE CONTENT -->
|
||||||
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
|
<template v-if="isReceiveMode && circuitsel">
|
||||||
|
<!-- Info Banner -->
|
||||||
|
<q-banner v-if="riscallrec" class="info-banner q-mb-md">
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<q-icon name="info" color="primary" />
|
||||||
|
</template>
|
||||||
|
{{ riscallrec }}
|
||||||
|
</q-banner>
|
||||||
|
|
||||||
|
<!-- Tab Panels -->
|
||||||
|
<q-tab-panels
|
||||||
|
v-model="receiveType"
|
||||||
|
animated
|
||||||
|
keep-alive
|
||||||
|
class="receive-panels"
|
||||||
|
>
|
||||||
|
<!-- ═══════════════════════════════════════ -->
|
||||||
|
<!-- Panel: Link -->
|
||||||
|
<!-- ═══════════════════════════════════════ -->
|
||||||
|
<q-tab-panel name="link" class="q-pa-none">
|
||||||
|
<div class="link-panel">
|
||||||
|
<!-- Descrizione -->
|
||||||
|
<div class="panel-description q-mb-md">
|
||||||
|
<q-icon name="lightbulb" color="amber" size="20px" class="q-mr-sm" />
|
||||||
|
<span v-html="t('circuit.compila_il_tuo_link_per_ricevere_ris')"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form richiesta -->
|
||||||
|
<div class="request-form">
|
||||||
|
<!-- Nota/Causale -->
|
||||||
|
<q-input
|
||||||
|
v-model="receiveCausal"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
maxlength="120"
|
||||||
|
counter
|
||||||
|
:label="$t('circuit.note_richiedente')"
|
||||||
|
class="q-mb-md modern-textarea"
|
||||||
|
autogrow
|
||||||
|
:input-style="{ minHeight: '60px' }"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="message" color="grey-6" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- Importo richiesto -->
|
||||||
|
<div class="amount-section">
|
||||||
|
<label class="section-label">Importo richiesto (opzionale)</label>
|
||||||
|
<div class="amount-input-wrapper">
|
||||||
|
<q-input
|
||||||
|
v-model="receiveQty"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
:rules="[(val) => !val || val > 0 || 'Importo non valido']"
|
||||||
|
class="amount-input receive-amount"
|
||||||
|
@update:model-value="limitReceiveQuantity"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<span class="currency-symbol">€</span>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<div
|
||||||
|
class="coin-badge"
|
||||||
|
:style="`background: ${circuitloaded?.color || '#ff5500'}`"
|
||||||
|
>
|
||||||
|
{{ circuitloaded?.symbol || 'RIS' }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Link generato -->
|
||||||
|
<div class="generated-link-section q-mt-sm">
|
||||||
|
<label class="section-label">Il tuo link di pagamento</label>
|
||||||
|
<div class="link-preview">
|
||||||
|
<div class="link-text">
|
||||||
|
<q-icon name="link" color="primary" size="20px" class="q-mr-sm" />
|
||||||
|
<span>{{ truncateLink(generatedLink) }}</span>
|
||||||
|
</div>
|
||||||
|
<CCopyBtnSmall
|
||||||
|
:showLink="true"
|
||||||
|
:texttocopy="generatedLink"
|
||||||
|
:showTelegram="true"
|
||||||
|
:small="false"
|
||||||
|
:btn="true"
|
||||||
|
class="copy-btn"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════ -->
|
||||||
|
<!-- Panel: QR Code -->
|
||||||
|
<!-- ═══════════════════════════════════════ -->
|
||||||
|
<q-tab-panel name="qrcode" class="q-pa-none">
|
||||||
|
<div class="qrcode-panel">
|
||||||
|
<div class="qr-description q-mb-sm text-center">
|
||||||
|
<q-icon name="qr_code_scanner" size="32px" color="primary" class="q-mb-sm" />
|
||||||
|
<div class="text-subtitle1">Mostra questo QR Code</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Chi vuole pagarti può scansionarlo con la fotocamera
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CQRCode
|
||||||
|
:read="false"
|
||||||
|
textlink="Link Profilo"
|
||||||
|
:link="generatedLink"
|
||||||
|
:size="280"
|
||||||
|
class="qr-code-wrapper"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Quick amount presets -->
|
||||||
|
<div class="quick-amounts">
|
||||||
|
<label class="section-label text-center">Importo preimpostato</label>
|
||||||
|
<div class="amount-chips">
|
||||||
|
<q-chip
|
||||||
|
v-for="amount in quickAmounts"
|
||||||
|
:key="amount"
|
||||||
|
clickable
|
||||||
|
:outline="receiveQty !== amount.toString()"
|
||||||
|
:color="receiveQty === amount.toString() ? 'primary' : 'grey-4'"
|
||||||
|
:text-color="receiveQty === amount.toString() ? 'white' : 'grey-8'"
|
||||||
|
@click="setQuickAmount(amount)"
|
||||||
|
>
|
||||||
|
{{ amount }} {{ circuitloaded?.symbol || 'RIS' }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════ -->
|
||||||
|
<!-- Panel: Vetrina -->
|
||||||
|
<!-- ═══════════════════════════════════════ -->
|
||||||
|
<q-tab-panel name="showonlist" class="q-pa-none">
|
||||||
|
<div class="showonlist-panel">
|
||||||
|
<div class="empty-state">
|
||||||
|
<q-icon name="storefront" size="64px" color="grey-4" />
|
||||||
|
<div class="text-h6 text-grey-6 q-mt-sm">Vetrina</div>
|
||||||
|
<div class="text-body2 text-grey-5 q-mt-sm text-center">
|
||||||
|
Mostra i tuoi prodotti e servizi per ricevere pagamenti
|
||||||
|
</div>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
label="Configura vetrina"
|
||||||
|
class="q-mt-sm"
|
||||||
|
@click="configureShowcase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Stato iniziale - Nessun circuito selezionato (RECEIVE) -->
|
||||||
|
<template v-if="isReceiveMode && !circuitsel">
|
||||||
|
<div class="empty-state-circuit">
|
||||||
|
<q-icon name="account_balance_wallet" size="64px" color="grey-4" />
|
||||||
|
<div class="text-h6 text-grey-6 q-mt-sm">Seleziona un Circuito</div>
|
||||||
|
<div class="text-body2 text-grey-5 q-mt-sm text-center">
|
||||||
|
Per ricevere pagamenti, prima seleziona il circuito
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<!-- Fixed Bottom Actions -->
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
<q-card-actions
|
<!-- ACTIONS -->
|
||||||
class="dialog-actions"
|
<!-- ═══════════════════════════════════════════ -->
|
||||||
:class="{ 'fixed-bottom-actions': $q.screen.lt.sm }"
|
<q-card-actions class="dialog-actions col-shrink">
|
||||||
>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
:label="t('dialog.cancel')"
|
:label="t('dialog.cancel')"
|
||||||
class="cancel-btn"
|
class="cancel-btn"
|
||||||
v-close-popup
|
v-close-popup
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Send Button (solo SEND mode) -->
|
||||||
<q-btn
|
<q-btn
|
||||||
|
v-if="!isReceiveMode"
|
||||||
:disable="!ifNextCheck(step)"
|
:disable="!ifNextCheck(step)"
|
||||||
class="send-btn"
|
class="send-btn"
|
||||||
:class="{ 'btn-disabled': !ifNextCheck(step) }"
|
:class="{ 'btn-disabled': !ifNextCheck(step) }"
|
||||||
@@ -304,12 +531,23 @@
|
|||||||
/>
|
/>
|
||||||
<q-icon v-else name="send" class="q-ml-sm" />
|
<q-icon v-else name="send" class="q-ml-sm" />
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
|
<!-- Share Button (solo RECEIVE mode con circuito selezionato) -->
|
||||||
|
<q-btn
|
||||||
|
v-if="isReceiveMode && circuitsel"
|
||||||
|
class="send-btn receive-btn"
|
||||||
|
@click="shareLink"
|
||||||
|
>
|
||||||
|
<span class="send-btn-text">Condividi Link</span>
|
||||||
|
<q-icon name="share" class="q-ml-sm" />
|
||||||
|
</q-btn>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<!-- Numeric Keyboard Dialog -->
|
<!-- Numeric Keyboard Dialog (solo SEND mode) -->
|
||||||
<q-dialog
|
<q-dialog
|
||||||
|
v-if="!isReceiveMode"
|
||||||
v-model="showKeyboard"
|
v-model="showKeyboard"
|
||||||
position="bottom"
|
position="bottom"
|
||||||
seamless
|
seamless
|
||||||
@@ -319,19 +557,14 @@
|
|||||||
<q-card-section class="keyboard-header">
|
<q-card-section class="keyboard-header">
|
||||||
<div class="keyboard-header-content">
|
<div class="keyboard-header-content">
|
||||||
<span class="keyboard-title">Inserisci importo</span>
|
<span class="keyboard-title">Inserisci importo</span>
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
label="Fatto"
|
|
||||||
color="primary"
|
|
||||||
class="done-btn"
|
|
||||||
v-close-popup
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="keyboard-display">
|
<div class="keyboard-display">
|
||||||
<span class="keyboard-amount">{{ qty || '0' }}</span>
|
<span class="keyboard-amount">{{ qty || '0' }}</span>
|
||||||
<div class="keyboard-coin-badge" :style="`background: ${circuitloaded.color || '#ff5500'}`">
|
<div
|
||||||
|
class="keyboard-coin-badge"
|
||||||
|
:style="`background: ${circuitloaded.color || '#ff5500'}`"
|
||||||
|
>
|
||||||
{{ circuitloaded.symbol }}
|
{{ circuitloaded.symbol }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -344,6 +577,14 @@
|
|||||||
@update:model-value="setQty"
|
@update:model-value="setQty"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
<div class="row justify-center q-my-sm">
|
||||||
|
<q-btn
|
||||||
|
label="Chiudi"
|
||||||
|
color="primary"
|
||||||
|
class="done-btn"
|
||||||
|
v-close-popup
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
.my-custom-border {
|
|
||||||
border: 1px solid #ccc; /* Imposta il colore del bordo (puoi personalizzare) */
|
|
||||||
|
|
||||||
background-color: #fff; /* Colore sfondo per il contenitore (puoi personalizzare) */
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
import { computed, defineComponent, onMounted, PropType, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
import { ICalcStat, IOperators } from '../../model';
|
|
||||||
import { useUserStore } from '../../store/UserStore';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useGlobalStore } from '../../store/globalStore';
|
|
||||||
import { useCircuitStore } from '../../store/CircuitStore';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
import { shared_consts } from '@/common/shared_vuejs';
|
|
||||||
import { costanti, IMainCard } from '@store/Modules/costanti';
|
|
||||||
|
|
||||||
import { CMyUser } from '../CMyUser';
|
|
||||||
import { CTitleBanner } from '../CTitleBanner';
|
|
||||||
import { CMyGroup } from '../CMyGroup';
|
|
||||||
import { CCopyBtnSmall } from '../CCopyBtnSmall';
|
|
||||||
import { CContactUser } from '../CContactUser';
|
|
||||||
import { CQRCode } from '../CQRCode';
|
|
||||||
import { CFindUsers } from '../CFindUsers';
|
|
||||||
import { CUserInfoAccount } from '../CUserInfoAccount';
|
|
||||||
import { tools } from '@tools';
|
|
||||||
import { useQuasar } from 'quasar';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'CSendRISTo',
|
|
||||||
props: {},
|
|
||||||
components: {
|
|
||||||
CMyUser,
|
|
||||||
CMyGroup,
|
|
||||||
CUserInfoAccount,
|
|
||||||
CCopyBtnSmall,
|
|
||||||
CTitleBanner,
|
|
||||||
CContactUser,
|
|
||||||
CFindUsers,
|
|
||||||
CQRCode,
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const globalStore = useGlobalStore();
|
|
||||||
const circuitStore = useCircuitStore();
|
|
||||||
const { t } = useI18n();
|
|
||||||
const $q = useQuasar();
|
|
||||||
const $router = useRouter();
|
|
||||||
|
|
||||||
const showSendCoin = ref(false);
|
|
||||||
const showReceiveCoin = ref(false);
|
|
||||||
const receiveType = ref('link');
|
|
||||||
const riscallrec = ref(<string>'');
|
|
||||||
|
|
||||||
const btnInviaRIS = ref(null);
|
|
||||||
|
|
||||||
const optionsReceive = ref([
|
|
||||||
{
|
|
||||||
label: 'Condividi il tuo Link',
|
|
||||||
value: 'link',
|
|
||||||
},
|
|
||||||
/*{
|
|
||||||
label: 'Rendi visibile il tuo profilo per 8 ore',
|
|
||||||
value: 'showonlist',
|
|
||||||
},*/
|
|
||||||
{
|
|
||||||
label: 'Genera il QR Code',
|
|
||||||
value: 'qrcode',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const tipoConto = ref(shared_consts.AccountType.USER);
|
|
||||||
const loading = ref(false);
|
|
||||||
const miolink = ref('');
|
|
||||||
const sendRIS = ref('');
|
|
||||||
const qtyRIS = ref('');
|
|
||||||
const causal = ref('');
|
|
||||||
const circuitpath = computed(() => {
|
|
||||||
const circ = circuitStore.getCircuitByProvinceAndCard(
|
|
||||||
userStore.my.profile.resid_province,
|
|
||||||
userStore.my.profile.resid_card
|
|
||||||
);
|
|
||||||
return circ && circ.path ? circ.path : '';
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => qtyRIS.value,
|
|
||||||
(to: any, from: any) => {
|
|
||||||
limitQuantity();
|
|
||||||
miolink.value = userStore.getLinkProfileAndRIS('', qtyRIS.value, causal.value);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
() => causal.value,
|
|
||||||
(to: any, from: any) => {
|
|
||||||
miolink.value = userStore.getLinkProfileAndRIS('', qtyRIS.value, causal.value);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const showonreclist = ref(false);
|
|
||||||
|
|
||||||
const contact = computed(() => userStore.my);
|
|
||||||
|
|
||||||
const arrTypesAccounts = ref(<any>[
|
|
||||||
{
|
|
||||||
label: t('circuit.user'),
|
|
||||||
value: shared_consts.AccountType.USER,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('circuit.conticollettivi'),
|
|
||||||
value: shared_consts.AccountType.CONTO_DI_GRUPPO,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
function mounted() {
|
|
||||||
miolink.value = userStore.getLinkProfileAndRIS('', qtyRIS.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function limitQuantity() {
|
|
||||||
// Converte qtyRIS in stringa per verificare la lunghezza
|
|
||||||
if (qtyRIS.value.length > 5) {
|
|
||||||
qtyRIS.value = qtyRIS.value.substring(0, 5); // Limita a 5 caratteri
|
|
||||||
}
|
|
||||||
|
|
||||||
qtyRIS.value = qtyRIS.value.replace(',', '.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollaBottone() {
|
|
||||||
const btnInviaRIS = document.getElementById('btnInviaRIS');
|
|
||||||
if (btnInviaRIS) {
|
|
||||||
const offset = 30; // spazio desiderato in pixel sopra il bottone
|
|
||||||
const y = btnInviaRIS.getBoundingClientRect().top + window.scrollY - offset;
|
|
||||||
window.scrollTo({ top: y, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickInviaRIS() {
|
|
||||||
scrollaBottone();
|
|
||||||
|
|
||||||
showSendCoin.value = !showSendCoin.value;
|
|
||||||
if (showSendCoin.value) showReceiveCoin.value = !showSendCoin.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clickriceviRIS() {
|
|
||||||
scrollaBottone();
|
|
||||||
|
|
||||||
showReceiveCoin.value = !showReceiveCoin.value;
|
|
||||||
if (showReceiveCoin.value) showSendCoin.value = !showReceiveCoin.value;
|
|
||||||
if (showReceiveCoin.value) clickAddtoRecList();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clickAddtoRecList() {
|
|
||||||
const risultato = await tools.addToTemporaryReceiverRIS(t);
|
|
||||||
if (risultato) {
|
|
||||||
riscallrec.value = risultato.msg;
|
|
||||||
showonreclist.value = risultato.ris;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(mounted);
|
|
||||||
|
|
||||||
return {
|
|
||||||
userStore,
|
|
||||||
tools,
|
|
||||||
costanti,
|
|
||||||
shared_consts,
|
|
||||||
arrTypesAccounts,
|
|
||||||
tipoConto,
|
|
||||||
loading,
|
|
||||||
contact,
|
|
||||||
circuitpath,
|
|
||||||
sendRIS,
|
|
||||||
miolink,
|
|
||||||
qtyRIS,
|
|
||||||
t,
|
|
||||||
causal,
|
|
||||||
limitQuantity,
|
|
||||||
showSendCoin,
|
|
||||||
optionsReceive,
|
|
||||||
receiveType,
|
|
||||||
showReceiveCoin,
|
|
||||||
showonreclist,
|
|
||||||
riscallrec,
|
|
||||||
clickAddtoRecList,
|
|
||||||
clickInviaRIS,
|
|
||||||
clickriceviRIS,
|
|
||||||
btnInviaRIS,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="tools.visualizzaHomeApp()"
|
|
||||||
class="row text-center justify-evenly items-center"
|
|
||||||
>
|
|
||||||
<div class="q-mb-sm">
|
|
||||||
<q-btn
|
|
||||||
id="btnInviaRIS"
|
|
||||||
icon="fas fa-upload"
|
|
||||||
color="positive"
|
|
||||||
size="md"
|
|
||||||
rounded
|
|
||||||
:label="$t('circuit.sendcoins_toso')"
|
|
||||||
@click="clickInviaRIS"
|
|
||||||
:push="showSendCoin"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
icon="fas fa-download"
|
|
||||||
color="accent"
|
|
||||||
size="md"
|
|
||||||
rounded
|
|
||||||
:label="$t('circuit.receive_coins')"
|
|
||||||
@click="clickriceviRIS"
|
|
||||||
:push="showReceiveCoin"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-slide-transition>
|
|
||||||
<CTitleBanner
|
|
||||||
v-show="showReceiveCoin"
|
|
||||||
:class="`q-pa-xs `"
|
|
||||||
:title="$t('circuit.receive_coins')"
|
|
||||||
bgcolor="white"
|
|
||||||
bgcolor2="lightblue"
|
|
||||||
:clcolor="`text-indigo`"
|
|
||||||
:canopen="true"
|
|
||||||
:small="true"
|
|
||||||
:open="true"
|
|
||||||
>
|
|
||||||
<q-banner
|
|
||||||
v-show="riscallrec"
|
|
||||||
rounded
|
|
||||||
class="bg-blue text-white"
|
|
||||||
style="text-align: center"
|
|
||||||
>
|
|
||||||
{{ riscallrec }}
|
|
||||||
<br />
|
|
||||||
</q-banner>
|
|
||||||
|
|
||||||
<q-option-group
|
|
||||||
class="q-ma-xs"
|
|
||||||
style="text-align: left !important"
|
|
||||||
v-model="receiveType"
|
|
||||||
:options="optionsReceive"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
<q-tab-panels
|
|
||||||
v-model="receiveType"
|
|
||||||
animated
|
|
||||||
keep-alive
|
|
||||||
class="shadow-2 rounded-borders"
|
|
||||||
>
|
|
||||||
<q-tab-panel name="link">
|
|
||||||
<div class="row justify-center">
|
|
||||||
<div
|
|
||||||
class="q-ma-xs q-pa-xs"
|
|
||||||
v-html="t('circuit.compila_il_tuo_link_per_ricevere_ris')"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-model="causal"
|
|
||||||
rounded
|
|
||||||
filled
|
|
||||||
maxlength="120"
|
|
||||||
counter
|
|
||||||
:label="$t('circuit.note_richiedente')"
|
|
||||||
class="q-ma-sm full-width q-px-xs"
|
|
||||||
autogrow
|
|
||||||
dense
|
|
||||||
>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<q-icon name="description" class="q-mr-xs" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
class="q-mb-sm text-h5"
|
|
||||||
style="width: 180px"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
v-model="qtyRIS"
|
|
||||||
:type="'number'"
|
|
||||||
@input="limitQuantity"
|
|
||||||
:label="t('circuit.quantita')"
|
|
||||||
input-class="text-right"
|
|
||||||
input-style="padding-bottom: 14px !important;"
|
|
||||||
v-on:keyup.enter="$event.target.nextElementSibling.focus()"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
|
||||||
<div class="text-h6">
|
|
||||||
<em
|
|
||||||
class="q-px-sm text-white rounded-borders"
|
|
||||||
style="background-color: #ff5500"
|
|
||||||
>RIS</em
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<CCopyBtnSmall :texttocopy="miolink" :small="true" :btn="true">
|
|
||||||
</CCopyBtnSmall>
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="showonlist">
|
|
||||||
<!--<q-btn
|
|
||||||
v-if="!showonreclist"
|
|
||||||
icon="fas fa-upload"
|
|
||||||
color="positive"
|
|
||||||
size="md"
|
|
||||||
rounded
|
|
||||||
:label="$t('circuit.addtothereceiverlist')"
|
|
||||||
@click="clickAddtoRecList"
|
|
||||||
>
|
|
||||||
</q-btn>-->
|
|
||||||
|
|
||||||
<q-slide-transition> </q-slide-transition>
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="qrcode">
|
|
||||||
<CQRCode
|
|
||||||
:read="false"
|
|
||||||
textlink="Link Profilo"
|
|
||||||
:link="userStore.getLinkProfileAndRIS('', '', '')"
|
|
||||||
></CQRCode>
|
|
||||||
</q-tab-panel>
|
|
||||||
</q-tab-panels>
|
|
||||||
</CTitleBanner>
|
|
||||||
</q-slide-transition>
|
|
||||||
<!--<div class="q-mb-sm"></div>
|
|
||||||
|
|
||||||
|
|
||||||
<CUserInfoAccount
|
|
||||||
:user="contact"
|
|
||||||
:circuitpath="circuitpath"
|
|
||||||
:admin="false"
|
|
||||||
:onlysaldo="true"
|
|
||||||
/>-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<q-slide-transition>
|
|
||||||
<CTitleBanner
|
|
||||||
v-show="showSendCoin"
|
|
||||||
:class="`q-pa-xs `"
|
|
||||||
:title="$t('circuit.sendcoins')"
|
|
||||||
bgcolor="white"
|
|
||||||
bgcolor2="lightblue"
|
|
||||||
:clcolor="`text-indigo`"
|
|
||||||
:canopen="true"
|
|
||||||
:small="true"
|
|
||||||
:open="true"
|
|
||||||
>
|
|
||||||
<CFindUsers
|
|
||||||
:actionType="costanti.ACTIONTYPE.SEND_RIS"
|
|
||||||
:sendRIS="sendRIS"
|
|
||||||
>
|
|
||||||
</CFindUsers>
|
|
||||||
</CTitleBanner>
|
|
||||||
</q-slide-transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" src="./CSendRISTo.ts">
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import './CSendRISTo.scss';
|
|
||||||
</style>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export {default as CSendRISTo} from './CSendRISTo.vue'
|
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
<div class="chart-wrapper">
|
<div v-if="tools.isAdmin()" class="chart-wrapper">
|
||||||
<CLineChart :mydata="datastat.reg_daily" :title="t('stat.reg_daily')"
|
<CLineChart :mydata="datastat.reg_daily" :title="t('stat.reg_daily')"
|
||||||
color="blue" bordercolor="blue" :sum="true" :showMedia="true" />
|
color="blue" bordercolor="blue" :sum="true" :showMedia="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
156
src/components/Dashboard/ActivityFeed/ActivityFeed.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<div class="activity-feed">
|
||||||
|
<div v-if="activities.length === 0" class="empty-feed">
|
||||||
|
<q-icon name="history" size="32px" color="grey-4" />
|
||||||
|
<p>Nessuna attività recente</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="feed-list">
|
||||||
|
<div
|
||||||
|
v-for="activity in activities"
|
||||||
|
:key="activity.id"
|
||||||
|
class="activity-item"
|
||||||
|
>
|
||||||
|
<div class="activity-icon" :class="`icon-${activity.color}`">
|
||||||
|
<q-icon :name="activity.icon" size="16px" />
|
||||||
|
</div>
|
||||||
|
<div class="activity-content">
|
||||||
|
<p class="activity-title">{{ activity.title }}</p>
|
||||||
|
<span class="activity-time">{{ formatTime(activity.timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Activity {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
timestamp: string;
|
||||||
|
icon: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
activities: Activity[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formatTime = (timestamp: string) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
|
||||||
|
const minutes = Math.floor(diff / 60000);
|
||||||
|
const hours = Math.floor(diff / 3600000);
|
||||||
|
const days = Math.floor(diff / 86400000);
|
||||||
|
|
||||||
|
if (minutes < 1) return 'Adesso';
|
||||||
|
if (minutes < 60) return `${minutes} min fa`;
|
||||||
|
if (hours < 24) return `${hours} ore fa`;
|
||||||
|
if (days < 7) return `${days} giorni fa`;
|
||||||
|
|
||||||
|
return date.toLocaleDateString('it-IT', { day: 'numeric', month: 'short' });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.activity-feed {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-feed {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
color: #888;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.icon-positive {
|
||||||
|
background: rgba(76, 175, 80, 0.1);
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon-primary {
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon-amber {
|
||||||
|
background: rgba(255, 152, 0, 0.1);
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.icon-negative {
|
||||||
|
background: rgba(244, 67, 54, 0.1);
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.activity-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.activity-feed {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-item {
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-title {
|
||||||
|
color: #eee !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
171
src/components/Dashboard/QuickActions/QuickActions.vue
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<div class="quick-actions">
|
||||||
|
<div
|
||||||
|
v-for="action in actions"
|
||||||
|
:key="action.id"
|
||||||
|
class="action-card"
|
||||||
|
:class="action.colorClass"
|
||||||
|
@click="$emit('action', action.id)"
|
||||||
|
>
|
||||||
|
<div class="action-icon">
|
||||||
|
<q-icon :name="action.icon" size="32px" />
|
||||||
|
</div>
|
||||||
|
<div class="action-content">
|
||||||
|
<h3>{{ action.title }}</h3>
|
||||||
|
<p>{{ action.description }}</p>
|
||||||
|
</div>
|
||||||
|
<q-icon name="arrow_forward" size="20px" class="action-arrow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'action', actionId: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
id: 'new-poster',
|
||||||
|
icon: 'add_photo_alternate',
|
||||||
|
title: 'Nuova Locandina',
|
||||||
|
description: 'Crea un poster da zero o da template',
|
||||||
|
colorClass: 'action-primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai-generate',
|
||||||
|
icon: 'auto_awesome',
|
||||||
|
title: 'Genera con AI',
|
||||||
|
description: 'Crea immagini uniche con intelligenza artificiale',
|
||||||
|
colorClass: 'action-amber'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'browse-templates',
|
||||||
|
icon: 'dashboard_customize',
|
||||||
|
title: 'Esplora Template',
|
||||||
|
description: 'Sfoglia la libreria di modelli pronti',
|
||||||
|
colorClass: 'action-teal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'my-posters',
|
||||||
|
icon: 'collections',
|
||||||
|
title: 'Le Mie Locandine',
|
||||||
|
description: 'Visualizza e gestisci i tuoi poster',
|
||||||
|
colorClass: 'action-purple'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.quick-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.action-arrow {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.action-primary:hover { border-color: #667eea; }
|
||||||
|
&.action-amber:hover { border-color: #ff9800; }
|
||||||
|
&.action-teal:hover { border-color: #009688; }
|
||||||
|
&.action-purple:hover { border-color: #9c27b0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.action-primary & {
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-amber & {
|
||||||
|
background: linear-gradient(135deg, #ff9800, #f57c00);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-teal & {
|
||||||
|
background: linear-gradient(135deg, #009688, #00796b);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-purple & {
|
||||||
|
background: linear-gradient(135deg, #9c27b0, #7b1fa2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-arrow {
|
||||||
|
color: #ccc;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.action-card {
|
||||||
|
background: #1e1e1e;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #252525;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-content h3 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
307
src/components/Dashboard/RecentPosters/RecentPosters.vue
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
<template>
|
||||||
|
<div class="recent-posters">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="isLoading" class="posters-grid">
|
||||||
|
<div v-for="i in 6" :key="i" class="poster-skeleton">
|
||||||
|
<q-skeleton type="rect" class="skeleton-image" />
|
||||||
|
<div class="skeleton-content">
|
||||||
|
<q-skeleton type="text" width="70%" />
|
||||||
|
<q-skeleton type="text" width="40%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else-if="posters.length === 0" class="empty-state">
|
||||||
|
<q-icon name="image" size="64px" color="grey-4" />
|
||||||
|
<h3>Nessuna locandina</h3>
|
||||||
|
<p>Crea la tua prima locandina per vederla qui</p>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="add"
|
||||||
|
label="Crea Locandina"
|
||||||
|
@click="$router.push('/posters/poster-generator')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Posters Grid -->
|
||||||
|
<div v-else class="posters-grid">
|
||||||
|
<div
|
||||||
|
v-for="poster in posters"
|
||||||
|
:key="poster._id"
|
||||||
|
class="poster-card"
|
||||||
|
>
|
||||||
|
<div class="poster-image" @click="$emit('view', poster)">
|
||||||
|
<img
|
||||||
|
:src="poster.renderOutput?.png?.url || poster.renderOutput?.jpg?.url || '/placeholder.png'"
|
||||||
|
:alt="poster.name"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div class="poster-overlay">
|
||||||
|
<q-btn round color="white" text-color="dark" icon="visibility" size="sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-badge
|
||||||
|
v-if="poster.metadata?.isFavorite"
|
||||||
|
color="amber"
|
||||||
|
floating
|
||||||
|
class="favorite-badge"
|
||||||
|
>
|
||||||
|
<q-icon name="star" size="12px" />
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="poster-info">
|
||||||
|
<h3 :title="poster.name">{{ poster.name }}</h3>
|
||||||
|
<p class="poster-date">
|
||||||
|
<q-icon name="schedule" size="14px" />
|
||||||
|
{{ formatDate(poster.createdAt) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="poster-actions">
|
||||||
|
<q-btn flat dense round icon="download" size="sm" @click="$emit('download', poster)">
|
||||||
|
<q-tooltip>Scarica</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn flat dense round icon="edit" size="sm" @click="$emit('edit', poster)">
|
||||||
|
<q-tooltip>Modifica</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn flat dense round icon="more_vert" size="sm">
|
||||||
|
<q-menu>
|
||||||
|
<q-list dense>
|
||||||
|
<q-item clickable v-close-popup @click="$emit('view', poster)">
|
||||||
|
<q-item-section avatar><q-icon name="visibility" size="20px" /></q-item-section>
|
||||||
|
<q-item-section>Visualizza</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable v-close-popup @click="sharePoster(poster)">
|
||||||
|
<q-item-section avatar><q-icon name="share" size="20px" /></q-item-section>
|
||||||
|
<q-item-section>Condividi</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator />
|
||||||
|
<q-item clickable v-close-popup @click="deletePoster(poster)" class="text-negative">
|
||||||
|
<q-item-section avatar><q-icon name="delete" size="20px" /></q-item-section>
|
||||||
|
<q-item-section>Elimina</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
posters: any[];
|
||||||
|
isLoading: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'view', poster: any): void;
|
||||||
|
(e: 'download', poster: any): void;
|
||||||
|
(e: 'edit', poster: any): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
|
||||||
|
const minutes = Math.floor(diff / 60000);
|
||||||
|
const hours = Math.floor(diff / 3600000);
|
||||||
|
const days = Math.floor(diff / 86400000);
|
||||||
|
|
||||||
|
if (minutes < 60) return `${minutes} min fa`;
|
||||||
|
if (hours < 24) return `${hours} ore fa`;
|
||||||
|
if (days < 7) return `${days} giorni fa`;
|
||||||
|
|
||||||
|
return date.toLocaleDateString('it-IT', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sharePoster = async (poster: any) => {
|
||||||
|
try {
|
||||||
|
if (navigator.share) {
|
||||||
|
await navigator.share({
|
||||||
|
title: poster.name,
|
||||||
|
text: `Guarda la mia locandina: ${poster.content?.title || poster.name}`,
|
||||||
|
url: window.location.origin + `/posters/${poster._id}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await navigator.clipboard.writeText(window.location.origin + `/posters/${poster._id}`);
|
||||||
|
$q.notify({ type: 'positive', message: 'Link copiato!' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Share error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePoster = (poster: any) => {
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Elimina Locandina',
|
||||||
|
message: `Vuoi eliminare "${poster.name}"?`,
|
||||||
|
cancel: true
|
||||||
|
}).onOk(() => {
|
||||||
|
$q.notify({ type: 'info', message: 'Eliminazione...' });
|
||||||
|
// Implement delete
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.recent-posters {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posters-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1.25rem;
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.poster-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-image {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 3/4;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-badge {
|
||||||
|
top: 0.5rem !important;
|
||||||
|
right: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-info {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-date {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 0.5rem 0.75rem;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skeleton
|
||||||
|
.poster-skeleton {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.skeleton-image {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty State
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.poster-card,
|
||||||
|
.poster-skeleton,
|
||||||
|
.empty-state {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-info h3 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
207
src/components/Dashboard/StatsCards/StatsCards.vue
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<div class="stats-cards">
|
||||||
|
<div
|
||||||
|
v-for="card in statsCards"
|
||||||
|
:key="card.id"
|
||||||
|
class="stat-card"
|
||||||
|
:class="card.colorClass"
|
||||||
|
>
|
||||||
|
<div class="stat-icon">
|
||||||
|
<q-icon :name="card.icon" size="28px" />
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">
|
||||||
|
<template v-if="isLoading">
|
||||||
|
<q-skeleton type="text" width="60px" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ card.value }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="stat-label">{{ card.label }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-trend" v-if="card.trend">
|
||||||
|
<q-icon
|
||||||
|
:name="card.trend > 0 ? 'trending_up' : 'trending_down'"
|
||||||
|
:color="card.trend > 0 ? 'positive' : 'negative'"
|
||||||
|
/>
|
||||||
|
<span :class="card.trend > 0 ? 'text-positive' : 'text-negative'">
|
||||||
|
{{ Math.abs(card.trend) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Stats {
|
||||||
|
totalPosters: number;
|
||||||
|
postersThisMonth: number;
|
||||||
|
aiGenerations: number;
|
||||||
|
favoriteTemplates: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
stats: Stats;
|
||||||
|
isLoading: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const statsCards = computed(() => [
|
||||||
|
{
|
||||||
|
id: 'total',
|
||||||
|
icon: 'image',
|
||||||
|
label: 'Locandine Totali',
|
||||||
|
value: props.stats.totalPosters,
|
||||||
|
colorClass: 'card-primary',
|
||||||
|
trend: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'month',
|
||||||
|
icon: 'calendar_month',
|
||||||
|
label: 'Questo Mese',
|
||||||
|
value: props.stats.postersThisMonth,
|
||||||
|
colorClass: 'card-success',
|
||||||
|
trend: 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ai',
|
||||||
|
icon: 'auto_awesome',
|
||||||
|
label: 'Generazioni AI',
|
||||||
|
value: props.stats.aiGenerations,
|
||||||
|
colorClass: 'card-warning',
|
||||||
|
trend: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'templates',
|
||||||
|
icon: 'dashboard',
|
||||||
|
label: 'Template Usati',
|
||||||
|
value: props.stats.favoriteTemplates,
|
||||||
|
colorClass: 'card-info',
|
||||||
|
trend: null
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.stats-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: -3rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
margin-top: -2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 4px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.card-primary::before { background: #667eea; }
|
||||||
|
&.card-success::before { background: #4caf50; }
|
||||||
|
&.card-warning::before { background: #ff9800; }
|
||||||
|
&.card-info::before { background: #2196f3; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.card-primary & {
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-success & {
|
||||||
|
background: rgba(76, 175, 80, 0.1);
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-warning & {
|
||||||
|
background: rgba(255, 152, 0, 0.1);
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info & {
|
||||||
|
background: rgba(33, 150, 243, 0.1);
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-trend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.stat-card {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
181
src/components/Dashboard/TemplateShowcase/TemplateShowcase.vue
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<div class="template-showcase">
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="isLoading" class="templates-list">
|
||||||
|
<div v-for="i in 4" :key="i" class="template-skeleton">
|
||||||
|
<q-skeleton type="rect" width="60px" height="80px" />
|
||||||
|
<div class="skeleton-content">
|
||||||
|
<q-skeleton type="text" width="80%" />
|
||||||
|
<q-skeleton type="text" width="50%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Templates List -->
|
||||||
|
<div v-else class="templates-list">
|
||||||
|
<div
|
||||||
|
v-for="template in templates"
|
||||||
|
:key="template._id"
|
||||||
|
class="template-item"
|
||||||
|
@click="$emit('select', template)"
|
||||||
|
>
|
||||||
|
<div class="template-preview" :style="getPreviewStyle(template)" />
|
||||||
|
<div class="template-info">
|
||||||
|
<h4>{{ template.name }}</h4>
|
||||||
|
<p>{{ formatType(template.templateType) }}</p>
|
||||||
|
<div class="usage-count" v-if="template.metadata?.usageCount">
|
||||||
|
<q-icon name="trending_up" size="12px" />
|
||||||
|
{{ template.metadata.usageCount }} usi
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-icon name="chevron_right" size="20px" class="item-arrow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Browse All -->
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="primary"
|
||||||
|
label="Vedi tutti i template"
|
||||||
|
icon-right="arrow_forward"
|
||||||
|
class="browse-all-btn"
|
||||||
|
@click="$router.push('/templates')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
templates: any[];
|
||||||
|
isLoading: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'select', template: any): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formatType = (type: string) => {
|
||||||
|
return type.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviewStyle = (template: any) => {
|
||||||
|
if (template.thumbnailUrl) {
|
||||||
|
return {
|
||||||
|
backgroundImage: `url(${template.thumbnailUrl})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const palette = template.palette || {};
|
||||||
|
return {
|
||||||
|
background: `linear-gradient(135deg, ${palette.primary || '#667eea'}, ${palette.secondary || '#764ba2'})`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.template-showcase {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.templates-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
|
||||||
|
.item-arrow {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-preview {
|
||||||
|
width: 50px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.15rem 0 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #4caf50;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-arrow {
|
||||||
|
color: #ccc;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-skeleton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
|
||||||
|
.skeleton-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.browse-all-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.template-showcase {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-info h4 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
128
src/components/EventPosterGenerator/EventPosterGenerator.scss
Executable file
@@ -0,0 +1,128 @@
|
|||||||
|
// src/components/EventPosterGenerator.scss
|
||||||
|
.event-poster-generator {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-card {
|
||||||
|
border-radius: 28px !important;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--q-surface, #fafafa);
|
||||||
|
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 3rem 2rem !important;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.q-icon { text-shadow: 0 4px 15px rgba(0,0,0,0.4); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1.8rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-span-full { grid-column: 1 / -1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn-wrapper {
|
||||||
|
text-align: center;
|
||||||
|
margin: 2rem 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 1.2rem 2rem !important;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
box-shadow: 0 15px 35px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
padding: 2rem 1rem 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-preview {
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 30px 70px rgba(0, 0, 0, 0.4);
|
||||||
|
max-height: 85vh;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-section {
|
||||||
|
background: rgba(0,0,0,0.03);
|
||||||
|
padding: 3rem 2rem !important;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-item {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-img {
|
||||||
|
height: 280px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-title {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
||||||
|
color: white;
|
||||||
|
padding: 1.5rem 1rem 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: rgba(0,0,0,0.6) !important;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active, .fade-leave-active { transition: all 0.6s ease; }
|
||||||
|
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(40px); }
|
||||||
218
src/components/EventPosterGenerator/EventPosterGenerator.ts
Executable file
@@ -0,0 +1,218 @@
|
|||||||
|
// src/components/EventPosterGenerator.ts
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { Api } from 'app/src/store/Api';
|
||||||
|
|
||||||
|
interface PosterFavorite {
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
form: any;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useEventPosterGenerator() {
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
titolo: string;
|
||||||
|
descrizione: string;
|
||||||
|
data: string;
|
||||||
|
ora: string;
|
||||||
|
luogo: string;
|
||||||
|
contatti: string;
|
||||||
|
fotoDescrizione: string;
|
||||||
|
provider: string;
|
||||||
|
stile: string;
|
||||||
|
aspectRatio: '9:16' | '1:1' | '16:9';
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<FormData>({
|
||||||
|
titolo: '',
|
||||||
|
descrizione: '',
|
||||||
|
data: '',
|
||||||
|
ora: '',
|
||||||
|
luogo: '',
|
||||||
|
contatti: '',
|
||||||
|
fotoDescrizione: '',
|
||||||
|
stile: 'techno dark futuristico, colori neon viola verde nero, cyberpunk',
|
||||||
|
aspectRatio: '9:16',
|
||||||
|
provider: 'hf',
|
||||||
|
});
|
||||||
|
|
||||||
|
const stileOptions = [
|
||||||
|
{
|
||||||
|
label: 'Techno Dark Cyberpunk',
|
||||||
|
value:
|
||||||
|
'vertical event poster design, techno dark cyberpunk style, neon purple green black color palette, futuristic city at night, dramatic lighting, glitch effects, industrial atmosphere, bold graphic design, clean empty area for title and event details, no text, high quality, 4k. Locandina verticale per evento techno dark futuristico.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Latino Caliente',
|
||||||
|
value:
|
||||||
|
'vertical event poster design, latin party style, vibrant red gold yellow colors, warm lighting, festive and caliente atmosphere, dynamic abstract shapes suggesting dance and music, glossy textures, bold curved graphic elements, clear central area for title, date and location, no text, high quality. Locandina per evento latino, atmosfera caliente.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Elegante Black & Gold',
|
||||||
|
value:
|
||||||
|
'vertical luxury event poster, elegant black and gold minimal style, deep black background, metallic gold accents, soft reflections, clean and symmetrical composition, premium graphic design, stylish and refined look, large empty space for logo, main title and event info, no text, ultra high quality. Locandina elegante nero e oro, stile luxury premium.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Vintage Retrò',
|
||||||
|
value:
|
||||||
|
'vertical retro poster design, 70s 80s vintage style, warm pastel colors, slightly faded texture, paper grain, geometric shapes and wavy forms, nostalgic mood, groovy layout, central area left blank for text, no text, high quality graphic design. Locandina in stile retrò anni 70 80, atmosfera nostalgica.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reggaeton Tropical',
|
||||||
|
value:
|
||||||
|
'vertical party poster design, reggaeton tropical style, palm trees, beach vibes, sunset sky, vibrant neon pink turquoise yellow colors, summer atmosphere, dynamic and fluid shapes, fun and energetic look, clear space reserved for title, DJs and event info, no text, high quality. Locandina per festa reggaeton tropicale, mood estivo.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Minimalista Moderno',
|
||||||
|
value:
|
||||||
|
'vertical minimalist modern poster, swiss graphic design style, black and white, grid layout, a lot of white space, thin lines and simple geometric shapes, clean vector design, professional and contemporary look, central area kept empty for title, date and logo, no text, ultra clean, high quality. Locandina minimalista moderna bianco e nero.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Psichedelico Fluo',
|
||||||
|
value:
|
||||||
|
'vertical psychedelic poster design, bright neon fluorescent colors, acid palette, abstract liquid and swirling patterns, trippy visual effect, club and rave atmosphere, slightly grainy texture, highly detailed, composition with central zone left empty for event text, no text, high quality, 4k. Locandina psichedelica fluo, trip visivo.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cinema Hollywood',
|
||||||
|
value:
|
||||||
|
'vertical cinematic poster design, Hollywood movie poster style, dramatic lighting, strong contrasts, rich deep colors, epic composition with central focus, slight vignette, professional film poster look, space reserved for big title and credits area, no text, high quality. Locandina cinematografica in stile Hollywood, atmosfera epica.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const providersList = [
|
||||||
|
{ label: 'Ideogram', value: 'ideogram' },
|
||||||
|
{ label: 'FAL (a Pagamento)', value: 'fal' },
|
||||||
|
{ label: 'Hugging Face', value: 'hf' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const aspectRatios = [
|
||||||
|
{ label: 'Verticale Instagram (9:16)', value: '9:16' },
|
||||||
|
{ label: 'Quadrata (1:1)', value: '1:1' },
|
||||||
|
{ label: 'Orizzontale (16:9)', value: '16:9' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isGenerating = ref(false);
|
||||||
|
const posterUrl = ref('');
|
||||||
|
const favorites = ref<PosterFavorite[]>([]);
|
||||||
|
|
||||||
|
const requiredRule = (val: string) => !!val || 'Campo obbligatorio';
|
||||||
|
|
||||||
|
const isFavorite = computed(() => {
|
||||||
|
return favorites.value.some((f) => f.url === posterUrl.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadSavedData = () => {
|
||||||
|
const savedForm = localStorage.getItem('lastPosterForm');
|
||||||
|
const savedFavorites = localStorage.getItem('posterFavorites');
|
||||||
|
|
||||||
|
if (savedForm) Object.assign(form, JSON.parse(savedForm));
|
||||||
|
if (savedFavorites) favorites.value = JSON.parse(savedFavorites);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveForm = () => {
|
||||||
|
localStorage.setItem('lastPosterForm', JSON.stringify(form));
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePoster = async () => {
|
||||||
|
if (!form.titolo || !form.data || !form.luogo) {
|
||||||
|
$q.notify({ type: 'negative', message: 'Compila i campi obbligatori!' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveForm();
|
||||||
|
isGenerating.value = true;
|
||||||
|
|
||||||
|
const prompt = `Locandina evento italiana ${form.aspectRatio === '9:16' ? 'verticale Instagram' : form.aspectRatio === '1:1' ? 'quadrata' : 'orizzontale'},
|
||||||
|
stile ${form.stile},
|
||||||
|
titolo grande leggibile: "${form.titolo.toUpperCase()}",
|
||||||
|
sottotitolo: "${form.descrizione}",
|
||||||
|
DATA: ${form.data}${form.ora ? ' - ORE ' + form.ora : ''},
|
||||||
|
LUOGO: ${form.luogo},
|
||||||
|
in basso: "${form.contatti}",
|
||||||
|
immagine principale: ${form.fotoDescrizione || "atmosfera spettacolare dell'evento"},
|
||||||
|
design premium italiano, testo perfetto, qualità altissima, niente errori`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await Api.SendReq('/api/generateposter', 'POST', { ...form, prompt });
|
||||||
|
|
||||||
|
if (res && res.data?.imageUrl) {
|
||||||
|
posterUrl.value = res.data.imageUrl;
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Locandina creata! ✨',
|
||||||
|
icon: 'auto_awesome',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$q.notify({ type: 'negative', message: res.data?.msgerr });
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
$q.notify({ type: 'negative', message: err.message || 'Errore AI' });
|
||||||
|
} finally {
|
||||||
|
isGenerating.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFavorite = () => {
|
||||||
|
if (isFavorite.value) {
|
||||||
|
const index = favorites.value.findIndex((f) => f.url === posterUrl.value);
|
||||||
|
if (index > -1) favorites.value.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
favorites.value.unshift({
|
||||||
|
url: posterUrl.value,
|
||||||
|
title: form.titolo,
|
||||||
|
form: { ...form },
|
||||||
|
date: new Date().toLocaleDateString('it-IT'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
localStorage.setItem('posterFavorites', JSON.stringify(favorites.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFavorite = (index: number) => {
|
||||||
|
favorites.value.splice(index, 1);
|
||||||
|
localStorage.setItem('posterFavorites', JSON.stringify(favorites.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFavorite = (fav: PosterFavorite) => {
|
||||||
|
Object.assign(form, fav.form);
|
||||||
|
posterUrl.value = fav.url;
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadPoster = () => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = posterUrl.value;
|
||||||
|
a.download = `${form.titolo.replace(/[^a-z0-9]/gi, '_')}_locandina_2025.png`;
|
||||||
|
a.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetResult = () => {
|
||||||
|
posterUrl.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('Mounted EventPosterGenerator');
|
||||||
|
loadSavedData();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
stileOptions,
|
||||||
|
aspectRatios,
|
||||||
|
isGenerating,
|
||||||
|
posterUrl,
|
||||||
|
favorites,
|
||||||
|
isFavorite,
|
||||||
|
requiredRule,
|
||||||
|
generatePoster,
|
||||||
|
toggleFavorite,
|
||||||
|
removeFavorite,
|
||||||
|
loadFavorite,
|
||||||
|
downloadPoster,
|
||||||
|
resetResult,
|
||||||
|
providersList,
|
||||||
|
};
|
||||||
|
}
|
||||||
221
src/components/EventPosterGenerator/EventPosterGenerator.vue
Executable file
@@ -0,0 +1,221 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import useEventPosterGenerator from './EventPosterGenerator';
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
stileOptions,
|
||||||
|
providersList,
|
||||||
|
aspectRatios,
|
||||||
|
isGenerating,
|
||||||
|
posterUrl,
|
||||||
|
favorites,
|
||||||
|
isFavorite,
|
||||||
|
requiredRule,
|
||||||
|
generatePoster,
|
||||||
|
toggleFavorite,
|
||||||
|
removeFavorite,
|
||||||
|
loadFavorite,
|
||||||
|
downloadPoster,
|
||||||
|
resetResult,
|
||||||
|
} = useEventPosterGenerator();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="event-poster-generator">
|
||||||
|
<q-card
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
class="poster-card"
|
||||||
|
>
|
||||||
|
<q-card-section class="header-section">
|
||||||
|
<div class="text-h5 text-weight-bold flex items-center justify-center gap-3">
|
||||||
|
<q-icon
|
||||||
|
name="auto_awesome"
|
||||||
|
size="36px"
|
||||||
|
/>
|
||||||
|
Generatore Locandine AI 2025
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle1 opacity-90">
|
||||||
|
Gratis per sempre • Grok Image + Flux Schnell
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section class="q-pa-sm">
|
||||||
|
<q-form
|
||||||
|
@submit.prevent="generatePoster"
|
||||||
|
class="form-grid"
|
||||||
|
>
|
||||||
|
<q-input
|
||||||
|
v-model="form.titolo"
|
||||||
|
filled
|
||||||
|
label="Titolo Evento *"
|
||||||
|
:rules="[requiredRule]"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="form.descrizione"
|
||||||
|
filled
|
||||||
|
label="Sottotitolo / Claim"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="form.data"
|
||||||
|
filled
|
||||||
|
label="Data *"
|
||||||
|
:rules="[requiredRule]"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="form.ora"
|
||||||
|
filled
|
||||||
|
label="Ora"
|
||||||
|
mask="##:##"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="form.luogo"
|
||||||
|
filled
|
||||||
|
label="Luogo *"
|
||||||
|
:rules="[requiredRule]"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="form.contatti"
|
||||||
|
filled
|
||||||
|
label="Contatti & Prenotazioni"
|
||||||
|
class="col-span-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="form.fotoDescrizione"
|
||||||
|
filled
|
||||||
|
type="textarea"
|
||||||
|
rows="3"
|
||||||
|
label="Immagine principale (descrivila bene!)"
|
||||||
|
class="col-span-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-model="form.stile"
|
||||||
|
filled
|
||||||
|
:options="stileOptions"
|
||||||
|
label="Stile grafico"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-model="form.aspectRatio"
|
||||||
|
filled
|
||||||
|
:options="aspectRatios"
|
||||||
|
label="Formato"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-model="form.provider"
|
||||||
|
filled
|
||||||
|
:options="providersList"
|
||||||
|
label="Generatore Immagini"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
</q-form>
|
||||||
|
|
||||||
|
<div class="generate-btn-wrapper">
|
||||||
|
<q-btn
|
||||||
|
:loading="isGenerating"
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
icon="auto_awesome"
|
||||||
|
label="GENERA LOCANDINA MAGICA"
|
||||||
|
class="generate-btn"
|
||||||
|
@click="generatePoster"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- Risultato -->
|
||||||
|
<transition
|
||||||
|
name="fade"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
|
<q-card-section
|
||||||
|
v-if="posterUrl"
|
||||||
|
class="result-section q-pb-xl"
|
||||||
|
>
|
||||||
|
<q-img
|
||||||
|
:src="posterUrl"
|
||||||
|
class="poster-preview"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="actions-row">
|
||||||
|
<q-btn
|
||||||
|
color="positive"
|
||||||
|
icon="download"
|
||||||
|
label="Scarica HD"
|
||||||
|
size="lg"
|
||||||
|
@click="downloadPoster"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
color="amber"
|
||||||
|
icon="favorite"
|
||||||
|
:label="isFavorite ? 'Preferita ✓' : 'Aggiungi ai Preferiti'"
|
||||||
|
size="lg"
|
||||||
|
:color="isFavorite ? 'amber-7' : 'grey-6'"
|
||||||
|
@click="toggleFavorite"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
color="grey-7"
|
||||||
|
icon="refresh"
|
||||||
|
label="Nuova"
|
||||||
|
size="lg"
|
||||||
|
flat
|
||||||
|
@click="resetResult"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- Galleria Preferiti -->
|
||||||
|
<q-card-section
|
||||||
|
v-if="favorites.length > 0"
|
||||||
|
class="favorites-section"
|
||||||
|
>
|
||||||
|
<div class="text-h6 text-center q-mb-lg">
|
||||||
|
⭐ Le tue locandine preferite ({{ favorites.length }})
|
||||||
|
</div>
|
||||||
|
<div class="favorites-grid">
|
||||||
|
<div
|
||||||
|
v-for="(fav, index) in favorites"
|
||||||
|
:key="index"
|
||||||
|
class="favorite-item"
|
||||||
|
@click="loadFavorite(fav)"
|
||||||
|
>
|
||||||
|
<q-img
|
||||||
|
:src="fav.url"
|
||||||
|
class="favorite-img"
|
||||||
|
/>
|
||||||
|
<div class="favorite-title">{{ fav.title }}</div>
|
||||||
|
<q-btn
|
||||||
|
icon="delete"
|
||||||
|
flat
|
||||||
|
round
|
||||||
|
size="sm"
|
||||||
|
color="negative"
|
||||||
|
class="delete-btn"
|
||||||
|
@click.stop="removeFavorite(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-inner-loading
|
||||||
|
:showing="isGenerating"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
<q-spinner-ios size="80px" />
|
||||||
|
<div class="text-h6 q-mt-lg text-white">
|
||||||
|
Sto creando la tua locandina perfetta...
|
||||||
|
</div>
|
||||||
|
</q-inner-loading>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import './EventPosterGenerator.scss';
|
||||||
|
</style>
|
||||||
1
src/components/EventPosterGenerator/index.ts
Executable file
@@ -0,0 +1 @@
|
|||||||
|
export { default as EventPosterGenerator } from './EventPosterGenerator.vue'
|
||||||
271
src/components/PosterGenerator/PosterGenerator.scss
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
.poster-generator {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header
|
||||||
|
.generator-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Steps Indicator
|
||||||
|
.steps-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
position: relative;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
.step-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 2rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: default;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&.is-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover .step-number {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-completed {
|
||||||
|
.step-number {
|
||||||
|
background: #4caf50;
|
||||||
|
border-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
.step-number {
|
||||||
|
background: #667eea;
|
||||||
|
border-color: #667eea;
|
||||||
|
color: white;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #888;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 15%;
|
||||||
|
right: 15%;
|
||||||
|
top: calc(1.5rem + 20px);
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, #4caf50, #667eea);
|
||||||
|
border-radius: 2px;
|
||||||
|
z-index: 1;
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: #e0e0e0;
|
||||||
|
z-index: -1;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content
|
||||||
|
.generator-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-with-preview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
position: sticky;
|
||||||
|
top: 100px;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
&.preview-large {
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animations
|
||||||
|
.slide-fade-enter-active,
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.generator-header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-indicator {
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
.step-item {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.generator-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.poster-generator {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generator-header {
|
||||||
|
background: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-indicator {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-color: #404040;
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
background: #333;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
background: #2d2d2d;
|
||||||
|
}
|
||||||
|
}
|
||||||
391
src/components/PosterGenerator/PosterGenerator.ts
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
import { ref, reactive, computed, watch, onMounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { Api } from 'src/store/Api';
|
||||||
|
import type {
|
||||||
|
PosterContent,
|
||||||
|
PosterAssets,
|
||||||
|
PosterAsset,
|
||||||
|
PosterData,
|
||||||
|
GenerationStep
|
||||||
|
} from '../../types/poster.types';
|
||||||
|
|
||||||
|
interface Step {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDefaultContent = (): PosterContent => ({
|
||||||
|
title: '',
|
||||||
|
subtitle: '',
|
||||||
|
eventDate: '',
|
||||||
|
eventTime: '',
|
||||||
|
location: '',
|
||||||
|
contacts: '',
|
||||||
|
extraText: [],
|
||||||
|
customFields: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDefaultAssets = (): PosterAssets => ({
|
||||||
|
backgroundImage: null,
|
||||||
|
mainImage: null,
|
||||||
|
logos: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export function usePosterGenerator() {
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
// Steps configuration
|
||||||
|
const steps: Step[] = [
|
||||||
|
{ id: 'template', label: 'Template', icon: 'dashboard' },
|
||||||
|
{ id: 'content', label: 'Contenuti', icon: 'edit' },
|
||||||
|
{ id: 'images', label: 'Immagini', icon: 'image' },
|
||||||
|
{ id: 'export', label: 'Esporta', icon: 'download' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// State
|
||||||
|
const currentStep = ref(0);
|
||||||
|
const maxReachedStep = ref(0);
|
||||||
|
const selectedTemplate = ref<any>(null);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isGenerating = ref(false);
|
||||||
|
const showAiGenerator = ref(false);
|
||||||
|
const aiGeneratorTarget = ref<'backgroundImage' | 'mainImage'>('backgroundImage');
|
||||||
|
|
||||||
|
const posterData = reactive<PosterData>({
|
||||||
|
templateId: '',
|
||||||
|
name: '',
|
||||||
|
content: createDefaultContent(),
|
||||||
|
assets: createDefaultAssets(),
|
||||||
|
layerOverrides: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const generationSteps = ref<GenerationStep[]>([]);
|
||||||
|
const generationResult = ref<{ imageUrl: string; posterId: string } | null>(null);
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const stepLineStyle = computed(() => {
|
||||||
|
const progress = (currentStep.value / (steps.length - 1)) * 100;
|
||||||
|
return {
|
||||||
|
width: `${progress}%`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const canProceed = computed(() => {
|
||||||
|
switch (currentStep.value) {
|
||||||
|
case 0: // Template
|
||||||
|
return !!selectedTemplate.value;
|
||||||
|
case 1: // Content
|
||||||
|
return !!(posterData.content.title && posterData.content.eventDate && posterData.content.location);
|
||||||
|
case 2: // Images
|
||||||
|
return true; // Immagini opzionali
|
||||||
|
case 3: // Export
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const handleBack = () => {
|
||||||
|
if (currentStep.value > 0) {
|
||||||
|
prevStep();
|
||||||
|
} else {
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Uscire?',
|
||||||
|
message: 'I dati non salvati andranno persi. Vuoi uscire?',
|
||||||
|
cancel: true
|
||||||
|
}).onOk(() => {
|
||||||
|
router.push('/posters/Dashboard');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
if (currentStep.value < steps.length - 1 && canProceed.value) {
|
||||||
|
currentStep.value++;
|
||||||
|
maxReachedStep.value = Math.max(maxReachedStep.value, currentStep.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevStep = () => {
|
||||||
|
if (currentStep.value > 0) {
|
||||||
|
currentStep.value--;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToStep = (index: number) => {
|
||||||
|
if (index <= maxReachedStep.value) {
|
||||||
|
currentStep.value = index;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectTemplate = async (template: any) => {
|
||||||
|
selectedTemplate.value = template;
|
||||||
|
posterData.templateId = template._id;
|
||||||
|
posterData.name = `Poster - ${template.name}`;
|
||||||
|
|
||||||
|
// Auto-proceed se cliccato
|
||||||
|
setTimeout(() => {
|
||||||
|
if (canProceed.value) {
|
||||||
|
nextStep();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateContent = (content: Partial<PosterContent>) => {
|
||||||
|
Object.assign(posterData.content, content);
|
||||||
|
|
||||||
|
// Auto-update name
|
||||||
|
if (content.title && !posterData.name.includes(content.title)) {
|
||||||
|
posterData.name = content.title;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAssets = (assets: Partial<PosterAssets>) => {
|
||||||
|
Object.assign(posterData.assets, assets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAiGenerator = (assetType: 'backgroundImage' | 'mainImage') => {
|
||||||
|
aiGeneratorTarget.value = assetType;
|
||||||
|
showAiGenerator.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAiPromptHint = (assetType: string): string => {
|
||||||
|
if (!selectedTemplate.value?.defaultAiPromptHints) return '';
|
||||||
|
return selectedTemplate.value.defaultAiPromptHints[assetType] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAiImageGenerated = (result: { url: string; aiParams: any }) => {
|
||||||
|
const asset: PosterAsset = {
|
||||||
|
sourceType: 'ai',
|
||||||
|
url: result.url,
|
||||||
|
aiParams: result.aiParams
|
||||||
|
};
|
||||||
|
|
||||||
|
if (aiGeneratorTarget.value === 'backgroundImage') {
|
||||||
|
posterData.assets.backgroundImage = asset;
|
||||||
|
} else if (aiGeneratorTarget.value === 'mainImage') {
|
||||||
|
posterData.assets.mainImage = asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
showAiGenerator.value = false;
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Immagine generata con successo!',
|
||||||
|
icon: 'auto_awesome'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePoster = async () => {
|
||||||
|
isGenerating.value = true;
|
||||||
|
generationResult.value = null;
|
||||||
|
|
||||||
|
// Setup generation steps
|
||||||
|
generationSteps.value = [
|
||||||
|
{ id: 'validate', label: 'Validazione dati', status: 'pending' },
|
||||||
|
{ id: 'upload', label: 'Upload assets', status: 'pending' },
|
||||||
|
{ id: 'render', label: 'Rendering poster', status: 'pending' },
|
||||||
|
{ id: 'save', label: 'Salvataggio', status: 'pending' }
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Validate
|
||||||
|
updateGenerationStep('validate', 'processing');
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
|
if (!posterData.content.title || !posterData.content.eventDate) {
|
||||||
|
throw new Error('Compila tutti i campi obbligatori');
|
||||||
|
}
|
||||||
|
updateGenerationStep('validate', 'completed');
|
||||||
|
|
||||||
|
// Step 2: Upload assets
|
||||||
|
updateGenerationStep('upload', 'processing');
|
||||||
|
await delay(800);
|
||||||
|
updateGenerationStep('upload', 'completed');
|
||||||
|
|
||||||
|
// Step 3: Render
|
||||||
|
updateGenerationStep('render', 'processing');
|
||||||
|
|
||||||
|
const res = await Api.SendReq('/api/posters', 'POST', {
|
||||||
|
templateId: posterData.templateId,
|
||||||
|
name: posterData.name,
|
||||||
|
content: posterData.content,
|
||||||
|
assets: posterData.assets,
|
||||||
|
layerOverrides: posterData.layerOverrides,
|
||||||
|
autoRender: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res?.data?.success) {
|
||||||
|
throw new Error(res?.data?.error || 'Errore durante la generazione');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGenerationStep('render', 'completed');
|
||||||
|
|
||||||
|
// Step 4: Save
|
||||||
|
updateGenerationStep('save', 'processing');
|
||||||
|
await delay(500);
|
||||||
|
updateGenerationStep('save', 'completed');
|
||||||
|
|
||||||
|
// Success
|
||||||
|
generationResult.value = {
|
||||||
|
imageUrl: res.data.data.renderOutput?.png?.url || res.data.data.renderOutput?.jpg?.url,
|
||||||
|
posterId: res.data.data._id
|
||||||
|
};
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Locandina creata con successo! 🎉',
|
||||||
|
icon: 'celebration'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
const failedStep = generationSteps.value.find(s => s.status === 'processing');
|
||||||
|
if (failedStep) {
|
||||||
|
updateGenerationStep(failedStep.id, 'error', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.message || 'Errore durante la generazione',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isGenerating.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateGenerationStep = (
|
||||||
|
stepId: string,
|
||||||
|
status: GenerationStep['status'],
|
||||||
|
message?: string
|
||||||
|
) => {
|
||||||
|
const step = generationSteps.value.find(s => s.id === stepId);
|
||||||
|
if (step) {
|
||||||
|
step.status = status;
|
||||||
|
if (message) step.message = message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
const downloadPoster = async (format: 'png' | 'jpg' = 'png') => {
|
||||||
|
if (!generationResult.value?.posterId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `/api/posters/${generationResult.value.posterId}/download/${format}`;
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${posterData.name.replace(/[^a-z0-9]/gi, '_')}_poster.${format}`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Download avviato!',
|
||||||
|
icon: 'download'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Errore durante il download'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const savePoster = async () => {
|
||||||
|
if (!generationResult.value?.posterId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Api.SendReq(`/api/posters/${generationResult.value.posterId}/favorite`, 'POST');
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Poster salvato nei preferiti!',
|
||||||
|
icon: 'favorite'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Errore durante il salvataggio'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetGenerator = () => {
|
||||||
|
$q.dialog({
|
||||||
|
title: 'Nuovo Poster',
|
||||||
|
message: 'Vuoi creare un nuovo poster? I dati attuali andranno persi.',
|
||||||
|
cancel: true
|
||||||
|
}).onOk(() => {
|
||||||
|
currentStep.value = 0;
|
||||||
|
maxReachedStep.value = 0;
|
||||||
|
selectedTemplate.value = null;
|
||||||
|
posterData.templateId = '';
|
||||||
|
posterData.name = '';
|
||||||
|
posterData.content = createDefaultContent();
|
||||||
|
posterData.assets = createDefaultAssets();
|
||||||
|
posterData.layerOverrides = {};
|
||||||
|
generationSteps.value = [];
|
||||||
|
generationResult.value = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load template if ID in URL
|
||||||
|
onMounted(async () => {
|
||||||
|
const templateId = route.query.template as string;
|
||||||
|
if (templateId) {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await Api.SendReq(`/api/templates/${templateId}`, 'GET');
|
||||||
|
if (res?.data?.success) {
|
||||||
|
selectTemplate(res.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading template:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
currentStep,
|
||||||
|
maxReachedStep,
|
||||||
|
steps,
|
||||||
|
stepLineStyle,
|
||||||
|
selectedTemplate,
|
||||||
|
posterData,
|
||||||
|
isLoading,
|
||||||
|
isGenerating,
|
||||||
|
generationSteps,
|
||||||
|
generationResult,
|
||||||
|
showAiGenerator,
|
||||||
|
aiGeneratorTarget,
|
||||||
|
canProceed,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
handleBack,
|
||||||
|
nextStep,
|
||||||
|
prevStep,
|
||||||
|
goToStep,
|
||||||
|
selectTemplate,
|
||||||
|
updateContent,
|
||||||
|
updateAssets,
|
||||||
|
openAiGenerator,
|
||||||
|
getAiPromptHint,
|
||||||
|
onAiImageGenerated,
|
||||||
|
generatePoster,
|
||||||
|
downloadPoster,
|
||||||
|
savePoster,
|
||||||
|
resetGenerator
|
||||||
|
};
|
||||||
|
}
|
||||||
207
src/components/PosterGenerator/PosterGenerator.vue
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<div class="poster-generator">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="generator-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<q-btn flat round icon="arrow_back" @click="handleBack" />
|
||||||
|
<div class="header-title">
|
||||||
|
<h1>Crea Locandina</h1>
|
||||||
|
<span class="subtitle" v-if="selectedTemplate">
|
||||||
|
Template: {{ selectedTemplate.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
<q-btn
|
||||||
|
v-if="currentStep > 0"
|
||||||
|
flat
|
||||||
|
icon="arrow_back"
|
||||||
|
label="Indietro"
|
||||||
|
@click="prevStep"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="currentStep < steps.length - 1"
|
||||||
|
color="primary"
|
||||||
|
icon-right="arrow_forward"
|
||||||
|
label="Avanti"
|
||||||
|
:disable="!canProceed"
|
||||||
|
@click="nextStep"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="currentStep === steps.length - 1"
|
||||||
|
color="positive"
|
||||||
|
icon="download"
|
||||||
|
label="Genera & Scarica"
|
||||||
|
:loading="isGenerating"
|
||||||
|
@click="generatePoster"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Progress Steps -->
|
||||||
|
<div class="steps-indicator">
|
||||||
|
<div
|
||||||
|
v-for="(step, index) in steps"
|
||||||
|
:key="step.id"
|
||||||
|
class="step-item"
|
||||||
|
:class="{
|
||||||
|
'is-active': index === currentStep,
|
||||||
|
'is-completed': index < currentStep,
|
||||||
|
'is-clickable': index <= maxReachedStep
|
||||||
|
}"
|
||||||
|
@click="goToStep(index)"
|
||||||
|
>
|
||||||
|
<div class="step-number">
|
||||||
|
<q-icon v-if="index < currentStep" name="check" size="18px" />
|
||||||
|
<span v-else>{{ index + 1 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-label">{{ step.label }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-line" :style="stepLineStyle" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="generator-content">
|
||||||
|
<!-- Step 1: Template Selection -->
|
||||||
|
<transition name="slide-fade" mode="out-in">
|
||||||
|
<div v-if="currentStep === 0" key="step-1" class="step-content">
|
||||||
|
<TemplateSelector
|
||||||
|
:selected-template="selectedTemplate"
|
||||||
|
@select="selectTemplate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Content -->
|
||||||
|
<div v-else-if="currentStep === 1" key="step-2" class="step-content step-with-preview">
|
||||||
|
<div class="form-section">
|
||||||
|
<ContentForm
|
||||||
|
:content="posterData.content"
|
||||||
|
:template="selectedTemplate"
|
||||||
|
@update="updateContent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="preview-section">
|
||||||
|
<LivePreview
|
||||||
|
:template="selectedTemplate"
|
||||||
|
:content="posterData.content"
|
||||||
|
:assets="posterData.assets"
|
||||||
|
:layer-overrides="posterData.layerOverrides"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Images -->
|
||||||
|
<div v-else-if="currentStep === 2" key="step-3" class="step-content step-with-preview">
|
||||||
|
<div class="form-section">
|
||||||
|
<AssetManager
|
||||||
|
:assets="posterData.assets"
|
||||||
|
:template="selectedTemplate"
|
||||||
|
@update="updateAssets"
|
||||||
|
@generate-ai="openAiGenerator"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="preview-section">
|
||||||
|
<LivePreview
|
||||||
|
:template="selectedTemplate"
|
||||||
|
:content="posterData.content"
|
||||||
|
:assets="posterData.assets"
|
||||||
|
:layer-overrides="posterData.layerOverrides"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 4: Review & Export -->
|
||||||
|
<div v-else-if="currentStep === 3" key="step-4" class="step-content step-with-preview">
|
||||||
|
<div class="form-section">
|
||||||
|
<ExportPanel
|
||||||
|
:poster-data="posterData"
|
||||||
|
:template="selectedTemplate"
|
||||||
|
:is-generating="isGenerating"
|
||||||
|
:generation-steps="generationSteps"
|
||||||
|
:result="generationResult"
|
||||||
|
@generate="generatePoster"
|
||||||
|
@download="downloadPoster"
|
||||||
|
@save="savePoster"
|
||||||
|
@reset="resetGenerator"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="preview-section preview-large">
|
||||||
|
<LivePreview
|
||||||
|
:template="selectedTemplate"
|
||||||
|
:content="posterData.content"
|
||||||
|
:assets="posterData.assets"
|
||||||
|
:layer-overrides="posterData.layerOverrides"
|
||||||
|
:final-image-url="generationResult?.imageUrl"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI Image Generator Dialog -->
|
||||||
|
<q-dialog v-model="showAiGenerator" persistent maximized transition-show="slide-up">
|
||||||
|
<AiImageGenerator
|
||||||
|
:asset-type="aiGeneratorTarget"
|
||||||
|
:template="selectedTemplate"
|
||||||
|
:initial-prompt="getAiPromptHint(aiGeneratorTarget)"
|
||||||
|
@generated="onAiImageGenerated"
|
||||||
|
@close="showAiGenerator = false"
|
||||||
|
/>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<!-- Loading Overlay -->
|
||||||
|
<q-inner-loading :showing="isLoading">
|
||||||
|
<q-spinner-gears size="80px" color="primary" />
|
||||||
|
<p class="q-mt-md text-h6">Caricamento...</p>
|
||||||
|
</q-inner-loading>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePosterGenerator } from './PosterGenerator';
|
||||||
|
import TemplateSelector from './components/TemplateSelector/TemplateSelector.vue';
|
||||||
|
import ContentForm from './components/ContentForm/ContentForm.vue';
|
||||||
|
import AssetManager from './components/AssetManager/AssetManager.vue';
|
||||||
|
import AiImageGenerator from './components/AiImageGenerator/AiImageGenerator.vue';
|
||||||
|
import LivePreview from './components/LivePreview/LivePreview.vue';
|
||||||
|
import ExportPanel from './components/ExportPanel/ExportPanel.vue';
|
||||||
|
|
||||||
|
const {
|
||||||
|
// State
|
||||||
|
currentStep,
|
||||||
|
maxReachedStep,
|
||||||
|
steps,
|
||||||
|
stepLineStyle,
|
||||||
|
selectedTemplate,
|
||||||
|
posterData,
|
||||||
|
isLoading,
|
||||||
|
isGenerating,
|
||||||
|
generationSteps,
|
||||||
|
generationResult,
|
||||||
|
showAiGenerator,
|
||||||
|
aiGeneratorTarget,
|
||||||
|
canProceed,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
handleBack,
|
||||||
|
nextStep,
|
||||||
|
prevStep,
|
||||||
|
goToStep,
|
||||||
|
selectTemplate,
|
||||||
|
updateContent,
|
||||||
|
updateAssets,
|
||||||
|
openAiGenerator,
|
||||||
|
getAiPromptHint,
|
||||||
|
onAiImageGenerated,
|
||||||
|
generatePoster,
|
||||||
|
downloadPoster,
|
||||||
|
savePoster,
|
||||||
|
resetGenerator
|
||||||
|
} = usePosterGenerator();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import './PosterGenerator.scss';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,749 @@
|
|||||||
|
<template>
|
||||||
|
<q-card class="ai-generator-dialog">
|
||||||
|
<!-- Header -->
|
||||||
|
<q-card-section class="dialog-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<q-icon name="auto_awesome" size="32px" color="amber" />
|
||||||
|
<div>
|
||||||
|
<h2>Genera Immagine con AI</h2>
|
||||||
|
<p>{{ assetTypeLabel }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-btn flat round icon="close" @click="$emit('close')" />
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<q-card-section class="dialog-body">
|
||||||
|
<div class="generator-layout">
|
||||||
|
<!-- Left: Form -->
|
||||||
|
<div class="form-panel">
|
||||||
|
<!-- Provider Selection -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">Provider AI</label>
|
||||||
|
<div class="provider-options">
|
||||||
|
<div
|
||||||
|
v-for="provider in providers"
|
||||||
|
:key="provider.value"
|
||||||
|
class="provider-option"
|
||||||
|
:class="{ 'is-selected': selectedProvider === provider.value }"
|
||||||
|
@click="selectedProvider = provider.value"
|
||||||
|
>
|
||||||
|
<q-icon :name="provider.icon" size="24px" />
|
||||||
|
<div class="provider-info">
|
||||||
|
<span class="provider-name">{{ provider.label }}</span>
|
||||||
|
<q-badge
|
||||||
|
v-if="provider.free"
|
||||||
|
color="green"
|
||||||
|
text-color="white"
|
||||||
|
label="Gratis"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prompt -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">
|
||||||
|
Descrivi l'immagine
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
icon="help_outline"
|
||||||
|
@click="showPromptTips = true"
|
||||||
|
>
|
||||||
|
<q-tooltip>Suggerimenti per prompt efficaci</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</label>
|
||||||
|
<q-input
|
||||||
|
v-model="prompt"
|
||||||
|
filled
|
||||||
|
type="textarea"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Descrivi l'immagine che vuoi generare..."
|
||||||
|
counter
|
||||||
|
maxlength="1000"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Quick prompts -->
|
||||||
|
<div class="quick-prompts">
|
||||||
|
<span class="quick-label">Suggerimenti rapidi:</span>
|
||||||
|
<q-chip
|
||||||
|
v-for="(suggestion, idx) in promptSuggestions"
|
||||||
|
:key="idx"
|
||||||
|
clickable
|
||||||
|
size="sm"
|
||||||
|
@click="appendToPrompt(suggestion)"
|
||||||
|
>
|
||||||
|
{{ suggestion }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Negative Prompt -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">
|
||||||
|
Prompt Negativo
|
||||||
|
<span class="optional">(opzionale)</span>
|
||||||
|
</label>
|
||||||
|
<q-input
|
||||||
|
v-model="negativePrompt"
|
||||||
|
filled
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Cosa NON vuoi vedere nell'immagine..."
|
||||||
|
/>
|
||||||
|
<div class="negative-presets">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="sm"
|
||||||
|
label="Usa preset standard"
|
||||||
|
@click="useStandardNegative"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aspect Ratio -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">Formato</label>
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="aspectRatio"
|
||||||
|
:options="aspectRatioOptions"
|
||||||
|
spread
|
||||||
|
no-caps
|
||||||
|
toggle-color="primary"
|
||||||
|
class="full-width"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Options -->
|
||||||
|
<q-expansion-item
|
||||||
|
icon="tune"
|
||||||
|
label="Opzioni Avanzate"
|
||||||
|
header-class="advanced-header"
|
||||||
|
>
|
||||||
|
<div class="advanced-options">
|
||||||
|
<div class="options-row">
|
||||||
|
<q-input
|
||||||
|
v-model.number="seed"
|
||||||
|
type="number"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="Seed"
|
||||||
|
hint="Lascia vuoto per casuale"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model.number="steps"
|
||||||
|
type="number"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="Steps"
|
||||||
|
:min="10"
|
||||||
|
:max="50"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model.number="cfg"
|
||||||
|
type="number"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
label="CFG Scale"
|
||||||
|
:min="1"
|
||||||
|
:max="20"
|
||||||
|
step="0.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-expansion-item>
|
||||||
|
|
||||||
|
<!-- Generate Button -->
|
||||||
|
<div class="generate-actions">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="auto_awesome"
|
||||||
|
label="Genera Immagine"
|
||||||
|
size="lg"
|
||||||
|
:loading="isGenerating"
|
||||||
|
:disable="!prompt.trim()"
|
||||||
|
class="full-width"
|
||||||
|
@click="generateImage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Preview -->
|
||||||
|
<div class="preview-panel">
|
||||||
|
<div class="preview-container" :class="{ 'has-image': !!generatedImage }">
|
||||||
|
<template v-if="isGenerating">
|
||||||
|
<div class="generating-state">
|
||||||
|
<q-spinner-orbit size="80px" color="primary" />
|
||||||
|
<p>Generazione in corso...</p>
|
||||||
|
<span class="time-estimate">Tempo stimato: ~15-30 secondi</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="generatedImage">
|
||||||
|
<img :src="generatedImage" alt="Generated image" class="preview-image" />
|
||||||
|
<div class="preview-actions">
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
color="white"
|
||||||
|
text-color="primary"
|
||||||
|
icon="refresh"
|
||||||
|
@click="generateImage"
|
||||||
|
>
|
||||||
|
<q-tooltip>Rigenera</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
color="green"
|
||||||
|
icon="check"
|
||||||
|
@click="confirmImage"
|
||||||
|
>
|
||||||
|
<q-tooltip>Usa questa immagine</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="empty-preview">
|
||||||
|
<q-icon name="image" size="80px" color="grey-4" />
|
||||||
|
<p>L'immagine generata apparirà qui</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generation History -->
|
||||||
|
<div v-if="history.length > 0" class="generation-history">
|
||||||
|
<h4>Generazioni recenti</h4>
|
||||||
|
<div class="history-grid">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in history"
|
||||||
|
:key="idx"
|
||||||
|
class="history-item"
|
||||||
|
@click="selectFromHistory(item)"
|
||||||
|
>
|
||||||
|
<img :src="item.url" :alt="`Generation ${idx + 1}`" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- Prompt Tips Dialog -->
|
||||||
|
<q-dialog v-model="showPromptTips">
|
||||||
|
<q-card style="max-width: 500px">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">💡 Suggerimenti per Prompt Efficaci</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<ul class="tips-list">
|
||||||
|
<li><strong>Sii specifico:</strong> Descrivi dettagli come colori, stile, atmosfera</li>
|
||||||
|
<li><strong>Indica lo stile:</strong> "fotorealistico", "illustrazione", "acquerello"</li>
|
||||||
|
<li><strong>Specifica la qualità:</strong> "high quality", "4k", "detailed"</li>
|
||||||
|
<li><strong>Evita il testo:</strong> Aggiungi sempre "no text, no letters"</li>
|
||||||
|
<li><strong>Composizione:</strong> Indica "central composition", "clean layout"</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="example-prompt q-mt-md">
|
||||||
|
<strong>Esempio:</strong>
|
||||||
|
<p class="q-mt-sm">"Mystical autumn forest at golden hour, morning mist between oak trees, photorealistic, cinematic lighting, warm colors, high quality, no text, no letters"</p>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="Capito!" color="primary" v-close-popup />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { Api } from 'src/store/Api';
|
||||||
|
import { PROVIDER_OPTIONS, ASPECT_RATIO_OPTIONS } from '../../../../types/poster.types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
assetType: 'backgroundImage' | 'mainImage';
|
||||||
|
template: any;
|
||||||
|
initialPrompt?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'generated', result: { url: string; aiParams: any }): void;
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const selectedProvider = ref('hf');
|
||||||
|
const prompt = ref('');
|
||||||
|
const negativePrompt = ref('');
|
||||||
|
const aspectRatio = ref('9:16');
|
||||||
|
const seed = ref<number | null>(null);
|
||||||
|
const steps = ref(28);
|
||||||
|
const cfg = ref(7.5);
|
||||||
|
const isGenerating = ref(false);
|
||||||
|
const generatedImage = ref<string | null>(null);
|
||||||
|
const history = ref<{ url: string; prompt: string }[]>([]);
|
||||||
|
const showPromptTips = ref(false);
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const assetTypeLabel = computed(() => {
|
||||||
|
return props.assetType === 'backgroundImage' ? 'Immagine di sfondo' : 'Immagine principale';
|
||||||
|
});
|
||||||
|
|
||||||
|
const providers = computed(() => PROVIDER_OPTIONS);
|
||||||
|
|
||||||
|
const aspectRatioOptions = computed(() =>
|
||||||
|
ASPECT_RATIO_OPTIONS.map(opt => ({
|
||||||
|
label: opt.label,
|
||||||
|
value: opt.value
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const promptSuggestions = computed(() => {
|
||||||
|
const suggestions = [
|
||||||
|
'high quality, 4k',
|
||||||
|
'cinematic lighting',
|
||||||
|
'no text, no letters',
|
||||||
|
'photorealistic',
|
||||||
|
'warm colors',
|
||||||
|
'dramatic atmosphere'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add template-specific suggestions
|
||||||
|
if (props.template?.defaultAiPromptHints?.[props.assetType]) {
|
||||||
|
const hint = props.template.defaultAiPromptHints[props.assetType];
|
||||||
|
const words = hint.split(',').slice(0, 3).map((w: string) => w.trim());
|
||||||
|
return [...words, ...suggestions.slice(0, 3)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return suggestions;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const appendToPrompt = (text: string) => {
|
||||||
|
if (prompt.value) {
|
||||||
|
prompt.value += ', ' + text;
|
||||||
|
} else {
|
||||||
|
prompt.value = text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStandardNegative = () => {
|
||||||
|
negativePrompt.value = 'text, letters, words, watermark, signature, blurry, low quality, distorted, ugly, bad anatomy, disfigured';
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateImage = async () => {
|
||||||
|
if (!prompt.value.trim()) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Inserisci una descrizione per l\'immagine'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isGenerating.value = true;
|
||||||
|
generatedImage.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await Api.SendReq('/api/assets/generate-ai', 'POST', {
|
||||||
|
prompt: prompt.value,
|
||||||
|
negativePrompt: negativePrompt.value,
|
||||||
|
provider: selectedProvider.value,
|
||||||
|
aspectRatio: aspectRatio.value,
|
||||||
|
category: props.assetType === 'backgroundImage' ? 'background' : 'main',
|
||||||
|
seed: seed.value,
|
||||||
|
steps: steps.value,
|
||||||
|
cfg: cfg.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.data?.success) {
|
||||||
|
generatedImage.value = res.data.data.file.url;
|
||||||
|
|
||||||
|
// Add to history
|
||||||
|
history.value.unshift({
|
||||||
|
url: res.data.data.file.url,
|
||||||
|
prompt: prompt.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 6
|
||||||
|
if (history.value.length > 6) {
|
||||||
|
history.value = history.value.slice(0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Immagine generata con successo!',
|
||||||
|
icon: 'auto_awesome'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(res?.data?.error || 'Errore durante la generazione');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.message || 'Errore durante la generazione',
|
||||||
|
icon: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isGenerating.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmImage = () => {
|
||||||
|
if (!generatedImage.value) return;
|
||||||
|
|
||||||
|
emit('generated', {
|
||||||
|
url: generatedImage.value,
|
||||||
|
aiParams: {
|
||||||
|
prompt: prompt.value,
|
||||||
|
negativePrompt: negativePrompt.value,
|
||||||
|
provider: selectedProvider.value,
|
||||||
|
aspectRatio: aspectRatio.value,
|
||||||
|
seed: seed.value,
|
||||||
|
steps: steps.value,
|
||||||
|
cfg: cfg.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectFromHistory = (item: { url: string; prompt: string }) => {
|
||||||
|
generatedImage.value = item.url;
|
||||||
|
prompt.value = item.prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize with hint
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.initialPrompt) {
|
||||||
|
prompt.value = props.initialPrompt;
|
||||||
|
}
|
||||||
|
useStandardNegative();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ai-generator-dialog {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generator-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form Panel
|
||||||
|
.form-panel {
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
.optional {
|
||||||
|
font-weight: 400;
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider Options
|
||||||
|
.provider-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 140px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-name {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick Prompts
|
||||||
|
.quick-prompts {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
|
||||||
|
.quick-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.negative-presets {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-header {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-options {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-actions {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview Panel
|
||||||
|
.preview-panel {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 400px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.has-image {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.generating-state,
|
||||||
|
.empty-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #888;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-estimate {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-actions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// History
|
||||||
|
.generation-history {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tips Dialog
|
||||||
|
.tips-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-prompt {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.form-panel {
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-option {
|
||||||
|
border-color: #444;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.is-selected {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
background: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-header {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-prompt {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
<template>
|
||||||
|
<div class="asset-manager">
|
||||||
|
<div class="panel-header">
|
||||||
|
<q-icon name="collections" size="28px" color="primary" />
|
||||||
|
<div>
|
||||||
|
<h3>Immagini</h3>
|
||||||
|
<p>Carica o genera con AI le immagini del poster</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="assets-body">
|
||||||
|
<!-- Background Image -->
|
||||||
|
<div class="asset-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">
|
||||||
|
<q-icon name="wallpaper" />
|
||||||
|
<span>Sfondo</span>
|
||||||
|
</div>
|
||||||
|
<q-chip size="sm" :color="assets.backgroundImage ? 'green' : 'grey'" text-color="white">
|
||||||
|
{{ assets.backgroundImage ? 'Impostato' : 'Opzionale' }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AssetUploader
|
||||||
|
:asset="assets.backgroundImage"
|
||||||
|
placeholder-icon="wallpaper"
|
||||||
|
placeholder-text="Trascina un'immagine o clicca per caricare"
|
||||||
|
accept="image/*"
|
||||||
|
@upload="file => uploadAsset('backgroundImage', file)"
|
||||||
|
@remove="removeAsset('backgroundImage')"
|
||||||
|
@generate-ai="$emit('generate-ai', 'backgroundImage')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Image -->
|
||||||
|
<div class="asset-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">
|
||||||
|
<q-icon name="image" />
|
||||||
|
<span>Immagine Principale</span>
|
||||||
|
</div>
|
||||||
|
<q-chip size="sm" :color="assets.mainImage ? 'green' : 'grey'" text-color="white">
|
||||||
|
{{ assets.mainImage ? 'Impostato' : 'Opzionale' }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AssetUploader
|
||||||
|
:asset="assets.mainImage"
|
||||||
|
placeholder-icon="add_photo_alternate"
|
||||||
|
placeholder-text="Aggiungi immagine principale"
|
||||||
|
accept="image/*"
|
||||||
|
@upload="file => uploadAsset('mainImage', file)"
|
||||||
|
@remove="removeAsset('mainImage')"
|
||||||
|
@generate-ai="$emit('generate-ai', 'mainImage')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logos -->
|
||||||
|
<div class="asset-section" v-if="hasLogoSlots">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">
|
||||||
|
<q-icon name="branding_watermark" />
|
||||||
|
<span>Loghi (max {{ maxLogos }})</span>
|
||||||
|
</div>
|
||||||
|
<q-chip size="sm" :color="assets.logos.length > 0 ? 'green' : 'grey'" text-color="white">
|
||||||
|
{{ assets.logos.length }} / {{ maxLogos }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="logos-grid">
|
||||||
|
<div
|
||||||
|
v-for="(logo, index) in assets.logos"
|
||||||
|
:key="index"
|
||||||
|
class="logo-item"
|
||||||
|
>
|
||||||
|
<img :src="logo.url" :alt="`Logo ${index + 1}`" />
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
size="sm"
|
||||||
|
icon="close"
|
||||||
|
color="negative"
|
||||||
|
class="remove-btn"
|
||||||
|
@click="removeLogo(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="assets.logos.length < maxLogos"
|
||||||
|
class="logo-add"
|
||||||
|
@click="triggerLogoUpload"
|
||||||
|
>
|
||||||
|
<q-icon name="add" size="32px" color="grey-5" />
|
||||||
|
<span>Aggiungi logo</span>
|
||||||
|
<input
|
||||||
|
ref="logoInputRef"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
hidden
|
||||||
|
@change="handleLogoUpload"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI Generation Tip -->
|
||||||
|
<div class="ai-tip">
|
||||||
|
<q-icon name="auto_awesome" size="24px" color="amber" />
|
||||||
|
<div>
|
||||||
|
<strong>Suggerimento AI</strong>
|
||||||
|
<p>Clicca "Genera con AI" per creare immagini uniche basate sul tema del template.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { Api } from 'src/store/Api';
|
||||||
|
import type { PosterAssets, PosterAsset } from '../../../../types/poster.types';
|
||||||
|
import AssetUploader from './AssetUploader.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
assets: PosterAssets;
|
||||||
|
template: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update', assets: Partial<PosterAssets>): void;
|
||||||
|
(e: 'generate-ai', type: 'backgroundImage' | 'mainImage'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const logoInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const hasLogoSlots = computed(() => props.template?.logoSlots?.enabled);
|
||||||
|
const maxLogos = computed(() => props.template?.logoSlots?.maxCount || 3);
|
||||||
|
|
||||||
|
const uploadAsset = async (type: 'backgroundImage' | 'mainImage', file: File) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('category', type === 'backgroundImage' ? 'background' : 'main');
|
||||||
|
|
||||||
|
const res = await Api.SendReq('/api/assets/upload', 'POST', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.data?.success) {
|
||||||
|
const asset: PosterAsset = {
|
||||||
|
sourceType: 'upload',
|
||||||
|
url: res.data.data.file.url,
|
||||||
|
thumbnailUrl: res.data.data.file.thumbnailUrl,
|
||||||
|
originalName: file.name,
|
||||||
|
mimeType: file.type,
|
||||||
|
size: file.size
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('update', { [type]: asset });
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Immagine caricata!'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.message || 'Errore durante il caricamento'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAsset = (type: 'backgroundImage' | 'mainImage') => {
|
||||||
|
emit('update', { [type]: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerLogoUpload = () => {
|
||||||
|
logoInputRef.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogoUpload = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('category', 'logo');
|
||||||
|
|
||||||
|
const res = await Api.SendReq('/api/assets/upload', 'POST', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.data?.success) {
|
||||||
|
const logo: PosterAsset = {
|
||||||
|
sourceType: 'upload',
|
||||||
|
url: res.data.data.file.url,
|
||||||
|
originalName: file.name
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('update', {
|
||||||
|
logos: [...props.assets.logos, logo]
|
||||||
|
});
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Logo aggiunto!'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: error.message || 'Errore durante il caricamento'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset input
|
||||||
|
input.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLogo = (index: number) => {
|
||||||
|
const newLogos = [...props.assets.logos];
|
||||||
|
newLogos.splice(index, 1);
|
||||||
|
emit('update', { logos: newLogos });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.asset-manager {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(118, 75, 162, 0.05));
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.assets-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logos-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-item {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-add {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-tip {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 193, 7, 0.1), rgba(255, 152, 0, 0.1));
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.panel-header,
|
||||||
|
.section-header {
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-item {
|
||||||
|
background: #333;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-add {
|
||||||
|
border-color: #555;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="asset-uploader"
|
||||||
|
:class="{
|
||||||
|
'has-asset': !!asset,
|
||||||
|
'is-dragging': isDragging,
|
||||||
|
'is-uploading': isUploading
|
||||||
|
}"
|
||||||
|
@dragover.prevent="isDragging = true"
|
||||||
|
@dragleave.prevent="isDragging = false"
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
@click="triggerUpload"
|
||||||
|
>
|
||||||
|
<!-- Has Asset -->
|
||||||
|
<template v-if="asset">
|
||||||
|
<div class="asset-preview">
|
||||||
|
<img :src="asset.thumbnailUrl || asset.url" :alt="asset.originalName" />
|
||||||
|
|
||||||
|
<div class="asset-overlay">
|
||||||
|
<div class="asset-actions">
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
color="white"
|
||||||
|
text-color="primary"
|
||||||
|
icon="refresh"
|
||||||
|
@click.stop="triggerUpload"
|
||||||
|
>
|
||||||
|
<q-tooltip>Sostituisci</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
color="white"
|
||||||
|
text-color="negative"
|
||||||
|
icon="delete"
|
||||||
|
@click.stop="$emit('remove')"
|
||||||
|
>
|
||||||
|
<q-tooltip>Rimuovi</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="asset-badge" v-if="asset.sourceType === 'ai'">
|
||||||
|
<q-icon name="auto_awesome" size="14px" />
|
||||||
|
AI Generated
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="asset-info">
|
||||||
|
<span class="asset-name">{{ asset.originalName || 'Immagine' }}</span>
|
||||||
|
<span class="asset-size" v-if="asset.size">{{ formatSize(asset.size) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- No Asset -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="upload-placeholder">
|
||||||
|
<q-icon :name="placeholderIcon" size="48px" color="grey-5" />
|
||||||
|
<p>{{ placeholderText }}</p>
|
||||||
|
|
||||||
|
<div class="upload-actions">
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="primary"
|
||||||
|
icon="upload"
|
||||||
|
label="Carica"
|
||||||
|
@click.stop="triggerUpload"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
color="amber"
|
||||||
|
text-color="dark"
|
||||||
|
icon="auto_awesome"
|
||||||
|
label="Genera con AI"
|
||||||
|
@click.stop="$emit('generate-ai')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Hidden File Input -->
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
type="file"
|
||||||
|
:accept="accept"
|
||||||
|
hidden
|
||||||
|
@change="handleFileSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Upload Progress -->
|
||||||
|
<div v-if="isUploading" class="upload-progress">
|
||||||
|
<q-spinner-dots size="40px" color="primary" />
|
||||||
|
<span>Caricamento in corso...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import type { PosterAsset } from '../../../../types/poster.types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
asset: PosterAsset | null;
|
||||||
|
placeholderIcon?: string;
|
||||||
|
placeholderText?: string;
|
||||||
|
accept?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'upload', file: File): void;
|
||||||
|
(e: 'remove'): void;
|
||||||
|
(e: 'generate-ai'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const isUploading = ref(false);
|
||||||
|
|
||||||
|
const triggerUpload = () => {
|
||||||
|
fileInputRef.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
emit('upload', file);
|
||||||
|
}
|
||||||
|
input.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: DragEvent) => {
|
||||||
|
isDragging.value = false;
|
||||||
|
const file = event.dataTransfer?.files[0];
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
emit('upload', file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatSize = (bytes: number): string => {
|
||||||
|
if (bytes < 1024) return bytes + ' B';
|
||||||
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||||
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.asset-uploader {
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover:not(.has-asset) {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-dragging {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-asset {
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-placeholder {
|
||||||
|
padding: 2.5rem 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-preview {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
|
||||||
|
.asset-uploader:hover & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
left: 0.75rem;
|
||||||
|
background: rgba(255, 193, 7, 0.9);
|
||||||
|
color: #333;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-info {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: #fafafa;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
|
||||||
|
.asset-name {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-size {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.asset-uploader {
|
||||||
|
border-color: #555;
|
||||||
|
|
||||||
|
&.has-asset {
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-info {
|
||||||
|
background: #333;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
background: rgba(30, 30, 30, 0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-form">
|
||||||
|
<div class="form-header">
|
||||||
|
<q-icon name="edit_note" size="28px" color="primary" />
|
||||||
|
<div>
|
||||||
|
<h3>Contenuti Locandina</h3>
|
||||||
|
<p>Inserisci i testi che appariranno nel poster</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-form class="form-body" @submit.prevent>
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label required">Titolo Evento</label>
|
||||||
|
<q-input
|
||||||
|
:model-value="content.title"
|
||||||
|
@update:model-value="update('title', $event)"
|
||||||
|
filled
|
||||||
|
placeholder="es. SAGRA DEL FUNGO PORCINO"
|
||||||
|
:rules="[val => !!val || 'Il titolo è obbligatorio']"
|
||||||
|
counter
|
||||||
|
maxlength="100"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="title" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subtitle -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">Sottotitolo / Claim</label>
|
||||||
|
<q-input
|
||||||
|
:model-value="content.subtitle"
|
||||||
|
@update:model-value="update('subtitle', $event)"
|
||||||
|
filled
|
||||||
|
placeholder="es. XXV Edizione - Tradizione e Sapori"
|
||||||
|
counter
|
||||||
|
maxlength="150"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="short_text" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date & Time Row -->
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-section flex-2">
|
||||||
|
<label class="section-label required">Data Evento</label>
|
||||||
|
<q-input
|
||||||
|
:model-value="content.eventDate"
|
||||||
|
@update:model-value="update('eventDate', $event)"
|
||||||
|
filled
|
||||||
|
placeholder="es. 15-16-17 Ottobre 2025"
|
||||||
|
:rules="[val => !!val || 'La data è obbligatoria']"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="event" />
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<q-btn flat dense icon="calendar_today" @click="showDatePicker = true">
|
||||||
|
<q-popup-proxy>
|
||||||
|
<q-date
|
||||||
|
:model-value="parsedDate"
|
||||||
|
@update:model-value="setDateFromPicker"
|
||||||
|
mask="DD MMMM YYYY"
|
||||||
|
:locale="dateLocale"
|
||||||
|
/>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section flex-1">
|
||||||
|
<label class="section-label">Ora</label>
|
||||||
|
<q-input
|
||||||
|
:model-value="content.eventTime"
|
||||||
|
@update:model-value="update('eventTime', $event)"
|
||||||
|
filled
|
||||||
|
placeholder="es. 10:00 - 23:00"
|
||||||
|
mask="##:##"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="schedule" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label required">Luogo</label>
|
||||||
|
<q-input
|
||||||
|
:model-value="content.location"
|
||||||
|
@update:model-value="update('location', $event)"
|
||||||
|
filled
|
||||||
|
placeholder="es. Parco delle Querce, Borgo Montano (PG)"
|
||||||
|
:rules="[val => !!val || 'Il luogo è obbligatorio']"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="place" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contacts -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">Contatti</label>
|
||||||
|
<q-input
|
||||||
|
:model-value="content.contacts"
|
||||||
|
@update:model-value="update('contacts', $event)"
|
||||||
|
filled
|
||||||
|
placeholder="es. Tel: 0742 123456 | info@evento.it"
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
|
autogrow
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="contact_phone" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Extra Text -->
|
||||||
|
<div class="form-section">
|
||||||
|
<label class="section-label">Informazioni Aggiuntive</label>
|
||||||
|
<q-input
|
||||||
|
:model-value="extraTextJoined"
|
||||||
|
@update:model-value="updateExtraText($event)"
|
||||||
|
filled
|
||||||
|
placeholder="es. Ingresso Libero • Stand Gastronomici • Musica dal Vivo"
|
||||||
|
hint="Separa con • oppure vai a capo per più righe"
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
|
autogrow
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="notes" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Fields (if template has customText layers) -->
|
||||||
|
<template v-if="customTextLayers.length > 0">
|
||||||
|
<q-separator class="q-my-lg" />
|
||||||
|
<div class="section-title">
|
||||||
|
<q-icon name="extension" />
|
||||||
|
Campi Personalizzati
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="layer in customTextLayers"
|
||||||
|
:key="layer.id"
|
||||||
|
class="form-section"
|
||||||
|
>
|
||||||
|
<label class="section-label">{{ layer.defaultValue || layer.id }}</label>
|
||||||
|
<q-input
|
||||||
|
:model-value="content.customFields[layer.id] || ''"
|
||||||
|
@update:model-value="updateCustomField(layer.id, $event)"
|
||||||
|
filled
|
||||||
|
:placeholder="layer.defaultValue"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-form>
|
||||||
|
|
||||||
|
<!-- Quick Fill Button -->
|
||||||
|
<div class="form-footer">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
icon="auto_fix_high"
|
||||||
|
label="Compila con esempio"
|
||||||
|
@click="fillWithExample"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import type { PosterContent } from '../../../../types/poster.types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
content: PosterContent;
|
||||||
|
template: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update', content: Partial<PosterContent>): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const showDatePicker = ref(false);
|
||||||
|
|
||||||
|
const dateLocale = {
|
||||||
|
days: ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'],
|
||||||
|
daysShort: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'],
|
||||||
|
months: ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'],
|
||||||
|
monthsShort: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic']
|
||||||
|
};
|
||||||
|
|
||||||
|
const customTextLayers = computed(() => {
|
||||||
|
if (!props.template?.layers) return [];
|
||||||
|
return props.template.layers.filter((l: any) => l.type === 'customText');
|
||||||
|
});
|
||||||
|
|
||||||
|
const extraTextJoined = computed(() => {
|
||||||
|
return props.content.extraText?.join(' • ') || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedDate = computed(() => {
|
||||||
|
// Try to parse existing date
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const update = (key: keyof PosterContent, value: any) => {
|
||||||
|
emit('update', { [key]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateExtraText = (value: string) => {
|
||||||
|
const parts = value.split(/[•\n]/).map(s => s.trim()).filter(Boolean);
|
||||||
|
emit('update', { extraText: parts });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCustomField = (fieldId: string, value: string) => {
|
||||||
|
emit('update', {
|
||||||
|
customFields: {
|
||||||
|
...props.content.customFields,
|
||||||
|
[fieldId]: value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDateFromPicker = (date: string) => {
|
||||||
|
update('eventDate', date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fillWithExample = () => {
|
||||||
|
const templateType = props.template?.templateType || '';
|
||||||
|
|
||||||
|
const examples: Record<string, Partial<PosterContent>> = {
|
||||||
|
'outdoor-nature': {
|
||||||
|
title: 'ESCURSIONE NEL BOSCO',
|
||||||
|
subtitle: 'Alla scoperta dei segreti della natura',
|
||||||
|
eventDate: '21 Settembre 2025',
|
||||||
|
eventTime: '09:00',
|
||||||
|
location: 'Riserva Naturale Monte Cucco, Sigillo (PG)',
|
||||||
|
contacts: 'Prenotazioni: 333 1234567 | info@escursioni.it',
|
||||||
|
extraText: ['Pranzo al sacco incluso', 'Difficoltà: media', 'Posti limitati']
|
||||||
|
},
|
||||||
|
'workshop-craft': {
|
||||||
|
title: 'LABORATORIO DI CERAMICA',
|
||||||
|
subtitle: 'Impara l\'arte del tornio',
|
||||||
|
eventDate: '8-9 Novembre 2025',
|
||||||
|
eventTime: '10:00 - 17:00',
|
||||||
|
location: 'Bottega Creativa, Via Roma 45, Deruta (PG)',
|
||||||
|
contacts: 'Info: ceramica@bottega.it | 075 9876543',
|
||||||
|
extraText: ['Materiali inclusi', 'Adatto a principianti', 'Max 8 partecipanti']
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
title: 'FESTA DI PAESE',
|
||||||
|
subtitle: 'Tradizione, musica e buon cibo',
|
||||||
|
eventDate: '15-16-17 Agosto 2025',
|
||||||
|
eventTime: '18:00 - 24:00',
|
||||||
|
location: 'Piazza Centrale, Borgo Antico',
|
||||||
|
contacts: 'Pro Loco: 333 9876543 | festa@proloco.it',
|
||||||
|
extraText: ['Ingresso Libero', 'Stand Gastronomici', 'Musica dal Vivo']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const example = examples[templateType] || examples.default;
|
||||||
|
emit('update', example);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.content-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(118, 75, 162, 0.05));
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.flex-1 { flex: 1; }
|
||||||
|
.flex-2 { flex: 2; }
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
&.required::after {
|
||||||
|
content: ' *';
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.form-header {
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
<template>
|
||||||
|
<div class="export-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<q-icon name="download" size="28px" color="primary" />
|
||||||
|
<div>
|
||||||
|
<h3>Esporta Locandina</h3>
|
||||||
|
<p>Genera e scarica il tuo poster</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
|
<!-- Summary -->
|
||||||
|
<div class="summary-section">
|
||||||
|
<h4>Riepilogo</h4>
|
||||||
|
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-item">
|
||||||
|
<q-icon name="title" color="primary" />
|
||||||
|
<div>
|
||||||
|
<label>Titolo</label>
|
||||||
|
<span>{{ posterData.content.title || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-item">
|
||||||
|
<q-icon name="event" color="primary" />
|
||||||
|
<div>
|
||||||
|
<label>Data</label>
|
||||||
|
<span>{{ posterData.content.eventDate || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-item">
|
||||||
|
<q-icon name="place" color="primary" />
|
||||||
|
<div>
|
||||||
|
<label>Luogo</label>
|
||||||
|
<span>{{ posterData.content.location || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-item">
|
||||||
|
<q-icon name="dashboard" color="primary" />
|
||||||
|
<div>
|
||||||
|
<label>Template</label>
|
||||||
|
<span>{{ template?.name || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-item">
|
||||||
|
<q-icon name="aspect_ratio" color="primary" />
|
||||||
|
<div>
|
||||||
|
<label>Formato</label>
|
||||||
|
<span>{{ template?.format?.width }} × {{ template?.format?.height }} px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-item">
|
||||||
|
<q-icon name="image" color="primary" />
|
||||||
|
<div>
|
||||||
|
<label>Immagini</label>
|
||||||
|
<span>
|
||||||
|
{{ imageCount }} immagine/i
|
||||||
|
<template v-if="aiImageCount > 0">
|
||||||
|
({{ aiImageCount }} AI)
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-separator class="q-my-lg" />
|
||||||
|
|
||||||
|
<!-- Generation Status -->
|
||||||
|
<div v-if="generationSteps.length > 0" class="generation-status">
|
||||||
|
<h4>Stato Generazione</h4>
|
||||||
|
|
||||||
|
<div class="steps-list">
|
||||||
|
<div
|
||||||
|
v-for="step in generationSteps"
|
||||||
|
:key="step.id"
|
||||||
|
class="step-item"
|
||||||
|
:class="`status-${step.status}`"
|
||||||
|
>
|
||||||
|
<div class="step-icon">
|
||||||
|
<q-spinner-dots v-if="step.status === 'processing'" size="20px" color="primary" />
|
||||||
|
<q-icon v-else-if="step.status === 'completed'" name="check_circle" color="positive" />
|
||||||
|
<q-icon v-else-if="step.status === 'error'" name="error" color="negative" />
|
||||||
|
<q-icon v-else name="radio_button_unchecked" color="grey" />
|
||||||
|
</div>
|
||||||
|
<div class="step-info">
|
||||||
|
<span class="step-label">{{ step.label }}</span>
|
||||||
|
<span v-if="step.message" class="step-message">{{ step.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result -->
|
||||||
|
<div v-if="result" class="result-section">
|
||||||
|
<div class="result-success">
|
||||||
|
<q-icon name="celebration" size="48px" color="amber" />
|
||||||
|
<h3>Locandina Pronta! 🎉</h3>
|
||||||
|
<p>La tua locandina è stata generata con successo</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="download-options">
|
||||||
|
<q-btn
|
||||||
|
color="positive"
|
||||||
|
icon="download"
|
||||||
|
label="Scarica PNG"
|
||||||
|
size="lg"
|
||||||
|
@click="$emit('download', 'png')"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
outline
|
||||||
|
color="primary"
|
||||||
|
icon="download"
|
||||||
|
label="Scarica JPG"
|
||||||
|
size="lg"
|
||||||
|
@click="$emit('download', 'jpg')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="extra-actions">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="amber"
|
||||||
|
icon="favorite"
|
||||||
|
label="Salva nei preferiti"
|
||||||
|
@click="$emit('save')"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
icon="share"
|
||||||
|
label="Condividi"
|
||||||
|
@click="shareResult"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generate Button (if not yet generated) -->
|
||||||
|
<div v-else class="generate-section">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="auto_awesome"
|
||||||
|
label="Genera Locandina"
|
||||||
|
size="xl"
|
||||||
|
:loading="isGenerating"
|
||||||
|
class="generate-btn"
|
||||||
|
@click="$emit('generate')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p class="generate-hint">
|
||||||
|
<q-icon name="info" size="16px" />
|
||||||
|
La generazione richiede circa 10-30 secondi
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Poster -->
|
||||||
|
<div v-if="result" class="new-poster-section">
|
||||||
|
<q-separator class="q-my-lg" />
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
icon="add"
|
||||||
|
label="Crea nuovo poster"
|
||||||
|
@click="$emit('reset')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import type { PosterData, GenerationStep } from '../../../../types/poster.types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
posterData: PosterData;
|
||||||
|
template: any;
|
||||||
|
isGenerating: boolean;
|
||||||
|
generationSteps: GenerationStep[];
|
||||||
|
result: { imageUrl: string; posterId: string } | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'generate'): void;
|
||||||
|
(e: 'download', format: 'png' | 'jpg'): void;
|
||||||
|
(e: 'save'): void;
|
||||||
|
(e: 'reset'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
const imageCount = computed(() => {
|
||||||
|
let count = 0;
|
||||||
|
if (props.posterData.assets.backgroundImage) count++;
|
||||||
|
if (props.posterData.assets.mainImage) count++;
|
||||||
|
count += props.posterData.assets.logos.length;
|
||||||
|
return count;
|
||||||
|
});
|
||||||
|
|
||||||
|
const aiImageCount = computed(() => {
|
||||||
|
let count = 0;
|
||||||
|
if (props.posterData.assets.backgroundImage?.sourceType === 'ai') count++;
|
||||||
|
if (props.posterData.assets.mainImage?.sourceType === 'ai') count++;
|
||||||
|
return count;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shareResult = async () => {
|
||||||
|
if (!props.result?.imageUrl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (navigator.share) {
|
||||||
|
await navigator.share({
|
||||||
|
title: props.posterData.name,
|
||||||
|
text: `Guarda la mia locandina: ${props.posterData.content.title}`,
|
||||||
|
url: window.location.href
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback: copy link
|
||||||
|
await navigator.clipboard.writeText(window.location.href);
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Link copiato negli appunti!'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Share error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.export-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(118, 75, 162, 0.05));
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
.summary-section {
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
.q-icon {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generation Status
|
||||||
|
.generation-status {
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&.status-completed {
|
||||||
|
background: rgba(76, 175, 80, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-processing {
|
||||||
|
background: rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-error {
|
||||||
|
background: rgba(244, 67, 54, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-message {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result
|
||||||
|
.result-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-success {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #888;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.extra-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate
|
||||||
|
.generate-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
padding: 1rem 3rem !important;
|
||||||
|
font-size: 1.1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-poster-section {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.panel-header {
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item,
|
||||||
|
.step-item {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
<template>
|
||||||
|
<div class="live-preview" :class="{ 'size-large': size === 'large' }">
|
||||||
|
<div class="preview-header">
|
||||||
|
<span class="preview-title">Anteprima Live</span>
|
||||||
|
<div class="preview-actions">
|
||||||
|
<q-btn flat dense round icon="zoom_in" @click="zoomIn" />
|
||||||
|
<q-btn flat dense round icon="zoom_out" @click="zoomOut" />
|
||||||
|
<q-btn flat dense round icon="fit_screen" @click="zoomFit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-container" ref="containerRef">
|
||||||
|
<div
|
||||||
|
class="preview-canvas"
|
||||||
|
:style="canvasStyle"
|
||||||
|
>
|
||||||
|
<!-- Final Image (if available) -->
|
||||||
|
<img
|
||||||
|
v-if="finalImageUrl"
|
||||||
|
:src="finalImageUrl"
|
||||||
|
alt="Final poster"
|
||||||
|
class="final-image"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Live Rendered Preview -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- Background -->
|
||||||
|
<div class="layer layer-background" :style="backgroundStyle" />
|
||||||
|
|
||||||
|
<!-- Overlay -->
|
||||||
|
<div class="layer layer-overlay" :style="overlayStyle" />
|
||||||
|
|
||||||
|
<!-- Main Image -->
|
||||||
|
<div
|
||||||
|
v-if="hasMainImage"
|
||||||
|
class="layer layer-main-image"
|
||||||
|
:style="getLayerPosition('mainImage')"
|
||||||
|
>
|
||||||
|
<img :src="assets.mainImage?.url" alt="" :style="mainImageStyle" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text Layers -->
|
||||||
|
<div
|
||||||
|
v-for="layer in textLayers"
|
||||||
|
:key="layer.id"
|
||||||
|
class="layer layer-text"
|
||||||
|
:style="getLayerPosition(layer.id)"
|
||||||
|
>
|
||||||
|
<div :style="getTextStyle(layer)">
|
||||||
|
{{ getLayerText(layer) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logos -->
|
||||||
|
<div
|
||||||
|
v-for="(logo, idx) in visibleLogos"
|
||||||
|
:key="`logo-${idx}`"
|
||||||
|
class="layer layer-logo"
|
||||||
|
:style="getLogoPosition(idx)"
|
||||||
|
>
|
||||||
|
<img :src="logo.url" alt="" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-footer">
|
||||||
|
<span class="dimensions">
|
||||||
|
{{ template?.format?.width || 1080 }} × {{ template?.format?.height || 1920 }} px
|
||||||
|
</span>
|
||||||
|
<span class="zoom-level">{{ Math.round(zoom * 100) }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
|
import type { PosterContent, PosterAssets } from '../../../../types/poster.types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
template: any;
|
||||||
|
content: PosterContent;
|
||||||
|
assets: PosterAssets;
|
||||||
|
layerOverrides?: Record<string, any>;
|
||||||
|
finalImageUrl?: string;
|
||||||
|
size?: 'normal' | 'large';
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
|
const zoom = ref(0.3);
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const canvasStyle = computed(() => {
|
||||||
|
const width = props.template?.format?.width || 1080;
|
||||||
|
const height = props.template?.format?.height || 1920;
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: `${width * zoom.value}px`,
|
||||||
|
height: `${height * zoom.value}px`,
|
||||||
|
background: props.template?.backgroundColor || '#1a1a2e'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasMainImage = computed(() => !!props.assets.mainImage?.url);
|
||||||
|
|
||||||
|
const backgroundStyle = computed(() => {
|
||||||
|
if (props.assets.backgroundImage?.url) {
|
||||||
|
return {
|
||||||
|
backgroundImage: `url(${props.assets.backgroundImage.url})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const overlayStyle = computed(() => {
|
||||||
|
const bgLayer = props.template?.layers?.find((l: any) => l.type === 'backgroundImage');
|
||||||
|
const overlay = bgLayer?.style?.overlay;
|
||||||
|
|
||||||
|
if (!overlay?.enabled) return { display: 'none' };
|
||||||
|
|
||||||
|
if (overlay.type === 'gradient' && overlay.stops) {
|
||||||
|
const stops = overlay.stops
|
||||||
|
.map((s: any) => `${s.color} ${s.position * 100}%`)
|
||||||
|
.join(', ');
|
||||||
|
return {
|
||||||
|
background: `linear-gradient(180deg, ${stops})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
background: overlay.color || 'rgba(0,0,0,0.5)'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainImageStyle = computed(() => {
|
||||||
|
const layer = props.template?.layers?.find((l: any) => l.type === 'mainImage');
|
||||||
|
const style = layer?.style || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: style.objectFit || 'cover',
|
||||||
|
borderRadius: `${style.borderRadius || 0}px`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const textLayers = computed(() => {
|
||||||
|
if (!props.template?.layers) return [];
|
||||||
|
return props.template.layers.filter((l: any) =>
|
||||||
|
['title', 'subtitle', 'eventDate', 'eventTime', 'location', 'contacts', 'extraText'].includes(l.type)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleLogos = computed(() => {
|
||||||
|
if (!props.template?.logoSlots?.enabled) return [];
|
||||||
|
return props.assets.logos.slice(0, props.template.logoSlots.maxCount || 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const getLayerPosition = (layerIdOrType: string) => {
|
||||||
|
const layer = props.template?.layers?.find((l: any) =>
|
||||||
|
l.id === layerIdOrType || l.type === layerIdOrType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!layer) return {};
|
||||||
|
|
||||||
|
const pos = layer.position;
|
||||||
|
const anchor = layer.anchor || 'center';
|
||||||
|
|
||||||
|
let transform = '';
|
||||||
|
switch (anchor) {
|
||||||
|
case 'center':
|
||||||
|
transform = 'translate(-50%, -50%)';
|
||||||
|
break;
|
||||||
|
case 'top-center':
|
||||||
|
transform = 'translateX(-50%)';
|
||||||
|
break;
|
||||||
|
case 'bottom-center':
|
||||||
|
transform = 'translate(-50%, -100%)';
|
||||||
|
break;
|
||||||
|
// Add more as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: `${pos.x * 100}%`,
|
||||||
|
top: `${pos.y * 100}%`,
|
||||||
|
width: `${pos.w * 100}%`,
|
||||||
|
height: `${pos.h * 100}%`,
|
||||||
|
transform,
|
||||||
|
zIndex: layer.zIndex || 1
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLogoPosition = (index: number) => {
|
||||||
|
const slots = props.template?.logoSlots?.slots || [];
|
||||||
|
const slot = slots[index];
|
||||||
|
|
||||||
|
if (!slot) return { display: 'none' };
|
||||||
|
|
||||||
|
const pos = slot.position;
|
||||||
|
return {
|
||||||
|
left: `${pos.x * 100}%`,
|
||||||
|
top: `${pos.y * 100}%`,
|
||||||
|
width: `${pos.w * 100}%`,
|
||||||
|
height: `${pos.h * 100}%`,
|
||||||
|
transform: 'translate(-50%, -50%)'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTextStyle = (layer: any) => {
|
||||||
|
const style = layer.style || {};
|
||||||
|
const palette = props.template?.palette || {};
|
||||||
|
|
||||||
|
const css: Record<string, any> = {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: style.textAlign === 'left' ? 'flex-start' :
|
||||||
|
style.textAlign === 'right' ? 'flex-end' : 'center',
|
||||||
|
fontFamily: `"${style.fontFamily || 'Open Sans'}", sans-serif`,
|
||||||
|
fontWeight: style.fontWeight || 400,
|
||||||
|
fontSize: `${(style.fontSize || 32) * zoom.value}px`,
|
||||||
|
color: style.color || palette.text || '#ffffff',
|
||||||
|
textAlign: style.textAlign || 'center',
|
||||||
|
textTransform: style.textTransform || 'none',
|
||||||
|
letterSpacing: `${(style.letterSpacing || 0) * zoom.value}px`,
|
||||||
|
lineHeight: style.lineHeight || 1.2,
|
||||||
|
padding: `0 ${8 * zoom.value}px`
|
||||||
|
};
|
||||||
|
|
||||||
|
if (style.shadow?.enabled) {
|
||||||
|
const s = style.shadow;
|
||||||
|
css.textShadow = `${s.offsetX || 0}px ${s.offsetY || 0}px ${s.blur || 0}px ${s.color || 'rgba(0,0,0,0.5)'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return css;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLayerText = (layer: any) => {
|
||||||
|
switch (layer.type) {
|
||||||
|
case 'title':
|
||||||
|
return props.content.title || 'Titolo Evento';
|
||||||
|
case 'subtitle':
|
||||||
|
return props.content.subtitle || '';
|
||||||
|
case 'eventDate':
|
||||||
|
const time = props.content.eventTime ? ` • ${props.content.eventTime}` : '';
|
||||||
|
return (props.content.eventDate || 'Data Evento') + time;
|
||||||
|
case 'eventTime':
|
||||||
|
return props.content.eventTime || '';
|
||||||
|
case 'location':
|
||||||
|
return props.content.location || 'Luogo Evento';
|
||||||
|
case 'contacts':
|
||||||
|
return props.content.contacts || '';
|
||||||
|
case 'extraText':
|
||||||
|
return props.content.extraText?.join(' • ') || '';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoomIn = () => {
|
||||||
|
zoom.value = Math.min(zoom.value + 0.1, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoomOut = () => {
|
||||||
|
zoom.value = Math.max(zoom.value - 0.1, 0.1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoomFit = () => {
|
||||||
|
if (!containerRef.value) return;
|
||||||
|
|
||||||
|
const container = containerRef.value;
|
||||||
|
const width = props.template?.format?.width || 1080;
|
||||||
|
const height = props.template?.format?.height || 1920;
|
||||||
|
|
||||||
|
const scaleX = (container.clientWidth - 40) / width;
|
||||||
|
const scaleY = (container.clientHeight - 40) / height;
|
||||||
|
|
||||||
|
zoom.value = Math.min(scaleX, scaleY, 0.5);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-fit on mount
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(zoomFit, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.template, () => {
|
||||||
|
setTimeout(zoomFit, 100);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.live-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 500px;
|
||||||
|
|
||||||
|
&.size-large {
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: #222;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
.q-btn {
|
||||||
|
color: #aaa;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow: auto;
|
||||||
|
background:
|
||||||
|
linear-gradient(45deg, #1a1a1a 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, #1a1a1a 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, #1a1a1a 75%);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||||
|
background-color: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-canvas {
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
&.layer-background,
|
||||||
|
&.layer-overlay {
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.layer-main-image img,
|
||||||
|
&.layer-logo img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.layer-text {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #222;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
<template>
|
||||||
|
<div class="template-selector-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Scegli un Template</h2>
|
||||||
|
<p>Seleziona il modello base per la tua locandina</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="filters-bar">
|
||||||
|
<q-select
|
||||||
|
v-model="selectedType"
|
||||||
|
:options="typeOptions"
|
||||||
|
label="Categoria"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
clearable
|
||||||
|
style="min-width: 180px"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="searchQuery"
|
||||||
|
label="Cerca template..."
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="flex-1"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div v-if="isLoading" class="loading-state">
|
||||||
|
<q-spinner-dots size="60px" color="primary" />
|
||||||
|
<p>Caricamento template...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Templates Grid -->
|
||||||
|
<div v-else class="templates-grid">
|
||||||
|
<div
|
||||||
|
v-for="template in filteredTemplates"
|
||||||
|
:key="template._id"
|
||||||
|
class="template-card"
|
||||||
|
:class="{ 'is-selected': selectedTemplate?._id === template._id }"
|
||||||
|
@click="$emit('select', template)"
|
||||||
|
>
|
||||||
|
<div class="template-preview" :style="getPreviewStyle(template)">
|
||||||
|
<div class="template-overlay">
|
||||||
|
<q-icon
|
||||||
|
v-if="selectedTemplate?._id === template._id"
|
||||||
|
name="check_circle"
|
||||||
|
size="56px"
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
round
|
||||||
|
color="white"
|
||||||
|
text-color="primary"
|
||||||
|
icon="visibility"
|
||||||
|
@click.stop="previewTemplate(template)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Anteprima</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="template-info">
|
||||||
|
<h3>{{ template.name }}</h3>
|
||||||
|
<p class="template-type">{{ formatType(template.templateType) }}</p>
|
||||||
|
<div class="template-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<q-icon name="layers" size="14px" />
|
||||||
|
{{ template.layers?.length || 0 }} layer
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<q-icon name="aspect_ratio" size="14px" />
|
||||||
|
{{ template.format?.preset || 'Custom' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="template-tags" v-if="template.metadata?.tags?.length">
|
||||||
|
<q-chip
|
||||||
|
v-for="tag in template.metadata.tags.slice(0, 3)"
|
||||||
|
:key="tag"
|
||||||
|
size="sm"
|
||||||
|
dense
|
||||||
|
color="grey-3"
|
||||||
|
text-color="grey-8"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="!isLoading && filteredTemplates.length === 0" class="empty-state">
|
||||||
|
<q-icon name="search_off" size="80px" color="grey-4" />
|
||||||
|
<h3>Nessun template trovato</h3>
|
||||||
|
<p>Prova a modificare i filtri di ricerca</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Template Preview Dialog -->
|
||||||
|
<q-dialog v-model="showPreviewDialog" maximized>
|
||||||
|
<q-card class="preview-dialog">
|
||||||
|
<q-card-section class="row items-center q-pb-none">
|
||||||
|
<div class="text-h6">{{ previewingTemplate?.name }}</div>
|
||||||
|
<q-space />
|
||||||
|
<q-btn icon="close" flat round dense v-close-popup />
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section class="preview-content">
|
||||||
|
<div class="preview-canvas" :style="getPreviewStyle(previewingTemplate, true)" />
|
||||||
|
|
||||||
|
<div class="preview-details">
|
||||||
|
<h3>{{ previewingTemplate?.name }}</h3>
|
||||||
|
<p>{{ previewingTemplate?.description || 'Nessuna descrizione' }}</p>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<strong>Formato:</strong>
|
||||||
|
{{ previewingTemplate?.format?.width }} × {{ previewingTemplate?.format?.height }} px
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-row">
|
||||||
|
<strong>Layer:</strong>
|
||||||
|
{{ previewingTemplate?.layers?.length || 0 }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-row" v-if="previewingTemplate?.metadata?.tags?.length">
|
||||||
|
<strong>Tag:</strong>
|
||||||
|
<div class="tags-list">
|
||||||
|
<q-chip
|
||||||
|
v-for="tag in previewingTemplate.metadata.tags"
|
||||||
|
:key="tag"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
icon="check"
|
||||||
|
label="Usa questo template"
|
||||||
|
class="q-mt-lg full-width"
|
||||||
|
size="lg"
|
||||||
|
@click="selectAndClose(previewingTemplate)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { Api } from 'src/store/Api';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
selectedTemplate: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'select', template: any): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const templates = ref<any[]>([]);
|
||||||
|
const isLoading = ref(true);
|
||||||
|
const searchQuery = ref('');
|
||||||
|
const selectedType = ref<string | null>(null);
|
||||||
|
const showPreviewDialog = ref(false);
|
||||||
|
const previewingTemplate = ref<any>(null);
|
||||||
|
|
||||||
|
const typeOptions = computed(() => {
|
||||||
|
const types = [...new Set(templates.value.map(t => t.templateType))];
|
||||||
|
return [
|
||||||
|
{ label: 'Tutte le categorie', value: null },
|
||||||
|
...types.map(t => ({
|
||||||
|
label: formatType(t),
|
||||||
|
value: t
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredTemplates = computed(() => {
|
||||||
|
let result = templates.value;
|
||||||
|
|
||||||
|
if (selectedType.value) {
|
||||||
|
result = result.filter(t => t.templateType === selectedType.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.value) {
|
||||||
|
const query = searchQuery.value.toLowerCase();
|
||||||
|
result = result.filter(t =>
|
||||||
|
t.name.toLowerCase().includes(query) ||
|
||||||
|
t.templateType.toLowerCase().includes(query) ||
|
||||||
|
t.metadata?.tags?.some((tag: string) => tag.toLowerCase().includes(query))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatType = (type: string) => {
|
||||||
|
return type
|
||||||
|
.replace(/-/g, ' ')
|
||||||
|
.replace(/\b\w/g, c => c.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviewStyle = (template: any, large = false) => {
|
||||||
|
if (!template) return {};
|
||||||
|
|
||||||
|
if (template.thumbnailUrl || template.previewUrl) {
|
||||||
|
return {
|
||||||
|
backgroundImage: `url(${template.previewUrl || template.thumbnailUrl})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: gradient from palette
|
||||||
|
const palette = template.palette || {};
|
||||||
|
return {
|
||||||
|
background: `linear-gradient(135deg,
|
||||||
|
${palette.primary || '#667eea'} 0%,
|
||||||
|
${palette.secondary || '#764ba2'} 50%,
|
||||||
|
${palette.background || '#1a1a2e'} 100%)`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewTemplate = (template: any) => {
|
||||||
|
previewingTemplate.value = template;
|
||||||
|
showPreviewDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAndClose = (template: any) => {
|
||||||
|
emit('select', template);
|
||||||
|
showPreviewDialog.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTemplates = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await Api.SendReq('/api/templates', 'GET');
|
||||||
|
if (res?.data?.success) {
|
||||||
|
templates.value = res.data.data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading templates:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadTemplates();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.template-selector-panel {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.templates-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
.template-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-selected {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.2);
|
||||||
|
|
||||||
|
.template-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(102, 126, 234, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-preview {
|
||||||
|
height: 200px;
|
||||||
|
position: relative;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-info {
|
||||||
|
padding: 1rem 1.25rem 1.25rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-type {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
color: #888;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preview Dialog
|
||||||
|
.preview-dialog {
|
||||||
|
.preview-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 400px;
|
||||||
|
gap: 2rem;
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-canvas {
|
||||||
|
border-radius: 12px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-details {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode
|
||||||
|
.body--dark {
|
||||||
|
.template-card {
|
||||||
|
background: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header p,
|
||||||
|
.template-type,
|
||||||
|
.meta-item {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||