bravio init project

This commit is contained in:
María
2025-10-15 09:41:47 +02:00
commit d6859c386f
74 changed files with 15820 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
22

0
README.md Normal file
View File

5
app.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

208
assets/css/main.css Normal file
View File

@@ -0,0 +1,208 @@
@import "tailwindcss";
@theme {
--font-barlow: 'Barlow', sans-serif;
--font-barlow-semi-condensed: 'BarlowSemiCondensed', sans-serif;
/* Color base */
--color-accent: #3F38C8;
--color-black: #1E1E1E;
--color-white: #FFFFFF;
--color-gray-light: #B9B9B9;
--color-muted: #6D6D6D;
--color-error: #B91C1C;
--color-success: #039B65;
/* Font sizes */
--text-5.5: 1.375rem; /* 22px */
--text-8: 2rem; /* 32px */
--text-32: 2rem; /* 32px */
--text-40: 2.5rem; /* 40px */
/* Shadow */
--shadow-button: 0 2px 4px 0 rgba(0, 0, 0, 0.25);
--shadow-tool-card: 0 2px 5px 0 rgba(0, 0, 0, 0.25);
}
/* Breakpoints */
/* Desktop < 1920px; */
/* Tablet < 1024px; */
/* Mobile < 640px; */
/* Super desktop */
/* @media screen and (min-width: 1023px) {
:root {
--default-font-size: var(--font-size-base, 16px);
--layout-padding-x: 2rem;
--font-size-Hero: 48px;
--font-size-H1: 48px;
--font-size-H2: 36px;
--font-size-H3: 32px;
--font-size-H4: 22px;
--font-size-H5: 16px;
--font-size-2xl-custom: 24px;
--font-size-lg-custom: 18px;
--font-size-base-custom: 16px;
--font-size-sm-custom: 14px;
--font-size-xs-custom: 12px;
--font-size-button-base: 16px;
}
} */
/* Tablet */
/* @media screen and (max-width: 1023px) {
:root {
--default-font-size: var(--font-size-base);
--layout-padding-x: 1rem;
--font-size-Hero: 40px;
--font-size-H1: 40px;
--font-size-H2: 32px;
--font-size-H3: 24px;
--font-size-H4: 22px;
--font-size-H5: 16px;
--font-size-2xl-custom: 20px;
--font-size-lg-custom: 16px;
--font-size-base-custom: 16px;
--font-size-sm-custom: 14px;
--font-size-xs-custom: 12px;
--font-size-button-base: 14px;
}
} */
/* Mobile */
/* @media screen and (max-width: 640px) {
:root {
--default-font-size: var(--font-size-base);
--layout-padding-x: 0.5rem;
--font-size-Hero: 32px;
--font-size-H1: 32px;
--font-size-H2: 30px;
--font-size-H3: 24px;
--font-size-H4: 22px;
--font-size-H5: 16px;
--font-size-2xl-custom: 18px;
--font-size-lg-custom: 16px;
--font-size-base-custom: 16px;
--font-size-sm-custom: 14px;
--font-size-xs-custom: 12px;
--font-size-button-base: 14px;
}
} */
@font-face {
font-family: "BarlowSemiCondensed";
font-style: normal;
font-weight: 700;
src: local("Barlow SemiCondensed Bold"), local("BarlowBarlowSemiCondensed-Bold"),
url(../fonts/BarlowSemiCondensed/BarlowSemiCondensed-Bold.ttf) format("opentype");
}
@font-face {
font-family: "Barlow";
font-style: normal;
font-weight: 700;
src: local("Barlow Bold"), local("Barlow-Bold"),
url(../fonts/Barlow/Barlow-Bold.ttf) format("opentype");
}
@font-face {
font-family: "Barlow";
font-style: normal;
font-weight: 600;
src: local("Barlow Semibold"), local("Barlow-Semibold"),
url(../fonts/Barlow/Barlow-Semibold.ttf) format("opentype");
}
@font-face {
font-family: "Barlow";
font-style: normal;
font-weight: 500;
src: local("Barlow Medium"), local("Barlow-Medium"),
url(../fonts/Barlow/Barlow-Medium.ttf) format("opentype");
}
@font-face {
font-family: "Barlow";
font-style: normal;
font-weight: 400;
src: local("Barlow Regular"), local("Barlow-Regular"),
url(../fonts/Barlow/Barlow-Regular.ttf) format("opentype");
}
html,
body {
font-family: var(--font-barlow);
font-size: var(--default-font-size, 16px);
height: 100dvh;
color: var(--color-black);
background-color: var(--color-background-extra-light);
}
header {
position: sticky;
top: 0;
z-index: 20;
}
body footer {
position: sticky;
top: 100dvh;
}
html {
scroll-behavior: smooth;
}
/* h1 {
font-size: var(--font-size-H1);
}
h2 {
font-size: var(--font-size-H2);
}
h3 {
font-size: var(--font-size-H3);
}
h4 {
font-size: var(--font-size-H4);
}
h5 {
font-size: var(--font-size-H5);
} */
/* .text-2xl-custom {
font-size: var(--font-size-2xl-custom);
}
.text-lg-custom {
font-size: var(--font-size-lg-custom);
}
.text-base-custom {
font-size: var(--font-size-base-custom);
}
.text-sm-custom {
font-size: var(--font-size-sm-custom);
}
.text-xs-custom {
font-size: var(--font-size-xs-custom);
}
.text-button-base {
font-size: var(--font-size-button-base);
} */
.body-overflow-hidden {
overflow: hidden;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.76202 9.5L12.762 15.5L18.762 9.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 213 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.746 18.5L9.746 12.5L15.746 6.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.25403 18.5L15.254 12.5L9.25403 6.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.238 15.5L12.238 9.5L6.23798 15.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 214 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.254 8.5V12.5M12.254 16.5H12.264M22.254 12.5C22.254 18.0228 17.7769 22.5 12.254 22.5C6.73118 22.5 2.25403 18.0228 2.25403 12.5C2.25403 6.97715 6.73118 2.5 12.254 2.5C17.7769 2.5 22.254 6.97715 22.254 12.5Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.547 10.5C23.0037 12.7413 22.6782 15.0714 21.6249 17.1018C20.5715 19.1322 18.8539 20.7401 16.7585 21.6573C14.6631 22.5746 12.3166 22.7458 10.1103 22.1424C7.90398 21.539 5.9712 20.1974 4.63429 18.3414C3.29737 16.4854 2.63713 14.2272 2.76367 11.9434C2.89022 9.65954 3.79589 7.4881 5.32966 5.79118C6.86343 4.09426 8.93259 2.97444 11.1921 2.61846C13.4516 2.26248 15.7648 2.69186 17.746 3.835M9.74601 11.5L12.746 14.5L22.746 4.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 604 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.746 12.5L11.746 14.5L15.746 10.5M22.746 12.5C22.746 18.0228 18.2689 22.5 12.746 22.5C7.22315 22.5 2.746 18.0228 2.746 12.5C2.746 6.97715 7.22315 2.5 12.746 2.5C18.2689 2.5 22.746 6.97715 22.746 12.5Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.32798 9.5C9.56308 8.83167 10.0271 8.26811 10.6379 7.90913C11.2487 7.55016 11.9669 7.41894 12.6652 7.53871C13.3634 7.65849 13.9968 8.02152 14.4531 8.56353C14.9093 9.10553 15.159 9.79152 15.158 10.5C15.158 12.5 12.158 13.5 12.158 13.5M12.238 17.5H12.248M22.238 12.5C22.238 18.0228 17.7608 22.5 12.238 22.5C6.71513 22.5 2.23798 18.0228 2.23798 12.5C2.23798 6.97715 6.71513 2.5 12.238 2.5C17.7608 2.5 22.238 6.97715 22.238 12.5Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 605 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.254 15.5V3.5M12.254 15.5L7.25403 10.5M12.254 15.5L17.254 10.5M21.254 15.5V19.5C21.254 20.0304 21.0433 20.5391 20.6682 20.9142C20.2932 21.2893 19.7845 21.5 19.254 21.5H5.25403C4.7236 21.5 4.21489 21.2893 3.83981 20.9142C3.46474 20.5391 3.25403 20.0304 3.25403 19.5V15.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

3
assets/svg/download.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 10.5V2.5M8 10.5L4.66667 7.16667M8 10.5L11.3333 7.16667M14 10.5V13.1667C14 13.5203 13.8595 13.8594 13.6095 14.1095C13.3594 14.3595 13.0203 14.5 12.6667 14.5H3.33333C2.97971 14.5 2.64057 14.3595 2.39052 14.1095C2.14048 13.8594 2 13.5203 2 13.1667V10.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 430 B

View File

@@ -0,0 +1,5 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.238 13.5C12.7903 13.5 13.238 13.0523 13.238 12.5C13.238 11.9477 12.7903 11.5 12.238 11.5C11.6857 11.5 11.238 11.9477 11.238 12.5C11.238 13.0523 11.6857 13.5 12.238 13.5Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.238 6.5C12.7903 6.5 13.238 6.05228 13.238 5.5C13.238 4.94772 12.7903 4.5 12.238 4.5C11.6857 4.5 11.238 4.94772 11.238 5.5C11.238 6.05228 11.6857 6.5 12.238 6.5Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.238 20.5C12.7903 20.5 13.238 20.0523 13.238 19.5C13.238 18.9477 12.7903 18.5 12.238 18.5C11.6857 18.5 11.238 18.9477 11.238 19.5C11.238 20.0523 11.6857 20.5 12.238 20.5Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 838 B

3
assets/svg/expand.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.254 15.5L21.254 21.5M21.254 21.5V16.5M21.254 21.5H16.254M15.254 9.5L21.254 3.5M21.254 3.5V8.5M21.254 3.5H16.254M3.25403 16.5V21.5M3.25403 21.5H8.25403M3.25403 21.5L9.25403 15.5M3.25403 8.5V3.5M3.25403 3.5H8.25403M3.25403 3.5L9.25403 9.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.238 3.5H21.238M21.238 3.5V9.5M21.238 3.5L10.238 14.5M18.238 13.5V19.5C18.238 20.0304 18.0273 20.5391 17.6522 20.9142C17.2771 21.2893 16.7684 21.5 16.238 21.5H5.23798C4.70754 21.5 4.19884 21.2893 3.82376 20.9142C3.44869 20.5391 3.23798 20.0304 3.23798 19.5V8.5C3.23798 7.96957 3.44869 7.46086 3.82376 7.08579C4.19884 6.71071 4.70754 6.5 5.23798 6.5H11.238" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.238 12.5V11.5M10.238 18.5V16.5M10.238 18.5C9.13341 18.5 8.23798 19.3954 8.23798 20.5C8.23798 21.6046 9.13341 22.5 10.238 22.5C11.3425 22.5 12.238 21.6046 12.238 20.5C12.238 19.3954 11.3425 18.5 10.238 18.5ZM10.238 7.5V6.5M14.238 2.5V6.5C14.238 7.03043 14.4487 7.53914 14.8238 7.91421C15.1988 8.28929 15.7075 8.5 16.238 8.5H20.238M15.738 22.5H18.238C18.7684 22.5 19.2771 22.2893 19.6522 21.9142C20.0273 21.5391 20.238 21.0304 20.238 20.5V7.5L15.238 2.5H6.23798C5.70754 2.5 5.19884 2.71071 4.82376 3.08579C4.44869 3.46086 4.23798 3.96957 4.23798 4.5V20.5C4.23805 20.855 4.33262 21.2036 4.51198 21.51" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 779 B

3
assets/svg/file-down.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.762 2.5V6.5C14.762 7.03043 14.9727 7.53914 15.3478 7.91421C15.7229 8.28929 16.2316 8.5 16.762 8.5H20.762M12.762 18.5V12.5M12.762 18.5L9.76202 15.5M12.762 18.5L15.762 15.5M15.762 2.5H6.76202C6.23159 2.5 5.72288 2.71071 5.34781 3.08579C4.97274 3.46086 4.76202 3.96957 4.76202 4.5V20.5C4.76202 21.0304 4.97274 21.5391 5.34781 21.9142C5.72288 22.2893 6.23159 22.5 6.76202 22.5H18.762C19.2925 22.5 19.8012 22.2893 20.1762 21.9142C20.5513 21.5391 20.762 21.0304 20.762 20.5V7.5L15.762 2.5Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 665 B

3
assets/svg/file-up.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.746 2.5V6.5C14.746 7.03043 14.9567 7.53914 15.3318 7.91421C15.7069 8.28929 16.2156 8.5 16.746 8.5H20.746M12.746 12.5V18.5M12.746 12.5L15.746 15.5M12.746 12.5L9.746 15.5M15.746 2.5H6.746C6.21557 2.5 5.70686 2.71071 5.33179 3.08579C4.95672 3.46086 4.746 3.96957 4.746 4.5V20.5C4.746 21.0304 4.95672 21.5391 5.33179 21.9142C5.70686 22.2893 6.21557 22.5 6.746 22.5H18.746C19.2764 22.5 19.7851 22.2893 20.1602 21.9142C20.5353 21.5391 20.746 21.0304 20.746 20.5V7.5L15.746 2.5Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 653 B

3
assets/svg/globe.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.254 12.5C22.254 18.0228 17.7769 22.5 12.254 22.5M22.254 12.5C22.254 6.97715 17.7769 2.5 12.254 2.5M22.254 12.5H2.25403M12.254 22.5C6.73118 22.5 2.25403 18.0228 2.25403 12.5M12.254 22.5C9.68626 19.8038 8.25403 16.2233 8.25403 12.5C8.25403 8.77674 9.68626 5.19615 12.254 2.5M12.254 22.5C14.8218 19.8038 16.254 16.2233 16.254 12.5C16.254 8.77674 14.8218 5.19615 12.254 2.5M2.25403 12.5C2.25403 6.97715 6.73118 2.5 12.254 2.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 603 B

3
assets/svg/info.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.762 16.5V12.5M12.762 8.5H12.772M22.762 12.5C22.762 18.0228 18.2849 22.5 12.762 22.5C7.23918 22.5 2.76202 18.0228 2.76202 12.5C2.76202 6.97715 7.23918 2.5 12.762 2.5C18.2849 2.5 22.762 6.97715 22.762 12.5Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 386 B

3
assets/svg/loader.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.254 2.5V6.5M16.454 8.3L19.354 5.4M18.254 12.5H22.254M16.454 16.7L19.354 19.6M12.254 18.5V22.5M5.15403 19.6L8.05403 16.7M2.25403 12.5H6.25403M5.15403 5.4L8.05403 8.3" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 346 B

3
assets/svg/mail.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.7484 7.5L13.7574 13.227C13.4522 13.4042 13.1057 13.4976 12.7529 13.4976C12.4 13.4976 12.0535 13.4042 11.7484 13.227L2.74835 7.5M4.74835 4.5H20.7484C21.8529 4.5 22.7484 5.39543 22.7484 6.5V18.5C22.7484 19.6046 21.8529 20.5 20.7484 20.5H4.74835C3.64378 20.5 2.74835 19.6046 2.74835 18.5V6.5C2.74835 5.39543 3.64378 4.5 4.74835 4.5Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 511 B

3
assets/svg/maximize.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.76202 3.5H5.76202C5.23159 3.5 4.72288 3.71071 4.34781 4.08579C3.97274 4.46086 3.76202 4.96957 3.76202 5.5V8.5M21.762 8.5V5.5C21.762 4.96957 21.5513 4.46086 21.1762 4.08579C20.8012 3.71071 20.2925 3.5 19.762 3.5H16.762M3.76202 16.5V19.5C3.76202 20.0304 3.97274 20.5391 4.34781 20.9142C4.72288 21.2893 5.23159 21.5 5.76202 21.5H8.76202M16.762 21.5H19.762C20.2925 21.5 20.8012 21.2893 21.1762 20.9142C21.5513 20.5391 21.762 20.0304 21.762 19.5V16.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 626 B

3
assets/svg/menu.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.76202 12.5H20.762M4.76202 18.5H20.762M4.76202 6.5H20.762" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 237 B

3
assets/svg/minimize.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.746 3.5V6.5C8.746 7.03043 8.53529 7.53914 8.16022 7.91421C7.78514 8.28929 7.27644 8.5 6.746 8.5H3.746M21.746 8.5H18.746C18.2156 8.5 17.7069 8.28929 17.3318 7.91421C16.9567 7.53914 16.746 7.03043 16.746 6.5V3.5M3.746 16.5H6.746C7.27644 16.5 7.78514 16.7107 8.16022 17.0858C8.53529 17.4609 8.746 17.9696 8.746 18.5V21.5M16.746 21.5V18.5C16.746 17.9696 16.9567 17.4609 17.3318 17.0858C17.7069 16.7107 18.2156 16.5 18.746 16.5H21.746" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 610 B

3
assets/svg/paperclip.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.238 6.5L7.82397 15.086C7.44882 15.4611 7.23807 15.97 7.23807 16.5005C7.23807 17.031 7.44882 17.5399 7.82397 17.915C8.19912 18.2901 8.70793 18.5009 9.23847 18.5009C9.76901 18.5009 10.2778 18.2901 10.653 17.915L19.067 9.329C19.8171 8.57884 20.2386 7.56139 20.2386 6.5005C20.2386 5.43961 19.8171 4.42216 19.067 3.672C18.3168 2.92183 17.2994 2.5004 16.2385 2.5004C15.1776 2.5004 14.1601 2.92183 13.41 3.672L5.03097 12.223C4.46637 12.7785 4.01734 13.4402 3.70977 14.1701C3.40221 14.9 3.2422 15.6836 3.23897 16.4756C3.23575 17.2677 3.38937 18.0525 3.69098 18.7849C3.99259 19.5172 4.43622 20.1826 4.99628 20.7427C5.55634 21.3027 6.22174 21.7464 6.95411 22.048C7.68648 22.3496 8.47132 22.5032 9.26335 22.5C10.0554 22.4968 10.8389 22.3368 11.5688 22.0292C12.2987 21.7216 12.9605 21.2726 13.516 20.708L21.895 12.157" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 987 B

3
assets/svg/shrink.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.254 15.5L21.254 21.5M15.254 15.5V20.3M15.254 15.5H20.054M9.25403 20.3V15.5M9.25403 15.5H4.45403M9.25403 15.5L3.25403 21.5M15.254 4.7V9.5M15.254 9.5H20.054M15.254 9.5L21.254 3.5M9.25403 4.7V9.5M9.25403 9.5H4.45403M9.25403 9.5L3.25403 3.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7484 18.5116H12.7584M7.74835 2.51157H17.7484C18.8529 2.51157 19.7484 3.407 19.7484 4.51157V20.5116C19.7484 21.6161 18.8529 22.5116 17.7484 22.5116H7.74835C6.64378 22.5116 5.74835 21.6161 5.74835 20.5116V4.51157C5.74835 3.407 6.64378 2.51157 7.74835 2.51157Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 439 B

3
assets/svg/x.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.746 6.5L6.746 18.5M6.746 6.5L18.746 18.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 222 B

59
components/ButtonCTA.vue Normal file
View File

@@ -0,0 +1,59 @@
<template>
<button
type="button"
class="flex items-center justify-center cursor-pointer "
:disabled="disabled" :class="buttonClasses" @click="handleClick">
<slot></slot>
<img v-if="hasIcon" src="@/assets/svg/download.svg" alt="" class="h-5 w-5" />
</button>
</template>
<script>
export default {
props: {
color: {
type: String,
default: "accent",
},
disabled: {
type: Boolean,
default: false,
},
size: {
type: String,
default: "md",
},
hasIcon: {
type: Boolean,
default: false,
},
},
computed: {
buttonClasses() {
return {
"px-3.5 py-2 gap-x-2 text-sm": this.size === "sm",
"px-5 py-2.5 gap-x-2 text-sm": this.size === "md",
"px-6 py-4 gap-x-1 text-base": this.size === "lg",
"bg-accent hover:bg-accent/90 focus:bg-accent border border-accent focus:border-accent/50 text-white rounded-lg":
this.color === "accent" && !this.disabled,
"bg-accent/55 border border-accent/10 cursor-not-allowed text-white rounded-lg":
this.color === "accent" && this.disabled,
"bg-white border border-accent text-accent hover:bg-accent focus:bg-white focus:text-accent focus:border focus:border-accent/50 hover:text-white rounded-lg":
this.color === "transparent" && !this.disabled,
"bg-white cursor-not-allowed text-accent/50 border border-accent/50 rounded-lg":
this.color === "transparent" && this.disabled,
};
}
},
methods: {
handleClick($ev) {
if (this.disabled) {
$ev.stopPropagation();
}
},
},
};
</script>

31
components/FormSlice.vue Normal file
View File

@@ -0,0 +1,31 @@
<template>
<section class="bg-white flex items-center justify-center p-4 min-h-[100dvh]">
<div
class="rounded-2xl p-6 md:p-10 border border-gray-200 min-w-80dvh max-w-2xl w-full"
:class="{'bg-[#FBF4FF]': type === 'feedback'}">
<!-- <FullscreenButton v-if="isFullscreenEnabled" class="ml-auto" /> -->
<slot />
</div>
</section>
</template>
<script>
import FullscreenButton from './FullscreenButton.vue'
export default {
components: { FullscreenButton },
props: {
type: {
type: String,
default: "default"
}
},
computed: {
isFullscreenEnabled() {
if (document.fullscreenElement)
return true;
return false;
}
}
};
</script>

View File

@@ -0,0 +1,31 @@
<template>
<ButtonCTA
color="transparent"
@click="toggleFullscreen"
>
</ButtonCTA>
</template>
<script>
import ButtonCTA from './ButtonCTA.vue';
export default {
name: "FullscreenButton",
components: { ButtonCTA },
methods: {
toggleFullscreen() {
const element = document.documentElement; // toda la página
// Si quieres solo un div del formulario, cambia esto por document.getElementById("formulario")
if (!document.fullscreenElement) {
element.requestFullscreen();
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
}
};
</script>

View File

@@ -0,0 +1,36 @@
<template>
<div>
<label for="file-upload" class="block text-sm text-gray-900">
<p class="mb-2">{{ label }}</p>
<div class="flex justify-between items-center p-2 cursor-pointer rounded-md bg-white outline-1 -outline-offset-1 outline-gray-300 focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-indigo-600 hover:text-indigo-500">
<span v-if="file && file.name" class="text-sm text-gray-500">{{ file.name }}</span>
<img src="@/assets/svg/file-up.svg" alt="" class="h-5 w-5 ml-auto" />
</div>
<input id="file-upload" type="file" name="file-upload" accept="image/png, image/jpeg" class="hidden" @change="onFileChange"/>
</label>
</div>
</template>
<script>
export default {
props: {
label: { type: String, default: 'Sube tu archivo' },
required: { type: Boolean, default: false },
},
emits: ['input-value'],
data() {
return {
file: null
}
},
methods: {
onFileChange(event) {
const selectedFile = event.target.files[0]
if (selectedFile) {
this.file = selectedFile
this.$emit('input-value', this.file);
}
},
},
}
</script>

View File

@@ -0,0 +1,42 @@
<template>
<div class="flex items-center mb-4 bg-white outline-1 -outline-offset-1 outline-gray-300 p-2 rounded-md">
<input
id="default-checkbox"
v-model="checkbox"
type="checkbox"
class="w-4 h-4 text-indigo-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label
for="default-checkbox"
class="ms-2 text-sm text-gray-900 dark:text-gray-300"
>{{ label }}</label>
</div>
</template>
<script>
export default {
props: {
label: { type: String, required: true },
},
emits: ['input-value'],
data() {
return {
checkbox: ''
}
},
watch: {
checkbox(newVal) {
this.updateCheckbox(newVal);
}
},
methods: {
updateCheckbox(value) {
if (value === true) {
this.$emit('input-value', this.label);
}
if (value === false) {
this.$emit('input-value', null);
}
}
}
}
</script>

50
components/InputEmail.vue Normal file
View File

@@ -0,0 +1,50 @@
<template>
<div class="space-y-2">
<label for="mail" class="block text-sm text-gray-900 dark:text-white">{{ label }} <span>{{ required ? '*' : '' }}</span></label>
<div class="mt-2 h-10 flex items-center rounded-md bg-white pl-3 outline-1 -outline-offset-1 outline-gray-300 has-[input:focus-within]:outline-2 has-[input:focus-within]:-outline-offset-2 has-[input:focus-within]:outline-indigo-600 dark:bg-white/5 dark:outline-gray-600 dark:has-[input:focus-within]:outline-indigo-500">
<img src="@/assets/svg/mail.svg" alt="" class="h-5 w-5" />
<input
id="mail"
v-model="email"
type="text"
name="mail"
class="block min-w-0 grow py-1.5 pr-3 pl-1 text-base text-gray-900 placeholder:text-gray-400 focus:outline-none sm:text-sm/6 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500"
:placeholder="placeholder"
/>
</div>
<p v-if="error" class="mt-2 text-sm text-error dark:text-error">{{ error }}</p>
</div>
</template>
<script>
export default {
props: {
label: { type: String, required: true },
required: { type: Boolean, default: true },
placeholder: { type: String, default: '' },
},
emits: ['input-value'],
data() {
return {
email: '',
error: ''
}
},
watch: {
email(newVal) {
this.error = '';
this.updateEmail(newVal);
}
},
methods: {
updateEmail(email) {
const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (pattern.test(String(email).toLowerCase())) {
this.$emit('input-value', email);
} else {
this.error = "Por favor, introduce un correo electrónico válido.";
}
}
}
}
</script>

52
components/InputRadio.vue Normal file
View File

@@ -0,0 +1,52 @@
<template>
<div
v-if="label"
class="mb-4 font-medium"
>
<p class="mb-2 block">{{ label }} <span v-if="required" class="text-red-500">*</span></p>
</div>
<div
v-for="(opt, index) in options" :key="`${opt.value}-radio-${index}`"
class="flex items-center gap-2 mb-4 bg-white hover:bg-accent/20 pl-2 outline-1 -outline-offset-1 outline-gray-300 rounded-md cursor-pointer">
<input
:id="`radio-` + opt.label"
v-model="picked"
type="radio"
:value="opt"
/>
<label :for="`radio-` + opt.label" class="cursor-pointer w-full py-2 pr-2">{{ opt.label }}</label>
</div>
</template>
<script>
export default {
props: {
type: { type: String, default: 'radio' },
label: { type: String, default: '' },
options: { type: Array, default: () => [] },
required: { type: Boolean, default: false }
},
emits: ['input-value'],
data() {
return {
picked: ''
}
},
watch: {
picked(newVal) {
//////console.log('radio', newVal);
this.updateRadio(newVal);
}
},
methods: {
updateRadio(value) {
if (value) {
this.$emit('input-value', value);
} else {
this.$emit('input-value', null);
}
}
}
}
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div class="space-y-2">
<label for="telephone" class="block text-sm text-gray-900 dark:text-white">{{ label }} <span>{{ required ? '*' : '' }}</span></label>
<div class="mt-2 h-10 flex items-center rounded-md bg-white pl-3 outline-1 -outline-offset-1 outline-gray-300 has-[input:focus-within]:outline-2 has-[input:focus-within]:-outline-offset-2 has-[input:focus-within]:outline-indigo-600 dark:bg-white/5 dark:outline-gray-600 dark:has-[input:focus-within]:outline-indigo-500">
<img src="@/assets/svg/smartphone.svg" alt="" class="h-5 w-5" />
<input
id="telephone"
v-model="telephone"
type="tel"
name="telephone"
:placeholder="placeholder"
:required="required"
class="block min-w-0 grow py-1.5 pr-3 pl-1 text-base text-gray-900 placeholder:text-gray-400 focus:outline-none sm:text-sm/6 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500"
/>
</div>
<p v-if="error" class="mt-2 text-sm text-error dark:text-error">{{ error }}</p>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
default: 'Teléfono'
},
placeholder: {
type: String,
default: 'Introduce tu teléfono'
},
required: {
type: Boolean,
default: false
}
},
emits: ['input-value'],
data() {
return {
telephone: '',
error: ''
}
},
watch: {
telephone(newVal) {
this.error = '';
this.updateTelephone(newVal);
}
},
methods: {
updateTelephone(value) {
this.error = '';
//const phonePattern = /^[\s-]*(?:\d[\s-]*){1,14}\d[\s-]*$/;
const phonePattern = /^\+?[0-9\s\-()]{7,15}$/;
//////console.log('Valor del teléfono:', phonePattern.test(value));
if (phonePattern.test(value)) {
this.$emit('input-value', value);
} else {
this.error = "Por favor, introduce un número de teléfono válido.";
}
}
}
}
</script>

