Compare commits

..

10 Commits

Author SHA1 Message Date
Ichitux
ced7925efa Enable Jenkins pipeline
Some checks failed
Node.js CI / build (18.x) (push) Has been cancelled
Node.js CI / build (20.x) (push) Has been cancelled
Node.js CI / build (22.x) (push) Has been cancelled
2026-03-14 19:45:45 +01:00
Ichitux
09adbf2964 Update node.js.yml 2026-03-13 15:45:05 +01:00
Ichitux
a89159f081 Update node.js.yml 2026-03-13 15:42:06 +01:00
Ichitux
3ff3ca6efa Update node.js.yml 2026-03-13 15:34:00 +01:00
Ichitux
6fdfec1e9e Update README.md 2026-03-13 15:31:46 +01:00
Ichitux
61407a3051 Merge pull request #1 from Ichitux/github-actions
Create node.js.yml
2026-03-13 15:25:25 +01:00
Ichitux
7d68f99ca8 Create node.js.yml 2026-03-13 15:05:05 +01:00
Antoni Nuñez Romeu
e070923f75 Hotfixes on height, staff section & carrousel 2026-03-13 14:53:40 +01:00
Antoni Nuñez Romeu
681d528c9e Hotfix scroll up when submit form 2026-03-13 10:59:46 +01:00
Ichitux
459792bdb0 Server mapping local for CORS 2026-03-12 02:08:14 +01:00
18 changed files with 357 additions and 194 deletions

40
.github/workflows/node.js.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- name: Test Summary
uses: test-summary/action@v2
with:
paths: "test/results/**/TEST-*.xml"
output: test-summary.md
if: always()
- name: Use checkout repository
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm test

75
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,75 @@
pipeline {
agent any
environment {
REMOTE_HOST = '192.168.1.102'
REMOTE_DIR = '/home/zouklambadabcn.com/public_html'
PM2_APP = 'ZLB'
// Name of Jenkins Credentials (Username with private key) to SSH
SSH_CREDS = 'ssh-remote' // <-- configure this in Jenkins Credentials
}
options {
ansiColor('xterm')
timestamps()
}
stages {
stage('Deploy over SSH') {
steps {
script {
// Validate Jenkins has the required credential
withCredentials([sshUserPrivateKey(credentialsId: env.SSH_CREDS, keyFileVariable: 'SSH_KEY', usernameVariable: 'SSH_USER')]) {
// Build the remote command to run
def remoteCmd = """
set -euo pipefail
cd "${env.REMOTE_DIR}"
echo "[$(date)] PWD=$(pwd) on ${HOSTNAME}"
# Ensure repo is cleanly updated
git fetch --all --prune
git reset --hard HEAD
git pull --rebase --autostash || git pull
# Use npm if available, fallback to npx if needed
if command -v npm >/dev/null 2>&1; then
npm ci || npm install
npm run build
else
npx --yes npm@latest ci || npx --yes npm@latest install
npx --yes npm@latest run build
fi
# Restart pm2 app
if command -v pm2 >/dev/null 2>&1; then
pm2 restart "${PM2_APP}" || pm2 start npm --name "${PM2_APP}" -- run start
pm2 save || true
else
echo 'pm2 not found in PATH' >&2
exit 1
fi
""".stripIndent()
// SSH options for non-interactive, secure connection
def sshOpts = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
// Execute remote command via ssh
sh label: 'Run remote deployment', script: "ssh -i \"${SSH_KEY}\" ${sshOpts} \"${SSH_USER}@${REMOTE_HOST}\" 'bash -lc '\''" + remoteCmd.replace("'", "'\''") + "'\'' '"
}
}
}
}
}
post {
success {
echo 'Deployment completed successfully.'
}
failure {
echo 'Deployment failed.'
}
always {
cleanWs(deleteDirs: true, notFailBuild: true)
}
}
}

View File

