Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,23 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
// Mock heavy sections to avoid side effects and async fetches during App render
jest.mock('./components', () => {
const React = require('react');
return {
MinimalFooter: () => React.createElement('div'),
Navigation: () => React.createElement('div'),
HeroSection: () => React.createElement('div'),
CTASection: () => React.createElement('div'),
CardsSection: () => React.createElement('div'),
RiverSection: () => React.createElement('div'),
NextEventsSection: () => React.createElement('h2', null, 'Próximos eventos'),
PastEventsSection: () => React.createElement('div'),
};
});

test('renders app shell with Upcoming events heading', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
const heading = screen.getByText(/Próximos eventos/i);
expect(heading).toBeInTheDocument();
});
36 changes: 20 additions & 16 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import '@primer/react-brand/lib/css/main.css'
import '@primer/react-brand/fonts/fonts.css'
import { ThemeProvider } from '@primer/react-brand';
import { MinimalFooter, Navigation, TimelineSection, HeroSection, CTASection, CardsSection, RiverSection } from './components';
import { MinimalFooter, Navigation, PastEventsSection, HeroSection, CTASection, CardsSection, RiverSection, NextEventsSection } from './components';

const designTokenOverrides = `
/* Map Primer brand tokens to Design System tokens */
.custom-colors[data-color-mode='dark'] {
/*
* Modify the value of these tokens.
* Remember to apply light mode equivalents if you're enabling theme switching.
*/
--brand-CTABanner-shadow-color-start: var(--base-color-scale-purple-5);
--brand-CTABanner-shadow-color-end: var(--base-color-scale-pink-5);
--brand-color-canvas-default: var(--ds-bg-default);
--brand-color-canvas-subtle: var(--ds-bg-subtle);
--brand-color-text-default: var(--ds-text-default);
--brand-color-text-muted: var(--ds-text-muted);
--brand-CTABanner-shadow-color-start: var(--ds-accent-purple);
--brand-CTABanner-shadow-color-end: #d946ef;
}

.custom-colors[data-color-mode='light'] {
/*
* Modify the value of these tokens.
* Remember to apply light mode equivalents if you're enabling theme switching.
*/
--brand-CTABanner-shadow-color-start: var(--base-color-scale-purple-5);
--brand-CTABanner-shadow-color-end: var(--base-color-scale-pink-5);
--brand-color-canvas-default: var(--ds-bg-default);
--brand-color-canvas-subtle: var(--ds-bg-subtle);
--brand-color-text-default: var(--ds-text-default);
--brand-color-text-muted: var(--ds-text-muted);
--brand-CTABanner-shadow-color-start: var(--ds-accent-purple);
--brand-CTABanner-shadow-color-end: #d946ef;
}
`

Expand All @@ -31,14 +32,17 @@ function App() {
position: 'relative',
width: '100%',
minHeight: '100vh',
backgroundColor: 'var(--brand-color-canvas-default)',
color: 'var(--brand-color-text-default)'
backgroundColor: 'var(--ds-bg-default)',
color: 'var(--ds-text-default)'
}}>
<Navigation />
<HeroSection />
{/* Upcoming events highlight */}
<NextEventsSection />
<CardsSection />
<CTASection />
<TimelineSection />
{/* Past events list (rendered with Timeline subcomponent) */}
<PastEventsSection />
<RiverSection />
<MinimalFooter socialLinks={["github", "linkedin", "youtube", "x", "meetup"]} />
</div>
Expand Down
14 changes: 7 additions & 7 deletions src/components/CardsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { Section, Stack, Animate, SectionIntro } from '@primer/react-brand';
import { Card } from './subcomponents/Card';
import { Card, CardCopy } from './subcomponents/Card';