46
components/InputText.vue Normal file
View File

@@ -0,0 +1,46 @@
<template>
<div class="space-y-2">
<label for="text" class="block text-sm text-gray-900">{{label}} <span>{{ required ? '*' : '' }}</span></label>
<div class="h-10 flex items-center rounded-md bg-white pl-3 outline-1 -outline-offset-1 outline-gray-300 has-[input:focus-within]:outline-2 has-[input:focus-within]:-outline-offset-2 has-[input:focus-within]:outline-indigo-600">
<input
id="text"
type="text"
name="text"
:placeholder="placeholder"
class="block min-w-0 grow py-1.5 pr-3 pl-1 text-base text-gray-900 placeholder:text-gray-400 focus:outline-none sm:text-sm/6 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500"
:required="required"
:value="modelValue"
@input="updateValue"
/>
</div>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
required: true
},
required: {
type: Boolean,
default: false
},
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: 'Input'
}
},
emits: ['input-value'],
methods: {
updateValue(event) {
this.$emit('input-value', event.target.value)
}
},
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="space-y-2">
<label for="textarea" class="block text-sm text-gray-900 dark:text-white">{{ label }}</label>
<div class="h-full flex items-center rounded-md bg-white pl-2 outline-1 -outline-offset-1 outline-gray-300 has-[input:focus-within]:outline-2 has-[input:focus-within]:-outline-offset-2 has-[input:focus-within]:outline-indigo-600 dark:bg-white/5 dark:outline-gray-600 dark:has-[input:focus-within]:outline-indigo-500">
<textarea
id="textarea"
name="textarea"
rows="3"
class="block grow p-2 text-base text-gray-900 placeholder:text-gray-400 focus:outline-none sm:text-sm/6 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500"
placeholder="Input"
@input="onInput" />
</div>
</div>
</template>
<script>
export default {
name: 'InputTextarea',
props: {
label: { type: String, default: 'Textarea' },
placeholder: { type: String, default: 'Input' },
required: { type: Boolean, default: false },
},
emits: ['input-value'],
data() {
return {
value: ''
}
},
methods: {
onInput(event) {
this.value = event.target.value;
this.$emit('input-value', this.value);
}
}
}
</script>