@@ -1,25 +1,3 @@
# Welcome to your Lovable project
## Project info
**URL**: https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID
## How can I edit this code?
There are several ways of editing your application.
**Use Lovable**
Simply visit the [Lovable Project](https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID) and start prompting.
Changes made via Lovable will be committed automatically to this repo.
**Use your preferred IDE**
If you want to work locally using your own IDE, you can clone this repo and push changes. Pushed changes will also be reflected in Lovable.
The only requirement is having Node.js & npm installed - [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
Follow these steps:
```sh
@@ -61,13 +39,20 @@ This project is built with:
- Tailwind CSS
## How can I deploy this project?
```sh
# Step 1: Clone the repository using the project's Git URL.
git clone <YOUR_GIT_URL>
Simply open [Lovable](https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID) and click on Share -> Publish.
# Step 2: Navigate to the project directory.
cd <YOUR_PROJECT_NAME>
## Can I connect a custom domain to my Lovable project?
# Step 3: Install the necessary dependencies.
npm i
Yes, you can!
# Step 4: Start the development server with auto-reloading and an instant preview.
npm run build
To connect a domain, navigate to Project > Settings > Domains and click Connect Domain.
npm install -g serve
Read more here: [Setting up a custom domain](https://docs.lovable.dev/features/custom-domain#custom-domain)
serve -s dist
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
src/assets/staff/djbiel.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

BIN
src/assets/staff/djwinx.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

View File

@@ -45,6 +45,7 @@ const BookingSection = () => {
const [showCountryDropdown, setShowCountryDropdown] = useState(false);
const countryDropdownRef = useRef<HTMLDivElement>(null);
const countryInputRef = useRef<HTMLInputElement>(null);
const sectionRef = useRef<HTMLElement>(null);
// Generate unique requestId on component mount
useEffect(() => {
const uniqueId = `REQ-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
@@ -107,6 +108,13 @@ const BookingSection = () => {
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [showCountryDropdown]);
// Ensure the booking section stays in view when status changes (e.g., after submit)
useEffect(() => {
if (status === "success" || status === "error") {
sectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
}
}, [status]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setErrors({});
@@ -141,7 +149,7 @@ const BookingSection = () => {
if (status === "success") {
return (
<section id="booking" className="section-padding bg-background">
<section id="booking" ref={sectionRef} className="section-padding bg-background scroll-mt-24">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
@@ -160,7 +168,7 @@ const BookingSection = () => {
}
return (
<section id="booking" className="section-padding bg-background">
<section id="booking" ref={sectionRef} className="section-padding bg-background scroll-mt-24">
<div className="container mx-auto max-w-2xl">
<motion.div
initial={{ opacity: 0, y: 20 }}

View File

@@ -4,7 +4,7 @@ import communityImg from "@/assets/community.jpg";
import { Instagram, Facebook, Youtube } from "lucide-react";
const OrgSection = () => (
<section className="section-padding bg-card">
<section className="section-padding bg-card py-20 md:py-28">
<div className="container mx-auto">
<div className="grid md:grid-cols-2 gap-12 items-center">
{/* Texto */}
@@ -70,7 +70,7 @@ const OrgSection = () => (
<img
src={communityImg}
alt="Comunidad ZoukLambadaBCN"
className="rounded-2xl shadow-elevated w-full object-cover aspect-square"
className="rounded-2xl shadow-elevated w-full h-auto object-contain bg-muted"
/>
</motion.div>
</div>

View File

@@ -1,67 +1,8 @@
import { motion } from "framer-motion";
import { User } from "lucide-react";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
} from "@/components/ui/carousel";
import { PROFESORES } from "@/data/event-data";
/**
* ProfesoresSection has been removed intentionally.
* Exporting a no-op component to avoid breaking imports.
*/
const ProfesoresSection = () => (
<section id="profesores" className="section-padding bg-background">
<div className="container mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
Profesores
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
Los mejores profesores internacionales de Lambada te esperan en este festival.
</p>
</motion.div>
<div className="max-w-4xl mx-auto px-12">
<Carousel opts={{ loop: true, align: "start" }}>
<CarouselContent>
{PROFESORES.map((prof, i) => (
<CarouselItem key={i} className="md:basis-1/2 lg:basis-1/3">
<div className="bg-card rounded-2xl overflow-hidden shadow-card h-full">
<div className="aspect-[3/4] bg-muted flex items-center justify-center overflow-hidden">
{prof.image ? (
<img
src={prof.image}
alt={prof.name}
className="w-full h-full object-cover"
loading="lazy"
/>
) : (
<User className="w-16 h-16 text-muted-foreground/40" />
)}
</div>
<div className="p-4 text-center">
<h3 className="font-display text-lg font-bold text-foreground">
{prof.name}
</h3>
{prof.origin && (
<p className="text-sm text-muted-foreground">{prof.origin}</p>
)}
</div>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
</div>
</section>
);
const ProfesoresSection = () => null;
export default ProfesoresSection;

View File

@@ -1,6 +1,6 @@
import { motion } from "framer-motion";
import { STAFF } from "@/data/event-data";
import { Instagram, User } from "lucide-react";
import { Instagram, User, ChevronLeft, ChevronRight } from "lucide-react";
/** Colores de badge por rol */
const roleBadgeClass: Record<string, string> = {
@@ -9,32 +9,79 @@ const roleBadgeClass: Record<string, string> = {
Organizador: "bg-accent text-accent-foreground",
};
const StaffSection = () => (
const StaffSection = () => {
// simple ref + helpers for horizontal scroll
const onPrev = () => {
const scroller = document.getElementById("staff-scroller");
if (!scroller) return;
const card = scroller.querySelector("[data-staff-card]") as HTMLElement | null;
const delta = card ? card.offsetWidth + 24 : 320; // 24 ~ gap-x-6
scroller.scrollBy({ left: -delta, behavior: "smooth" });
};
const onNext = () => {
const scroller = document.getElementById("staff-scroller");
if (!scroller) return;
const card = scroller.querySelector("[data-staff-card]") as HTMLElement | null;
const delta = card ? card.offsetWidth + 24 : 320;
scroller.scrollBy({ left: delta, behavior: "smooth" });
};
return (
<section id="staff" className="section-padding bg-background">
<div className="container mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
className="flex items-end justify-between gap-4 mb-6 md:mb-8"
>
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
<div className="text-center md:text-left w-full">
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-2">
Staff del Evento
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
<p className="text-muted-foreground max-w-2xl mx-auto md:mx-0">
Conoce a los artistas e instructores que harán de este festival una experiencia inolvidable.
</p>
</div>
{/* Arrows (desktop) */}
<div className="hidden md:flex items-center gap-2 shrink-0">
<button
aria-label="Anterior"
onClick={onPrev}
className="h-10 w-10 rounded-full bg-card border border-input text-foreground hover:bg-accent hover:text-accent-foreground grid place-items-center shadow-sm"
>
<ChevronLeft className="h-5 w-5" />
</button>
<button
aria-label="Siguiente"
onClick={onNext}
className="h-10 w-10 rounded-full bg-card border border-input text-foreground hover:bg-accent hover:text-accent-foreground grid place-items-center shadow-sm"
>
<ChevronRight className="h-5 w-5" />
</button>
</div>
</motion.div>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Carousel scroller */}
<div className="relative">
{/* gradient edges */}
<div className="pointer-events-none absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-background to-transparent rounded-l-2xl" />
<div className="pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-background to-transparent rounded-r-2xl" />
<div
id="staff-scroller"
className="flex gap-6 overflow-x-auto snap-x snap-mandatory scroll-smooth pb-2 -mx-4 px-4 md:mx-0 md:px-0"
>
{STAFF.map((member, i) => (
<motion.div
key={member.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1 }}
className="bg-card rounded-2xl overflow-hidden shadow-card hover:shadow-elevated transition-shadow group"
transition={{ delay: i * 0.05 }}
data-staff-card
className="snap-start shrink-0 w-4/5 sm:w-1/2 lg:w-1/3 xl:w-1/4 bg-card rounded-2xl overflow-hidden shadow-card hover:shadow-elevated transition-shadow group"
>
{/* Foto */}
<div className="aspect-square bg-muted flex items-center justify-center overflow-hidden">
@@ -82,8 +129,28 @@ const StaffSection = () => (
</motion.div>
))}
</div>
{/* Arrows (mobile overlay) */}
<div className="md:hidden absolute inset-y-1/2 -translate-y-1/2 left-1 right-1 flex items-center justify-between pointer-events-none">
<button
aria-label="Anterior"
onClick={onPrev}
className="pointer-events-auto h-9 w-9 rounded-full bg-card/90 backdrop-blur border border-input text-foreground hover:bg-accent hover:text-accent-foreground grid place-items-center shadow"
>
<ChevronLeft className="h-5 w-5" />
</button>
<button
aria-label="Siguiente"
onClick={onNext}
className="pointer-events-auto h-9 w-9 rounded-full bg-card/90 backdrop-blur border border-input text-foreground hover:bg-accent hover:text-accent-foreground grid place-items-center shadow"
>
<ChevronRight className="h-5 w-5" />
</button>
</div>
</div>
</div>
</section>
);
};
export default StaffSection;

View File

@@ -9,6 +9,16 @@
*
* NOTA: Las imágenes deben colocarse en src/assets/ e importarse.
*/
import arielyasmin from "@/assets/staff/arielyasmin.jpg";
import hilaleo from "@/assets/staff/hilaleo.jpg";
import matheuslydia from "@/assets/staff/matheuslydia.jpg";
import omeradva from "@/assets/staff/omeradva.jpg";
import pablolena from "@/assets/staff/pablolena.jpg";
import djbiel from "@/assets/staff/djbiel.jpg";
import djwinx from "@/assets/staff/djwinx.jpg";
import djklebynho from "@/assets/staff/djklebynho.jpg";
// ---- INFORMACIÓN GENERAL DEL EVENTO ----
export const EVENT_INFO = {
@@ -49,8 +59,7 @@ export const WEBHOOK_URL = "https://n8n.hacecalor.net/webhook/event-reservation"
// ---- SOBRE EL EVENTO ----
export const ABOUT_EVENT = {
title: "Sobre el Evento",
description: `[Descripción del evento. Explica qué hace especial esta edición,
qué pueden esperar los asistentes, y por qué no se lo pueden perder.]`,
description: `El ZoukLambadaBCN Beach Festival es uno de los eventos más destacados del panorama internacional de baile, dedicado exclusivamente a las disciplinas del Zouk y la Lambazouk. Se celebra en la encantadora localidad de Santa Susana, en la costa del Maresme, aprovechando su entorno mediterráneo, sus amplias playas y su espíritu acogedor para crear una experiencia única que atrae a bailarines de todos los rincones del mundo. `,
lambadaInfo: `La Lambada es un baile brasileño nacido en los años 80,
conocido por su sensualidad, conexión y ritmo envolvente.
Mezcla influencias de forró, merengue y carimbó,
@@ -85,41 +94,93 @@ export const ABOUT_ORG = {
export const STAFF = [
{
id: "1",
name: "[Nombre Instructor 1]",
name: "[Pablo & Lena]",
role: "Instructor" as const,
description: "[Breve biografía del instructor]",
/** Reemplazar con ruta a foto real */
image: "",
image: pablolena,
socials: {
instagram: "",
},
},
{
id: "2",
name: "[Nombre Instructor 2]",
name: "[Ariel & Yasmin]",
role: "Instructor" as const,
description: "[Breve biografía del instructor]",
image: "",
image: arielyasmin,
socials: {
instagram: "",
},
},
{
id: "3",
name: "[Nombre DJ]",
name: "[Hila & Leo]",
role: "Instructor" as const,
description: "[Breve biografía del instructor]",
image: hilaleo,
socials: {
instagram: "",
},
},
{
id: "4",
name: "[Matheus & Lydia]",
role: "Instructor" as const,
description: "[Breve biografía del instructor]",
image: matheuslydia,
socials: {
instagram: "",
},
},
{
id: "5",
name: "[Omer & Adva]",
role: "Instructor" as const,
description: "[Breve biografía del instructor]",
image: omeradva,
socials: {
instagram: "",
},
},
{
id: "6",
name: "[DJ Biel]",
role: "DJ" as const,
description: "[Breve biografía del DJ]",
image: "",
image: djbiel,
socials: {
instagram: "",
soundcloud: "",
},
},
{
id: "4",
name: "[Nombre Organizador]",
role: "Organizador" as const,
description: "[Breve biografía del organizador]",
id: "7",
name: "[DJ WinX]",
role: "DJ" as const,
description: "[Breve biografía del DJ]",
image: djwinx,
socials: {
instagram: "",
soundcloud: "",
},
},
{
id: "8",
name: "[DJ Klebynho]",
role: "DJ" as const,
description: "[Breve biografía del DJ]",
image: djklebynho,
socials: {
instagram: "",
soundcloud: "",
},
},
{
id: "9",
name: "[Nombre Special Guest]",
role: "Special Guest" as const,
description: "[Breve biografía del invitado especial]",
image: "",
socials: {
instagram: "",
@@ -205,20 +266,6 @@ export const PRACTICAL_INFO = {
],
};
// ---- PROFESORES ----
/**
* Para añadir un profesor, agrega un objeto al array.
* Importa la imagen desde src/assets/ si es local.
*/
export const PROFESORES = [
{ name: "[Profesor 1]", image: "", origin: "[País]" },
{ name: "[Profesor 2]", image: "", origin: "[País]" },
{ name: "[Profesor 3]", image: "", origin: "[País]" },
{ name: "[Profesor 4]", image: "", origin: "[País]" },
{ name: "[Profesor 5]", image: "", origin: "[País]" },
{ name: "[Profesor 6]", image: "", origin: "[País]" },
];
// ---- GALERÍA ----
/**
* Añade URLs o importaciones de imágenes.

View File

@@ -24,7 +24,7 @@ export default {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(230.5, 57.6%, 74.1%);",
DEFAULT: "hsl(230.5, 57.6%, 74.1%)",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {

View File

@@ -6,7 +6,7 @@ import { componentTagger } from "lovable-tagger";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
server: {
host: "0.0.0.0",
origin: "http://0.0.0.0:8080",
port: 8080,
hmr: {
overlay: false,