const CardsSection: React.FC = () => {
return (
Expand All @@ -22,22 +22,22 @@ const CardsSection: React.FC = () => {

>
<Animate animate="scale-in-up">
<Card href="https://www.meetup.com/ghspain" hasBorder ctaText="Saber más" fullWidth align="center">
<CardCopy href="https://www.meetup.com/ghspain" hasBorder fullWidth align="center">
<Card.Heading>Altruista y generosa</Card.Heading>
<Card.Description>Nadie en la organización obtiene algún tipo de beneficio monetario de la misma. Queremos aportar valor al conjunto de la sociedad y al sector tecnológico con nuestros proyectos, nuestras ideas y nuestras experiencias.</Card.Description>
</Card>
</CardCopy>
</Animate>
<Animate animate="scale-in-up">
<Card href="https://www.meetup.com/ghspain" hasBorder ctaText="Saber más" fullWidth align="center">
<CardCopy href="https://www.meetup.com/ghspain" hasBorder fullWidth align="center">
<Card.Heading>Abierta y participativa</Card.Heading>
<Card.Description>Cualquier persona que quiera colaborar y ayudar a dinamizar nuestra comunidad es bienvenida. Queremos expandir nuestra red por diferentes lugares y ciudades de España, para llegar a más personas y crear más oportunidades.</Card.Description>
</Card>
</CardCopy>
</Animate>
<Animate animate="scale-in-up">
<Card href="https://www.meetup.com/ghspain" hasBorder ctaText="Saber más" fullWidth align="center">
<CardCopy href="https://www.meetup.com/ghspain" hasBorder fullWidth align="center">
<Card.Heading>Basada en Github</Card.Heading>
<Card.Description>GitHub nos permite gestionar nuestros proyectos, documentar nuestro trabajo y compartir nuestro código con otros desarrolladores. Reconocemos el valor de GitHub como herramienta, como filosofía y como un pilar fundamental para el desarrollo.</Card.Description>
</Card>
</CardCopy>
</Animate>
</Stack>
</Section>
Expand Down
71 changes: 71 additions & 0 deletions src/components/NextEventsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { useMemo } from 'react';
import { Section, SectionIntro, Stack, AnimationProvider, Animate } from '@primer/react-brand'
import EventCard from './subcomponents/EventCard';
import eventStyles from './css/EventSection.module.css'
import { filterAndSortEvents } from '../utils/eventFilters'
import { useEvents } from '../hooks/useEvents'

const NextEventsSection: React.FC = () => {
const { events, loading, error } = useEvents();
const { upcomingEvents } = useMemo(() => filterAndSortEvents(events), [events]);

if (loading) {
return (
<Section>
<SectionIntro align="center">
<SectionIntro.Heading size="2">Próximos eventos</SectionIntro.Heading>
</SectionIntro>
<Stack padding="spacious" alignItems="center" gap="spacious">
Cargando eventos...
</Stack>
</Section>
);
}

if (error) {
return (
<Section>
<SectionIntro align="center">
<SectionIntro.Heading size="2">Próximos eventos</SectionIntro.Heading>
</SectionIntro>
<Stack padding="spacious" alignItems="center" gap="spacious">
Error cargando eventos: {error}
</Stack>
</Section>
);
}

if (upcomingEvents.length === 0) {
return (
<Section>
<SectionIntro align="center">
<SectionIntro.Heading size="2">Próximos eventos</SectionIntro.Heading>
</SectionIntro>
<Stack padding="spacious" alignItems="center" gap="spacious">
No hay eventos próximos ahora mismo. ¡Vuelve pronto!
</Stack>
</Section>
);
}

return (
<AnimationProvider>
<Section paddingBlockEnd="none">
<SectionIntro align="center">
<SectionIntro.Heading size="2">Próximos eventos</SectionIntro.Heading>
</SectionIntro>
<div className={eventStyles.upcomingContainer}>
<Stack direction="vertical" padding="spacious" alignItems="center" justifyContent="center" gap="normal">
{upcomingEvents.map((event) => (
<Animate key={event.event_id} animate="scale-in-up">
<EventCard event={event} />
</Animate>
))}
</Stack>
</div>
</Section>
</AnimationProvider>
);
};

export default NextEventsSection;
113 changes: 113 additions & 0 deletions src/components/PastEventsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useState, useEffect } from 'react';
import { Timeline, Section, Stack, SectionIntro, AnimationProvider } from '@primer/react-brand'
import eventStyles from './css/EventSection.module.css'
import { EVENT_CONFIG } from '../utils/events.constants'
import { EventData, filterAndSortEvents } from '../utils/eventFilters'
import ListMessage from './subcomponents/ListMessage'
import PastEventsItem from './subcomponents/PastEventsItem'

const PastEventsSection: React.FC = () => {
const [events, setEvents] = useState<EventData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const loadEvents = async () => {
try {
// Usar la misma URL tanto en desarrollo como en producción
const jsonUrl = `${process.env.PUBLIC_URL}/data/issues.json`;

console.log('Loading events from:', jsonUrl); // Debug log

const response = await fetch(jsonUrl);

if (!response.ok) {
throw new Error(`Error loading events: ${response.status}`);
}

const eventsData: EventData[] = await response.json();

console.log('Loaded events:', eventsData.length); // Debug log

// Don't sort here; we'll sort after filtering
setEvents(eventsData);
// Upcoming events are handled in EventsSection; image sizing measurement removed.
} catch (err) {
console.error('Error loading events:', err);
setError(err instanceof Error ? err.message : 'Error desconocido');
} finally {
setLoading(false);
}
};

loadEvents();
}, []);


if (loading) {
return (
<Section>
<SectionIntro align="center">
<SectionIntro.Heading size="1">Eventos</SectionIntro.Heading>
</SectionIntro>
<ListMessage message="Cargando eventos..." fullWidth={false} />
</Section>
);
}

if (error) {
return (
<Section>
<SectionIntro align="center">
<SectionIntro.Heading size="1">Eventos</SectionIntro.Heading>
</SectionIntro>
<ListMessage message={`Error cargando eventos: ${error}`} fullWidth={true} />
</Section>
);
}

if (events.length === 0) {
return (
<Section>
<SectionIntro align="center">
<SectionIntro.Heading size="1">Eventos</SectionIntro.Heading>
</SectionIntro>
<ListMessage message="No hay eventos disponibles" fullWidth={true} />
</Section>
);
}

return (
<AnimationProvider>
<Section paddingBlockEnd="none">
<SectionIntro align="center">
<SectionIntro.Heading size="2">Eventos pasados</SectionIntro.Heading>
</SectionIntro>
<Stack padding="spacious" alignItems="center" gap="spacious">
{(() => {
const { pastEvents } = filterAndSortEvents(events);

if (pastEvents.length === 0) return null;

return (
<div className={eventStyles.pastEventsContainer}>
<h3 className={eventStyles.sectionTitle}>
{EVENT_CONFIG.PAST_TITLE}
</h3>
<div className={eventStyles.timelineWrapper}>
<Timeline fullWidth={false}>
{pastEvents.map((event) => (
<PastEventsItem key={event.event_id} event={event} openInNewTab />
))}
</Timeline>
</div>
</div>
);
})()}
</Stack>
</Section>
</AnimationProvider>
);
};

export default PastEventsSection;
Loading