51
components/InputUrl.vue Normal file
View File

@@ -0,0 +1,51 @@
<template>
<div>
<label for="web" class="block text-sm text-gray-900 dark:text-white">{{ label }} <span>{{ required ? '*' : '' }}</span></label>
<div class="mt-2 h-10 flex items-center rounded-md bg-white pl-3 outline-1 -outline-offset-1 outline-gray-300 has-[input:focus-within]:outline-2 has-[input:focus-within]:-outline-offset-2 has-[input:focus-within]:outline-indigo-600 dark:bg-white/5 dark:outline-gray-600 dark:has-[input:focus-within]:outline-indigo-500">
<img src="@/assets/svg/globe.svg" alt="" class="h-5 w-5" />
<input
id="web"
v-model="web"
type="url"
name="web"
:placeholder="placeholder"
class="block min-w-0 grow py-1.5 pr-3 pl-1 text-base text-gray-900 placeholder:text-gray-400 focus:outline-none sm:text-sm/6 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500"
/>
</div>
<p v-if="error" class="mt-2 text-sm text-error dark:text-error">{{ error }}</p>
</div>
</template>
<script>
export default {
props: {
label: { type: String, required: true },
required: { type: Boolean, default: true },
placeholder: { type: String, default: '' },
},
emits: ['input-value'],
data() {
return {
web: '',
error: ''
}
},
watch: {
web(newVal) {
this.error = '';
this.updateUrl(newVal);
}
},
methods: {
updateUrl(value){
const pattern = /^(https?:\/\/)?([\w-]+(\.[\w-]+)+)(\/[\w-./?%&=]*)?$/i;
if(pattern.test(value)){
this.error = '';
this.$emit('input-value', value);
} else {
this.error = "Por favor, introduce una URL válida.";
}
}
}
}
</script>

