Compare commits

..

11 Commits

Author SHA1 Message Date
Antoni Nuñez Romeu
9a1154a400 Enable pm2 deploy & changes in visual 2026-03-16 17:57:27 +01:00
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
19 changed files with 386 additions and 195 deletions

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

@@ -0,0 +1,68 @@
# 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
deploy:
needs: build
runs-on: Tonis-Mac-mini
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: PM2 Deployment
# You may pin to the exact commit or the version.
# uses: victorargento/pm2-deployment@9f1857537a23be1e6eb173bc5394a49c6e3e3e1a
uses: victorargento/pm2-deployment@0.1
with:
#
build: true # optional, default is false
# Remote host
host: 192.168.1.102
# Username to login
username: root
# SSH port
port: 22
# SSH password
# Removed direct password input for security, using environment variable instead
with:
DEPLOY_PASSWORD: ${{ secrets.DEPLOY_PASSWORD }}
# Remote path where the files are going to be copied
remote-path: /home/zouklambadabcn.com/public_html
# PM2 ID or Name of the process
pm2-id: ZLB

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,40 +9,87 @@ 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">
<div className="bg-muted flex items-center justify-center overflow-hidden">
{member.image ? (
<img
src={member.image}
alt={member.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
className="w-full h-auto object-contain object-center group-hover:scale-105 transition-transform duration-500 bg-muted"
/>
) : (
<User className="w-16 h-16 text-muted-foreground/40" />
@@ -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

@@ -47,7 +47,7 @@
--radius: 0.75rem;
/* Custom tokens */
--gradient-tropical: linear-gradient(135deg, hsl(231 66% 37%), hsl(225 34% 48%), hsl(220 81% 75%));
--gradient-tropical: linear-gradient(135deg, hsla(28.2, 80.4%, 44.1%, 0.91), hsla(27.1, 88.4%, 37.3%, 0.76), hsl(22.3, 95.5%, 56.9%));
--gradient-warm: linear-gradient(180deg, hsl(225 30% 96%), hsl(225 25% 92%));
--shadow-glow: 0 0 40px hsl(231 66% 37% / 0.3);
--shadow-card: 0 8px 30px hsl(260 32% 24% / 0.08);

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,