View File

@@ -0,0 +1,56 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="label" for="option-range" class="text-sm mb-2">
{{ label }}
</label>
<ul class="flex justify-start items-center gap-4 relative">
<li
v-for="(option, index) in options"
:key="`option-range-${index}`"
class="w-10 h-10"
>
<input
:id="`option-range-${index}`"
v-model="selectedOption"
type="radio"
name="option-range"
:value="option"
class="hidden peer"
/>
<label
:for="`option-range-${index}`"
class="border border-gray-light rounded-sm shadow-sm px-4 py-2.5 text-sm cursor-pointer bg-white text-gray-900
peer-checked:bg-accent peer-checked:text-white peer-checked:border-accent"
>
{{ option.label }}
</label>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "RangeInputs",
props: {
type: { type: String, default: "radio" },
label: { type: String, default: "" },
options: { type: Array, default: () => [] }, // [{ label: '1', value: 1 }, ...]
required: { type: Boolean, default: false },
},
emits: ["input-value"],
data() {
return {
selectedOption: null,
};
},
watch: {
selectedOption(newValue) {
//console.log("range", newValue);
this.$emit("input-value", newValue);
},
},
};
</script>

42
components/SelectMenu.vue Normal file
View File

@@ -0,0 +1,42 @@
<template>
<div class="space-y-2">
<label for="select" class="block text-sm text-gray-900">{{ label }} <span>{{ required ? '*' : '' }}</span></label>
<select
id="select"
v-model="selectedOption"
name="select"
class="block outline-1 -outline-offset-1 outline-gray-300 w-full rounded-md border-0 bg-white p-2 text-base text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-600"
@change="updateSelection">
<option disabled value="">Seleccione una opción</option>
<option v-for="option in options" :key="option.value" :value="option.value">{{ option.label }}</option>
</select>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
default: 'Select Menu'
},
required: {
type: Boolean,
default: false
},
options: { type: Array, default: () => [] }
},
emits: ['input-value'],
data() {
return {
selectedOption: null
}
},
methods: {
updateSelection(event) {
this.selectedOption = event.target.value
this.$emit('input-value', this.selectedOption)
}
}
}
</script>

View File

@@ -0,0 +1,89 @@
<template>
<div id="pdf-content" class="space-y-4">
<h2 class="text-xl font-semibold text-center">{{ data.title }}</h2>
<p class="text-center text-muted">{{ data.subtitle }}</p>
<div class="text-center">
<h3 class="text-lg font-semibold my-4">Tu puntuación total es {{ totalScore?.score }}/{{ totalScore?.totalPossiblePoints }}</h3>
<p v-if="totalScore?.recommendations" class="text-lg font-semibold my-4">Recomendarión: {{ totalScore?.recommendations }}</p>
</div>
<section v-for="(res, index) in results" :key="index" class="border border-gray-200 rounded-3xl">
<div v-if="res.result !== 'SIN REGLA'" class="flex flex-col gap-2 justify-start items-center p-6 mb-4">
<div class="w-full flex flex-col md:flex-row justify-between items-center gap-2 border-b border-gray-200 pb-4">
<h3 class="font-semibold text-xl md:text-2xl">{{ res.sectionTitle }}</h3>
<div class="flex gap-4 justify-end items-center w-full">
<p class="text-muted">Puntuación</p>
<div class="p-1 rounded-xl bg-[#DAB2F4] flex justify-center min-w-20"><p>{{ res.score }}/{{ res.sectionPossibleTotalPoints }}</p></div>
</div>
</div>
<p class="">{{ res.result }}</p>
</div>
</section>
</div>
<client-only>
<ButtonCTA color="accent" has-icon @click="downloadPDF">
Descargar Informe (PDF)
</ButtonCTA>
</client-only>
</template>
<script>
export default {
name: "SliceEnd",
props: {
data: {
type: Object,
default: () => ({})
},
currentIndex: { type: Number, default: 0 },
total: { type: Number, default: 1 }
},
emits: ['previous'],
data() {
return {
results: null,
totalScore: null,
recommendations: [
"Implementar una estrategia de comunicación interna.",
"Evaluar la satisfacción del equipo trimestralmente.",
"Adoptar un sistema de gestión de proyectos colaborativo."
]
}
},
mounted() {
this.getResults();
},
methods: {
getResults() {
this.results = JSON.parse(localStorage.getItem('results'));
this.totalScore = JSON.parse(localStorage.getItem('final-score'));
},
async downloadPDF() {
if (import.meta.client) {
const { jsPDF } = await import("jspdf")
const html2canvas = (await import("html2canvas")).default
const content = document.getElementById("pdf-content")
// Parchear colores 'oklch' -> hex (html2canvas workaround)
content.querySelectorAll("*").forEach(el => {
const style = getComputedStyle(el)
if (style.color.includes("oklch")) el.style.color = "#4f46e5"
if (style.backgroundColor.includes("oklch")) el.style.backgroundColor = "#eef2ff"
if (style.borderColor.includes("oklch")) el.style.borderColor = "#4f46e5"
})
const canvas = await html2canvas(content, { scale: 2, backgroundColor: "#ffffff" })
const imgData = canvas.toDataURL("image/png")
const pdf = new jsPDF("p", "mm", "a4")
const pdfWidth = pdf.internal.pageSize.getWidth()
const pdfHeight = (canvas.height * pdfWidth) / canvas.width
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight)
pdf.save("recomendaciones.pdf")
}
}
}
}
</script>

100
components/SliceEnd.vue Normal file
View File

@@ -0,0 +1,100 @@
<template>
<div id="pdf-content" class="space-y-4">
<h2 class="text-xl font-semibold text-center">{{ data.title }}</h2>
<p class="text-center text-muted">{{ data.subtitle }}</p>
<div class="text-center">
<h3 class="text-lg font-semibold my-4">Tu puntuación total es {{ totalScore?.score }}/{{ totalScore?.totalPossiblePoints }}</h3>
<p v-if="totalScore?.recommendations" class="text-lg font-semibold my-4">Recomendarión: {{ totalScore?.recommendations }}</p>
</div>
<section v-for="(res, index) in results" :key="index" class="border border-gray-200 rounded-3xl">
<div v-if="res.result !== 'SIN REGLA'" class="flex flex-col gap-2 justify-start items-center p-6 mb-4">
<div class="w-full flex flex-col md:flex-row justify-between items-center gap-2 border-b border-gray-200 pb-4">
<h3 class="font-semibold text-xl md:text-2xl">{{ res.sectionTitle }}</h3>
<div class="flex gap-4 justify-end items-center w-full">
<p class="text-muted">Puntuación</p>
<div class="p-1 rounded-xl bg-[#DAB2F4] flex justify-center min-w-20"><p>{{ res.score }}/{{ res.sectionPossibleTotalPoints }}</p></div>
</div>
</div>
<p class="">{{ res.result }}</p>
</div>
</section>
</div>
<client-only>
<ButtonCTA color="accent" has-icon @click="downloadPDF">
Descargar Informe (PDF)
</ButtonCTA>
</client-only>
</template>
<script>
export default {
name: "SliceEnd",
props: {
data: {
type: Object,
default: () => ({})
},
currentIndex: { type: Number, default: 0 },
total: { type: Number, default: 1 }
},
emits: ['previous'],
data() {
return {
results: null,
totalScore: null,
recommendations: [
"Implementar una estrategia de comunicación interna.",
"Evaluar la satisfacción del equipo trimestralmente.",
"Adoptar un sistema de gestión de proyectos colaborativo."
]
}
},
mounted() {
this.getResults();
},
methods: {
getResults() {
this.results = JSON.parse(localStorage.getItem('results'));
this.totalScore = JSON.parse(localStorage.getItem('final-score'));
},
async downloadPDF() {
if (import.meta.client) {
const { jsPDF } = await import("jspdf")
const html2canvas = (await import("html2canvas")).default
// const content = document.getElementById("pdf-content")
// // Parchear colores 'oklch' -> hex (html2canvas workaround)
// content.querySelectorAll("*").forEach(el => {
// const style = getComputedStyle(el)
// if (style.color.includes("oklch")) el.style.color = "#DAB2F4"
// if (style.backgroundColor.includes("oklch")) el.style.backgroundColor = "#eef2ff"
// if (style.borderColor.includes("oklch")) el.style.borderColor = "#DAB2F4"
// })
const content = this.recommendations
const canvas = await html2canvas(content, { scale: 2, backgroundColor: "#ffffff" })
const imgData = canvas.toDataURL("image/png")
const pdf = new jsPDF("p", "mm", "a4")
const pageWidth = pdf.internal.pageSize.getWidth()
const pageHeight = pdf.internal.pageSize.getHeight()
// Definir márgenes (ejemplo: 20mm)
const margin = 20
const pdfWidth = pageWidth - 2 * margin
const pdfHeight = (canvas.height * pdfWidth) / canvas.width
pdf.setFontSize(12)
// Si el contenido es más alto que la página, ajustamos para que se pueda dividir en varias páginas
pdf.addImage(imgData, "PNG", 20, 20, pdfWidth, pdfHeight)
pdf.save("recomendaciones.pdf")
}
}
}
}
</script>

View File

@@ -0,0 +1,51 @@
<template>
<div class="flex flex-col justify-center items-start space-y-4">
<h1 class="text-3xl font-bold">
{{ data.title }}
</h1>
<p class="text-sm" v-html="feedbackText"></p>
<button-CTA
color="accent"
class="ml-auto"
@click="$emit('next')"
>
{{ data.button }}
</button-CTA>
</div>
</template>
<script>
export default {
props: {
data: {
type: Object,
default: () => ({})
},
currentIndex: { type: Number, default: 0 },
total: { type: Number, default: 1 }
},
emits: ["next", "previous"],
data() {
return {
feedbackText: ""
}
},
mounted() {
this.generateFeedbackText();
},
methods: {
generateFeedbackText() {
const results = JSON.parse(localStorage.getItem('results'));
if (!results) {
this.feedbackText = "No hay resultados disponibles.";
return;
}
// Buscar el resultado correspondiente al bloque actual buscando en el array results donde algun objeto tenga el mismo sectionId que this.data.sectionId
const blockId = this.data.section.id;
const blockResult = results.find(r => r.sectionId === blockId);
this.feedbackText = blockResult ? blockResult.result : "No hay feedback disponible para este bloque.";
}
}
}
</script>

View File

@@ -0,0 +1,116 @@
<template>
<div class="space-y-6">
<form @submit.prevent="handleSubmit" class="space-y-4">
<p class="text-sm text-muted">
Pregunta {{ currentIndex + 1 }} de {{ total }}
</p>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h2 class="text-xl font-semibold text-gray-900">{{ data.section.title }}</h2>
<p v-if="data.section.description" class="text-muted text-xs">{{ data.section.description }}</p>
</div>
<div class="flex flex-col gap-1">
<h3 class="text-base font-semibold text-gray-900">{{ data.question.title }}</h3>
<p v-if="data.question.description" class="text-muted text-xs">{{ data.question.description }}</p>
</div>
<p v-if="data.question.recommendation" class="w-fit text-xs bg-[#AFE3D2] px-3 py-1 rounded-3xl"><strong>Recomendado:</strong> {{ data.question.recommendation }}</p>
</div>
<template v-if="inputComponent">
<component
:is="inputComponent"
:key="data.question"
v-bind="inputProps"
:required="data.input.required || false"
@input-value="localValue = $event"
/>
</template>
<div class="flex justify-start gap-4 mt-6">
<button-CTA
color="transparent"
@click="$emit('previous')"
>
Atrás
</button-CTA>
<button-CTA
color="accent"
:disabled="localValue ? false : true"
@click="$emit('next', localValue)"
>
Siguiente
</button-CTA>
</div>
</form>
</div>
</template>
<script>
import InputText from "@/components/InputText.vue"
import InputEmail from "@/components/InputEmail.vue"
import InputTextarea from "@/components/InputTextarea.vue"
import SelectMenu from "@/components/SelectMenu.vue"
import InputCheckbox from "@/components/InputCheckbox.vue"
import InputRadio from "@/components/InputRadio.vue"
import InputAttach from "@/components/InputAttach.vue"
import InputTelephone from "@/components/InputTelephone.vue"
import InputUrl from "@/components/InputUrl.vue"
import RangeInputs from "@/components/RangeInputs.vue"
export default {
components: {
InputText,
InputEmail,
InputTextarea,
SelectMenu,
InputCheckbox,
InputRadio,
InputAttach,
InputTelephone,
InputUrl,
RangeInputs
},
props: {
data: { type: Object, required: true },
currentIndex: { type: Number, default: 0 },
total: { type: Number, default: 1 }
},
emits: ["next", "previous", "answer"],
data() {
return {
localValue: null
}
},
computed: {
inputComponent() {
switch (this.data.input.type) {
case "text": return "InputText"
case "email": return "InputEmail"
case "textarea": return "InputTextarea"
case "select": return "SelectMenu"
case "checkbox": return "InputCheckbox"
case "radio": return "InputRadio"
case "file": return "InputAttach"
case "telephone": return "InputTelephone"
case "url": return "InputUrl"
case "range": return "RangeInputs"
default: return "InputText"
}
},
inputProps() {
return this.data.input
}
},
methods: {
handleSubmit() {
this.$emit("answer", { id: this.data.question, value: this.localValue })
this.$emit("next", this.localValue)
}
}
}
</script>

View File

@@ -0,0 +1,29 @@
<template>
<div class="flex flex-col justify-center items-center text-center space-y-4">
<h1 class="text-3xl font-bold">
{{ data.title }}
</h1>
<p class="text-xl font-semibold">
{{ data.subtitle }}
</p>
<button-CTA
color="accent"
@click="$emit('next')"
>
{{ data.button }}
</button-CTA>
</div>
</template>
<script>
import ButtonCTA from './ButtonCTA.vue'
export default {
components: { ButtonCTA },
props: {
data: {
type: Object,
default: () => ({ title: "Welcome", subtitle: "", button: "Start" })
}
}
}
</script>

505
data/certifica-t.js Normal file
View File

@@ -0,0 +1,505 @@
export const surveyData = {
title: "Formulario Demo",
slices: [
{
id: "welcome",
type: "welcome",
section: {
title: "Introducción",
description: ""
},
title: "Bienvenida a Certifica-T",
subtitle: "Cuestionario de evaluación",
button: "Comenzar",
next: "B0-01"
},
{
id: "B0-01",
type: "question",
section: {
id: "B0",
title: "1. Estrategia y gobernanza",
description: "",
},
question: {
title: "a) Cuenta con una memoria o plan de sostenibilidad aprobado",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "B0-02"
},
{
id: "B0-02",
type: "question",
section: {
id: "B0",
title: "1. Estrategia y gobernanza",
description: "",
},
question: {
title: "b) Integra criterios sociales y ambientales en decisiones estratégicas.",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "B0-03"
},
{
id: "B0-03",
type: "question",
section: {
id: "B0",
title: "1. Estrategia y gobernanza",
description: "",
},
question: {
title: "c) Existe un equipo o responsable de sostenibilidad",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "F0"
},
{
id: "F0",
type: "feedback",
section: {
id: "B0",
title: "1. Estrategia y gobernanza",
description: ""
},
title: "Resultados del bloque",
subtitle: "",
button: "Entendido · Continuar",
next: "B1-01"
},
{
id: "B1-01",
type: "question",
section: {
id: "B1",
title: "2. Impacto social",
description: "",
},
question: {
title: "a) Mide indicadores sociales (empleo local, inclusión, igualdad)",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "B1-02"
},
{
id: "B1-02",
type: "question",
section: {
id: "B1",
title: "2. Impacto social",
description: "",
},
question: {
title: "b) Realiza acciones con la comunidad o grupos vulnerables",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "B1-03"
},
{
id: "B1-03",
type: "question",
section: {
id: "B1",
title: "2. Impacto social",
description: "",
},
question: {
title: "c) Publica resultados sociales de forma transparente",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "F1"
},
{
id: "F1",
type: "feedback",
section: {
id: "B1",
title: "2. Impacto social",
description: ""
},
title: "Resultados del bloque",
subtitle: "",
button: "Entendido · Continuar",
next: "B2-01"
},
{
id: "B2-01",
type: "question",
section: {
id: "B2",
title: "3. Impacto ambiental",
description: "",
},
question: {
title: "a) Mide y reduce consumo de energía, agua y recursos.",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "B2-02"
},
{
id: "B2-02",
type: "question",
section: {
id: "B2",
title: "3. Impacto ambiental",
description: "",
},
question: {
title: "b) Gestiona residuos y emisiones con objetivos claros",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "B2-03"
},
{
id: "B2-03",
type: "question",
section: {
id: "B2",
title: "3. Impacto ambiental",
description: "",
},
question: {
title: "c) Utiliza energías renovables o soluciones circulares",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "F2"
},
{
id: "F2",
type: "feedback",
section: {
id: "B2",
title: "3. Impacto ambiental",
description: ""
},
title: "Resultados del bloque",
subtitle: "",
button: "Entendido · Continuar",
next: "B3-01"
},
{
id: "B3-01",
type: "question",
section: {
id: "B3",
title: "4. Transparencia y comunicación",
description: "",
},
question: {
title: "a) Elabora algún informe no financiero o memoria social",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "B3-02"
},
{
id: "B3-02",
type: "question",
section: {
id: "B3",
title: "4. Transparencia y comunicación",
description: "",
},
question: {
title: "b) Informa de los resultados conforme a estándares reconocidos (GRI, RSE, etc.)",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "B3-03"
},
{
id: "B3-03",
type: "question",
section: {
id: "B3",
title: "4. Transparencia y comunicación",
description: "",
},
question: {
title: "c) Comunicación externa incluye logros y objetivos reales",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "F3"
},
{
id: "F3",
type: "feedback",
section: {
id: "B3",
title: "4. Transparencia y comunicación",
description: ""
},
title: "Resultados del bloque",
subtitle: "",
button: "Entendido · Continuar",
next: "B4-01"
},
{
id: "B4-01",
type: "question",
section: {
id: "B4",
title: "5. Cumplimiento normativo y certificaciones previas",
description: "",
},
question: {
title: "a) Cumple requisitos legales ambientales, igualdad, laborales",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "B4-02"
},
{
id: "B4-02",
type: "question",
section: {
id: "B4",
title: "5. Cumplimiento normativo y certificaciones previas",
description: "",
},
question: {
title: "b) Cuenta ya con algún certificado (ISO, Comercio Justo, etc.)",
description: "Siendo 1 la puntuación más baja y 5 la más alta.",
recommendation: ""
},
input: {
type: "range",
label: "",
options: [
{ value: "1", label: "1", score: 1},
{ value: "2", label: "2", score: 2},
{ value: "3", label: "3", score: 3},
{ value: "4", label: "4", score: 4},
{ value: "5", label: "5", score: 5},
]
},
questionTotalPoints: 5,
next: "F4"
},
{
id: "F4",
type: "feedback",
section: {
id: "B4",
title: "5. Cumplimiento normativo y certificaciones previas",
description: ""
},
title: "Resultados del bloque",
subtitle: "",
button: "Entendido · Continuar",
next: "end"
},
{
id: "end",
type: "end",
title: "¡Aquí están los resultados!",
subtitle: "Gracias por completar el cuestionario.",
}
],
rules: {
B0: (score) => {
if (score <= 12) return "Redactar y aprobar una memoria de sostenibilidad alineada con los valores de la entidad.<br>Nombrar a una persona responsable (aunque sea a tiempo parcial).<br>Incluir objetivos sociales y ambientales en el plan estratégico"
if (score > 12 && score < 21) return "Revisar y actualizar la política de sostenibilidad.<br>Establecer un grupo de trabajo en sostenibilidad<br>Definir indicadores clave y medirlos periódicamente.<br>Definir indicadores clave y medirlos periódicamente"
return "¡Estáis en buen camino!"
},
B1: (score) => {
if (score <= 12) return "Diseñar un sistema simple para medir empleo local, inclusión y equidad.<br>Desarrollar programas o colaboraciones con la comunidad.<br>Documentar buenas prácticas sociales."
if (score > 12 && score < 21) return "Ampliar el sistema de medición a nuevos indicadores sociales.<br>Formalizar la comunicación de los logros sociales<br>Involucrar a socios y trabajadores en proyectos de impacto social."
return "¡Estáis en buen camino!"
},
B2: (score) => {
if (score <= 12) return "Medir el consumo de energía, agua y recursos al menos trimestralmente.<br>Implementar un plan básico de gestión de residuos y emisiones.<br>Explorar opciones de energías renovables accesibles."
if (score > 12 && score < 21) return "Establecer objetivos de reducción con plazos<br>Documentar mejoras ambientales logradas<br>Iniciar un plan de economía circular o reutilización"
return "¡Estáis en buen camino!"
},
B3: (score) => {
if (score <= 12) return "Elaborar un primer informe de sostenibilidad (puede ser breve)<br>Publicar compromisos y acciones en la web o redes<br>Adoptar un formato sencillo de reporte basado en GRI o similar"
if (score > 12 && score < 21) return "Incluir objetivos y áreas de mejora en la comunicación<br>Verificar externamente la información clave<br>Capacitar al equipo en comunicación responsable"
return "¡Estáis en buen camino!"
},
B4: (score) => {
if (score <= 12) return "Revisar el cumplimiento legal en materia ambiental, laboral y de igualdad<br>Corregir incumplimientos y documentar las acciones<br>Investigar certificaciones sectoriales básicas"
if (score > 12 && score < 21) return "Preparar auditorías internas anuales<br>Renovar o ampliar certificaciones existentes<br>Integrar criterios legales en la gestión diaria"
return "¡Estáis en buen camino!"
}
},
finalRecommendations: {
total: (score) => {
if (score <= 25) {
return "Certificado regional de RSE, Informe básico GRI no certificado, memoria de sostenibilidad"
} else if (score <= 40) {
return "B Corp, ISO 14001, ISO 45001"
} else {
return "GRI verificado, EMAS, combinación B Corp + GRI verificado"
}
}
}
}

655
data/iguales.js Normal file
View File

@@ -0,0 +1,655 @@
export const surveyData = {
title: "Formulario Demo",
slices: [
{
id: "welcome",
type: "welcome",
section: {
title: "Introducción",
description: ""
},
title: "Bienvenida a Iguales",
subtitle: "Cuestionario de evaluación",
button: "Comenzar",
next: "B0-01"
},
{
id: "B0-01",
type: "question",
section: {
id: "B0",
title: "1. Informativa",
description: "",
},
question: {
title: "a) Nº de trabajadores",
description: "Por favor, selecciona una opción",
recommendation: ""
},
input: {
type: "radio",
label: "Sector",
options: [
{ value: "<50", label: "Inferior a 50", score: 0},
{ value: ">50", label: "Superior a 50", score: 0},
],
},
questionTotalPoints: 1,
next: "end"
},
{
id: "B0-02",
type: "question",
section: {
id: "B0",
title: "1. Informativa",
description: "",
},
question: {
title: "b) ¿Tiene plan de igualdad?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
{ value: "nsnc", label: "No sabe / No contesta", score: 0},
],
},
questionTotalPoints: 1,
next: "B1-01"
},
{
id: "B1-01",
type: "question",
section: {
id: "B1",
title: "2. Brecha de género y salarial",
description: "",
},
question: {
title: "a) Cual es la proporción entre Hombres/Mujeres de toda la plantilla con vinculación laboral (no perfiles voluntarios)",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: ">60", label: "Más del 60% son mujeres", score: 0},
{ value: "<60", label: "Menos del 60% son mujeres", score: 0},
{ value: "60/40", label: "60% Hombres / 40% Mujeres", score: 1},
],
},
questionTotalPoints: 1,
next: "B1-02"
},
{
id: "B1-02",
type: "question",
section: {
id: "B1",
title: "2. Brecha de género y salarial",
description: "",
},
question: {
title: "b) A la hora de incorporar nuevos perfiles en la entidad, ¿existe un proceso de selección y reclutamiento basados en principios de transparencia e igualdad, público y difundido interna y externamente con perspectiva de género?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
{ value: "nsnc", label: "No sabe / No contesta", score: 0},
],
},
questionTotalPoints: 1,
next: "B1-03"
},
{
id: "B1-03",
type: "question",
section: {
id: "B1",
title: "2. Brecha de género y salarial",
description: "",
},
question: {
title: "c) ¿Existe algún puesto/ocupación en que solo hayan contratado hombres?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
{ value: "nsnc", label: "No sabe / No contesta", score: 0},
],
},
questionTotalPoints: 1,
next: "B1-04"
},
{
id: "B1-04",
type: "question",
section: {
id: "B1",
title: "2. Brecha de género y salarial",
description: "",
},
question: {
title: "d) ¿Existe algún puesto/ocupación en que solo hayan contratado mujeres?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
{ value: "nsnc", label: "No sabe / No contesta", score: 0},
],
},
questionTotalPoints: 1,
next: "B1-05"
},
{
id: "B1-05",
type: "question",
section: {
id: "B1",
title: "2. Brecha de género y salarial",
description: "",
},
question: {
title: "e) ¿Cuál es la retribución media salarial que predomina en vuestra entidad?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "igual", label: "La retribución media salarial mensual de los hombres es igual que la retribución media mensual de las mujeres", score: 1},
{ value: "menor-hombres", label: "La retribución media salarial mensual de los hombres es menor", score: 0},
{ value: "menor-mujeres", label: "La retribución media salarial mensual de las mujeres es menor", score: 0},
],
},
questionTotalPoints: 1,
next: "B1-06"
},
{
id: "B1-06",
type: "question",
section: {
id: "B1",
title: "2. Brecha de género y salarial",
description: "",
},
question: {
title: "f) ¿Cuál es la proporción de Hombres/Mujeres en cargos/puestos en mandos intermedios?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: ">60-H", label: "Los mando intermedios están mayoritariamente ocupados por hombres (+ del 60%)", score: 0},
{ value: ">60-M", label: "Los mando intermedios están mayoritariamente ocupados por mujeres (+ del 60%)", score: 0},
{ value: "50/50", label: "Los mando intermedios están ocupados de manera igualitaria (50%-50%)", score: 1},
],
},
questionTotalPoints: 1,
next: "B1-07"
},
{
id: "B1-07",
type: "question",
section: {
id: "B1",
title: "2. Brecha de género y salarial",
description: "",
},
question: {
title: "g) ¿Cuál es la proporción de Hombres/Mujeres en cargos directivos?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: ">60-H", label: "Los mando intermedios están mayoritariamente ocupados por hombres (+ del 60%)", score: 0},
{ value: ">60-M", label: "Los mando intermedios están mayoritariamente ocupados por mujeres (+ del 60%)", score: 0},
{ value: "50/50", label: "Los mando intermedios están ocupados de manera igualitaria (50%-50%)", score: 1},
],
},
questionTotalPoints: 1,
next: "F1-B1"
},
{
id: "F1-B1",
type: "feedback",
section: {
id: "B1",
title: "2. Brecha de género y salarial",
description: ""
},
title: "Resultados del bloque",
subtitle: "",
button: "Entendido · Continuar",
next: "B2-01"
},
{
id: "B2-01",
type: "question",
section: {
id: "B2",
title: "3. Formación y promoción",
description: "",
},
question: {
title: "a) El plan o acciones de formación se han comunicado de forma abierta con opciones flexibles de horario y perspectiva de género",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "B2-02"
},
{
id: "B2-02",
type: "question",
section: {
id: "B2",
title: "3. Formación y promoción",
description: "",
},
question: {
title: "b) Se han realizado acciones formativas en habilidades de liderazgo o específicas de dirección de forma transversal para incentivar la promoción igualitaria?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "B2-03"
},
{
id: "B2-03",
type: "question",
section: {
id: "B2",
title: "3. Formación y promoción",
description: "",
},
question: {
title: "c) ¿Existen alternativas flexibles de formación (online/presencial) transversal para toda la plantilla?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "B2-04"
},
{
id: "B2-04",
type: "question",
section: {
id: "B2",
title: "3. Formación y promoción",
description: "",
},
question: {
title: "d) ¿Existen procesos de evaluación por objetivos y/o méritos formulados transversal e igualitariamente con perspectiva de género?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "B2-05"
},
{
id: "B2-05",
type: "question",
section: {
id: "B2",
title: "3. Formación y promoción",
description: "",
},
question: {
title: "e) ¿Existen procesos de evaluación por objetivos y/o méritos formulados transversal e igualitariamente con perspectiva de género?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "F1-B2"
},
{
id: "F1-B2",
type: "feedback",
section: {
id: "B2",
title: "3. Formación y promoción",
description: ""
},
title: "Resultados del bloque",
subtitle: "",
button: "Entendido · Continuar",
next: "B3-01"
},
{
id: "B3-01",
type: "question",
section: {
id: "B301",
title: "4.1. Prevención y conciliación",
description: "",
},
question: {
title: "a) ¿Existen procesos para la gestión de teletrabajo/herramientas adecuadas (software/hardware) provistos por la organización?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "F1-B3-1"
},
{
id: "F1-B3-1",
type: "feedback",
section: {
id: "B301",
title: "4. Prevención y conciliación",
description: ""
},
title: "Resultados del subbloque",
subtitle: "",
button: "Entendido · Continuar",
next: "B3-02"
},
{
id: "B3-02",
type: "question",
section: {
id: "B302",
title: "4.2. Prevención y conciliación",
description: "",
},
question: {
title: "b) Existe la posibilidad de trabajo voluntario y/o flexibilidad laboral bajo políticas igualitarias",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "B3-03"
},
{
id: "B3-03",
type: "question",
section: {
id: "B302",
title: "4.2. Prevención y conciliación",
description: "",
},
question: {
title: "c) ¿Existe un programa/politicas de conocimiento público sobre plan de conciliación laboral?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "B3-04"
},
{
id: "B3-04",
type: "question",
section: {
id: "B302",
title: "4.2. Prevención y conciliación",
description: "",
},
question: {
title: "d) ¿Cuentan con beneficios/servicios sociales? (guarderías/beneficios/descuentos)",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "B3-05"
},
{
id: "B3-05",
type: "question",
section: {
id: "B302",
title: "4.2. Prevención y conciliación",
description: "",
},
question: {
title: "e) ¿Se realizan campañas de sensibilización/divulgación sobre la corresponsabilidad de tareas domésticas/conciliación , cuidado de personas dependientes?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "F1-B3-2"
},
{
id: "F1-B3-2",
type: "feedback",
section: {
id: "B302",
title: "4.2. Prevención y conciliación",
description: ""
},
title: "Resultados del subbloque",
subtitle: "",
button: "Entendido · Continuar",
next: "B3-06"
},
{
id: "B3-06",
type: "question",
section: {
id: "B303",
title: "4.3. Prevención y conciliación",
description: "",
},
question: {
title: "f) Se ha realizado alguna campaña o acción de divulgación de información sobre los derechos de las mujeres victimas de violencia de género? (charlas en colaboración con organizaciones expertas en violencia de género)",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "B3-07"
},
{
id: "B3-07",
type: "question",
section: {
id: "B303",
title: "4.3. Prevención y conciliación",
description: "",
},
question: {
title: "g) Acerca del protocolo de acoso (sexual o por sexo) y procedimientos específicos para la prevención del acoso, conocimiento de los procesos, causa de las denuncias y reclamaciones. Responde por favor las siguientes preguntas; ¿Existe código ético de conducta y/o protocolo de acoso con la descripción de procedimientos para lam prevención y actuación en caso de acoso (sexual o por sexo)?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "B3-08"
},
{
id: "B3-08",
type: "question",
section: {
id: "B303",
title: "4.3. Prevención y conciliación",
description: "",
},
question: {
title: "h) ¿Existen formularios o plantillas de acceso público de denuncia por acoso sexual o por sexo?",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "B3-09"
},
{
id: "B3-09",
type: "question",
section: {
id: "B303",
title: "4.3. Prevención y conciliación",
description: "",
},
question: {
title: "i) El plan de PRL o normas de prevención laboral ¿Incluye(n) perspectiva de género para prevenir acoso sexual o por sexo? (disposición de los puestos de trabajo, acceso a servicios, herramientas y equipos de trabajo)",
description: "",
recommendation: ""
},
input: {
type: "radio",
label: "",
options: [
{ value: "si", label: "Si", score: 1},
{ value: "no", label: "No", score: 0},
],
},
questionTotalPoints: 1,
next: "F1-B3-3"
},
{
id: "F1-B3-3",
type: "feedback",
section: {
id: "B303",
title: "4. Prevención y conciliación",
description: ""
},
title: "Resultados del subbloque",
subtitle: "",
button: "Entendido · Continuar",
next: "end"
},
{
id: "end",
type: "end",
title: "¡Aquí están los resultados!",
subtitle: "Este es el diagnóstico de tu entidad en igualdad de género. Puedes leer cada bloque o descargar el informe completo en PDF. Para interpretar los resultados y planificar próximos pasos, sigue el curso IGUALES del Aula Virtual.",
}
],
rules: {
B1: (score) => score >= 6 ? "Vuestra entidad está comprometida con la eliminación de las brechas de género y salariales en base a género, y está haciendo las cosas bien. Ha conseguido acercarse a la proporción igualitaria en la plantilla y que las remuneraciones se hayan equiparado o estén cercanas a ello." : "Os animamos a continuar trabajando para la eliminación de las brechas de género y a conseguir la igualdad tanto en la proporción de hombres y mujeres en plantilla como la equiparación salarial. Queda camino pero conseguir una entidad igualitaria impacta positivamente en la sociedad.",
B2: (score) => score >= 3 ? "La formación y la promoción interna de vuestra entidad tiene perspectiva de género y muestra un compromiso con la igualdad de oportunidades para toda la plantilla. Os animamos a continuar así y (si no tienen Plan Igualdad) ha plasmar todas vuestras políticas en un plan de igualdad- (si tienen Plan de Igualdad) a comunicar vuestras políticas igualitarias a toda vuestra cadena de valor, colaboradores y entidades afines." : "Es importante que las entidades apliquen la perspectiva de género en materia formativa y de promoción de la plantilla. El compromiso de la entidad en este área reportará grandes beneficios a medio y largo plazo, teniendo una plantilla que integra la igualdad desde el conocimiento y naturaliza unos patrones sostenibles. No hay tiempo que perder, trabajemos en ello!",
B301: (score) => score >= 1 ? "La gestión del teletrabajo es una potente herramienta para la conciliación por lo que, el hecho de dotar de herramientas a la entidad ya es un simbolo del compromiso con la igualdad de oportunidades, enhorabuena." : "Es interesante incorporar medidas en las que el teletrabajo ayude a la conciliación y a la igualdad, no te quedes atrás y explora esa vía para fortalecer tu organización y la igualdad",
B302: (score) => score >= 2 ? "La conciliación y el poner los cuidados en el centro hacen de una entidad un lugar más lugar más amable e igualitario y no solo a nivel personal, sino también a nivel colectivo reporta grandes beneficios como el aumento de productividad, reducción del absentismo, fijación del talento y sostenibilidad de la plantilla. Un reconocimiento al buen hacer de la entidad integrando estas medidas de igualdad." : "La incorporación de medidas de conciliación pueden reportar grandes beneficios a vuestra entidad por lo que os impulsamos a incluir pequeños, pero firmes, pasos en el cuidado de la plantilla y la conciliación para tener un entorno laboral positivo y que el bienestar del equipo repercuta en un espacio más productivo y colaborativo. ",
B303: (score) => score >= 2 ? "Los protocolos de acoso y todas las medidas para frenar actitudes discriminatorias por razón de sexo o de cualquier otra índole que habéis incorporado en la entidad son imprescindibles para hacer un entorno más igualitario y sin duda alguna os han ayudado a crear un ambiente de trabajo sano e igualitario impactando positivamente en todos los aspectos de la organización. Esto os conviente en referentes de igualdad en vuestro sector y esperamos permee a toda vuestra cadena de valor!" : "Es imprescindible que la organización haga un esfuerzo y trabaje en la igualdad creando e implantando medidas que eviten el acoso y la discriminación por razón de sexo o de cualquier otra índole. Además tomar medidas y formar al equipo para dar visibilidad a la violencia de género ayuda no solo a un entorno de trabajo consciente y saludable sino también a una sociedad igualitaria y justa. Todos y todas somos necesarios para lograr la igualdad real."
}
}

6
eslint.config.mjs Normal file
View File

@@ -0,0 +1,6 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// Your custom configs here
)

5
layouts/default.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<div>
<NuxtPage />
</div>
</template>

69
nuxt.config.ts Normal file
View File

@@ -0,0 +1,69 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
import tailwindcss from "@tailwindcss/vite";
import { defineNuxtConfig } from "nuxt/config";
export default defineNuxtConfig({
devServer: {
port: 4000,
},
compatibilityDate: '2025-07-15',
srcDir: '.',
ssr: true,
devtools: { enabled: true },
modules: ['@nuxt/eslint'],
css: ['~/assets/css/main.css'],
vite: {
plugins: [tailwindcss()]
},
app: {
//TODO: Cambiar los metadatos
head: {
title: "Formulario",
htmlAttrs: {
lang: "es",
},
link: [
{ rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/tailwindcss@3.3.2/dist/tailwind.min.css' }
]
// meta: [
// { charset: "utf-8" },
// { name: "viewport", content: "width=device-width, initial-scale=1" },
// {
// hid: "description",
// name: "description",
// content:
// "Kit de herramientas para el desarrollo sostenible de las entidades de economía social",
// },
// { hid: "og-type", property: "og:type", content: "website" },
// {
// hid: "og-title",
// property: "og:title",
// content: "KIT ECONOMIA SOCIAL",
// },
// {
// hid: "og-desc",
// property: "og:description",
// content:
// "KIT ECONOMIA SOCIAL - Kit de herramientas para el desarrollo sostenible de las entidades de economía social",
// },
// {
// hid: "og-image",
// property: "og:image",
// content: "https://www.kit-eco.social/es/thumbnail.png",
// },
// {
// hid: "og-url",
// property: "og:url",
// content: "https://www.kit-eco.social/es/",
// },
// { hid: "t-type", name: "twitter:card", content: "summary_large_image" },
// { hid: "whatsapp-image", name: "whatsapp:image", content: "https://www.kit-eco.social/es/thumbnail.png" },
// { name: "format-detection", content: "telephone=no" },
// ],
// link: [
// { rel: 'canonical', href: 'https://www.kit-eco.social/es/'},
// { rel: "icon", type: "image/x-icon", href: "/favicon.ico" }
// ]
},
},
});

12961
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"start": "node .output/server/index.mjs",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/eslint": "^1.7.1",
"@tailwindcss/vite": "^4.1.11",
"eslint": "^9.31.0",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.3",
"nuxt": "^3.17.7",
"tailwindcss": "^4.1.11",
"vue": "^3.5.17",
"vue-router": "^4.5.1",
"vuex": "^4.0.2"
}
}

243
pages/index.vue Normal file
View File

@@ -0,0 +1,243 @@
<template>
<FormSlice :type="currentSlice.type">
<template v-if="currentSliceComponent">
<transition name="fade" mode="out-in">
<component
:is="currentSliceComponent"
:key="currentSliceId"
:data="currentSlice"
:current-index="currentQuestionIndex"
:total="questionSlices.length"
@previous="goToPrevious"
@next="goToNext($event)"
/>
</transition>
</template>
<br>
</FormSlice>
</template>
<script>
import { surveyData } from "@/data/iguales.js" // Ajusta la ruta según la ubicación de tu archivo de datos
import FormSlice from "@/components/FormSlice.vue"
import SliceWelcome from "@/components/SliceWelcome.vue"
import SliceQuestion from "@/components/SliceQuestion.vue"
import SliceEnd from "@/components/SliceEnd.vue"
import ButtonCTA from "@/components/ButtonCTA.vue"
import SliceFeedback from "@/components/SliceFeedback.vue"
export default {
components: {
FormSlice,
SliceWelcome,
SliceQuestion,
SliceEnd,
SliceFeedback,
ButtonCTA
},
data() {
return {
surveyData,
currentSliceId: surveyData.slices[0].id,
answers: [],
score: null,
results: null
}
},
computed: {
currentSlice() {
return surveyData.slices.find(s => s.id === this.currentSliceId)
},
currentSliceComponent() {
switch (this.currentSlice.type) {
case "welcome": return "SliceWelcome"
case "question": return "SliceQuestion"
case "feedback": return "SliceFeedback"
case "end": return "SliceEnd"
default: return "SliceQuestion"
}
},
questionSlices() {
return surveyData.slices.filter(s => s.type === "question")
},
currentQuestionIndex() {
return this.questionSlices.findIndex(s => s.id === this.currentSliceId)
},
},
watch: {
currentSlice() {
if (this.currentSlice.type === "feedback") {
this.calculateBlockResult()
}
if (this.currentSlice.type === "end") {
this.calculateFinalScore()
}
}
},
// mounted() {
// // Al montar, escuchar el evento de recarga/cierre
// window.addEventListener("beforeunload", this.clearStorageOnReload)
// },
// beforeUnmount() {
// // Limpiar el listener al desmontar el componente
// window.removeEventListener("beforeunload", this.clearStorageOnReload)
// },
methods: {
clearStorageOnReload() {
localStorage.removeItem("previous-slices")
localStorage.removeItem("answers")
localStorage.removeItem("score")
localStorage.removeItem("results")
localStorage.removeItem("final-score")
},
goToNext(event) {
if (this.currentSlice.type === "question" && event !== undefined) {
const answerObj = {
sectionId: this.currentSlice.section.id,
sectionTitle: this.currentSlice.section.title,
sliceId: this.currentSlice.id,
value: event.value,
score: event.score,
questionTotalPoints: this.currentSlice.questionTotalPoints || 0,
}
this.saveAnswer(answerObj)
}
const nextConfig = this.currentSlice.next
//console.log("Navegando desde slice:", this.currentSliceId, "con configuración next:", nextConfig)
if (!nextConfig) {
////console.log("Encuesta finalizada", this.answers)
return
}
// Guardar la respuesta en localStorage 'previous-slices' y avanzar a la siguiente pregunta
if (nextConfig) {
const previousSlices = JSON.parse(localStorage.getItem('previous-slices')) || []
previousSlices.push(this.currentSliceId)
localStorage.setItem('previous-slices', JSON.stringify(previousSlices))
}
if (typeof nextConfig === "string") {
// Navegación lineal
this.currentSliceId = nextConfig
} else if (typeof nextConfig === "object") {
// Navegación condicional por valor de respuesta
const nextSlice = nextConfig[event]
if (nextSlice) {
this.currentSliceId = nextSlice
} else {
console.warn("No se encontró siguiente slice para:", event)
}
}
},
goToPrevious() {
// Leer el historial
const previousSlices = JSON.parse(localStorage.getItem('previous-slices')) || []
////console.log("Historial de slices anteriores:", previousSlices)
if (previousSlices.length === 0) {
console.warn("No hay slices anteriores en el historial")
return
}
// Sacar el último paso visitado
const lastSliceId = previousSlices.pop()
const currentAnswer = this.answers.pop() // Eliminar la última respuesta guardada
// Actualizar el historial en localStorage
localStorage.setItem('previous-slices', JSON.stringify(previousSlices))
localStorage.setItem('answers', JSON.stringify(this.answers))
// Cambiar al slice anterior
this.currentSliceId = lastSliceId
},
saveAnswer(payload) {
// payload: { id: "question1", value: "Si" }
////console.log("Guardando respuesta:", payload)
let el;
if (typeof payload !== "object") {
el = {
value: payload,
label: payload,
score: 0,
}
} else {
el = payload
}
////console.log("Guardando respuesta:", el)
this.answers.push(el)
localStorage.setItem("answers", JSON.stringify(this.answers))
},
calculateBlockResult() {
// Calcular el score basado en las respuestas guardadas con el mismo sectionId
const answers = JSON.parse(localStorage.getItem('answers'))
if (answers && answers.length > 0) {
const currentSectionId = this.currentSlice.section.id
const sectionAnswers = answers.filter(a => a.sectionId === currentSectionId)
const sectionScore = sectionAnswers.reduce((acc, curr) => acc + (curr.score || 0), 0)
//console.log(`Score para la sección ${currentSectionId}:`, sectionScore)
// En funcion de la sección, aplicar regla si existe
const evaluate = surveyData.rules[currentSectionId]
const sectionResult = evaluate ? evaluate(sectionScore) : "SIN REGLA"
//console.log(`Resultado para la sección ${currentSectionId}:`, sectionResult)
// Calcular los puntos totales posibles para la sección
const sectionPossibleTotalPoints = sectionAnswers.reduce((acc, curr) => acc + (curr.questionTotalPoints || 0), 0)
//console.log(`Puntos totales posibles para la sección ${currentSectionId}:`, sectionPossibleTotalPoints)
// Guardar el resultado parcial en localStorage
const existingResults = JSON.parse(localStorage.getItem('results')) || []
const updatedResults = existingResults.filter(r => r.sectionId !== currentSectionId) // Eliminar resultado previo de la misma sección
updatedResults.push({ sectionId: currentSectionId, sectionTitle: this.currentSlice.section.title, sectionPossibleTotalPoints: sectionPossibleTotalPoints, score: sectionScore, result: sectionResult })
localStorage.setItem('results', JSON.stringify(updatedResults))
}
},
calculateFinalScore() {
const results = JSON.parse(localStorage.getItem('results'))
if (results && results.length > 0) {
// Calcular el número total de puntos posibles
const totalPossiblePoints = results.reduce((acc, curr) => acc + (curr.sectionPossibleTotalPoints || 0), 0)
// Calcular el score total sumando los scores de cada sección
const totalScore = results.reduce((acc, curr) => acc + (curr.score || 0), 0)
this.score = totalScore
// En funcion del score total, aplicar regla si existe
const finalRecommendations = surveyData.finalRecommendations?.total?.(totalScore) ?? null;
// Guardar el score total en localStorage
localStorage.setItem('final-score', JSON.stringify({ totalPossiblePoints: totalPossiblePoints, score: totalScore, recommendations: finalRecommendations || "" }))
this.results = results
}
}
}
}
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.fade-enter-to, .fade-leave-from {
opacity: 1;
}
</style>

7
plugins/vuex.client.js Normal file
View File

@@ -0,0 +1,7 @@
import { kitecosocialStore } from '~/store/index';
export default defineNuxtPlugin((nuxtApp) => {
const store = kitecosocialStore()
nuxtApp.vueApp.use(store)
nuxtApp.provide('store', store)
});

7
plugins/vuex.server.js Normal file
View File

@@ -0,0 +1,7 @@
import { kitecosocialStore } from '~/store/index';
export default defineNuxtPlugin((nuxtApp) => {
const store = kitecosocialStore()
nuxtApp.vueApp.use(store)
nuxtApp.provide('store', store)
});

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow:

BIN
public/thumbnail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

3
server/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

9
store/index.js Normal file
View File

@@ -0,0 +1,9 @@
import { createStore } from 'vuex';
import { langcodeModule } from './langcode';
export const kitecosocialStore = () =>
createStore({
modules: {
langcode: langcodeModule,
},
});

18
store/langcode.js Normal file
View File

@@ -0,0 +1,18 @@
export const langcodeModule = {
state: () => ({
langcode: 'es'
}),
mutations: {
SET_LANGCODE: (state, langcode) => {
state.langcode = langcode;
}
},
actions: {
setLangcode: ({ commit }, langcode) => {
commit("SET_LANGCODE", langcode);
}
},
getters: {
langcode: (state) => state.langcode
},
};

0
tailwind.config.js Normal file
View File

3
utils/dates.js Normal file
View File

@@ -0,0 +1,3 @@
export const getCurrentYear = () => {
return new Date().getFullYear().toString()
}