Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: events page #70

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
"@formatjs/intl-localematcher": "^0.5.7",
"@headlessui/react": "^2.2.0",
"@types/negotiator": "^0.6.3",
"axios": "^1.7.9",
"motion": "^11.11.15",
"negotiator": "^1.0.0",
"next": "14.2.16",
"react": "^18",
"react-dom": "^18",
"react-intersection-observer": "^9.13.1"
"react-intersection-observer": "^9.13.1",
"react-swipeable": "^7.0.2"
},
"devDependencies": {
"@types/eslint": "^8.56.6",
Expand Down
111 changes: 108 additions & 3 deletions src/app/[lang]/events/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,108 @@
export default function Events() {
return <main>Events</main>;
}
"use client"

import { Calendar } from "@/components/calendar"
import { EventList } from "@/components/event-list"
import PromotionalCard from "@/components/promotional-card"
import { useDictionary } from "@/contexts/dictionary-provider"
import getEvents from "@/lib/api/getEvents"
import { type Event, CardType } from "@/lib/types"
import Link from "next/link"

import { useEffect, useState } from "react"

export default function EventsPage() {
const dict = useDictionary()

/*
const [events, setEvents] = useState<Event[]>([])

useEffect(() => {
async function fetchEvents() {
const eventsData = await getEvents()
setEvents(eventsData)
}
fetchEvents().catch(error => console.error('Failed to fetch events:', error))
}, [])
*/

const events: Event[] = [
{
title: "[AL] Teste",
place: "CP2 - 0.11 + 0.20 + 1.03 + 1.05 + 1.07",
link: "https://instagram.com/cesiuminho",
start: new Date("2025-10-31T00:00:00"),
end: new Date("2025-10-31T00:00:00"),
},
{
title: "[AL] Teste",
place: "CP2 - 0.11 + 0.20 + 1.03 + 1.05 + 1.07",
link: "https://instagram.com/cesiuminho",
start: new Date("2025-01-04T00:00:00"),
end: new Date("2025-01-04T00:00:00"),
},
{
title: "[AL] Recurso",
place: "CP2 - 0.11 + 0.20 + 1.03 + 1.05 + 1.07",
link: "https://instagram.com/cesiuminho",
start: new Date("2025-01-25T09:00:00"),
end: new Date("2025-01-25T12:00:00"),
},
{
title: "[AL] Época Especial",
place: "CP2 - 0.11 + 0.20 + 1.03 + 1.05 + 1.07",
link: "https://instagram.com/cesiuminho",
start: new Date("2025-07-19T09:00:00"),
end: new Date("2025-07-19T12:00:00"),
}
];


return (
<>
<div className="md:px-5">
<div className="flex items-center justify-between py-8">
<h1 className="text-3xl font-medium font-title">{dict.events.title}</h1>
<div className="hidden md:flex items-center gap-4">
<Link href="https://calendario.cesium.di.uminho.pt/" className="text-primary hover:underline flex items-center gap-1">
Calendarium
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" className="text-primary">
<path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" strokeWidth="2" />
</svg>
</Link>
<Link href="https://instagram.com/cesiuminho" className="text-primary hover:underline flex items-center gap-1">
Instagram
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" className="text-primary">
<path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" strokeWidth="2" />
</svg>
</Link>
</div>
</div>
</div>

<div className="md:px-5">
<div className="md:flex md:gap-12">
<div className="w-full md:w-2/5 mb-8 md:mb-0">
<Calendar events={events} />
<div className="mt-4">
<PromotionalCard type={CardType.Membership}/>
</div>
</div>
<div className="flex-1">
<EventList events={events} />
<p className="mt-8 text-sm text-black/50">
{dict.events.warning.split("Calendarium")[0]}
<Link href="https://calendario.cesium.di.uminho.pt/" className="text-primary hover:underline">
Calendarium
</Link>
{dict.events.warning.split("Calendarium")[1]?.split("Instagram")[0]}
<Link href="https://instagram.com/cesiuminho" className="text-primary hover:underline">
Instagram
</Link>
{dict.events.warning.split("Instagram")[1]}
</p>
</div>
</div>
</div>
</>
)
}
91 changes: 91 additions & 0 deletions src/components/calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"use client"

import { use, useState } from "react"
import { useSwipeable } from "react-swipeable"
import type { CalendarProps, Event } from "../lib/types"
import { getMonthDays, isSameDay, isWithinRange } from "../lib/utils"
import { useDictionary, useLang } from "@/contexts/dictionary-provider"

export function Calendar({ events = [], onDateSelect, onEventClick, className }: CalendarProps) {
const [currentDate, setCurrentDate] = useState(new Date())
const [selectedDate, setSelectedDate] = useState<Date | null>(null)

const handlers = useSwipeable({
onSwipedLeft: () => handleNextMonth(),
onSwipedRight: () => handlePreviousMonth(),
preventScrollOnSwipe: true,
trackMouse: true,
})

const handlePreviousMonth = () => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1))
}

const handleNextMonth = () => {
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1))
}

const handleDateSelect = (date: Date) => {
setSelectedDate(date)
onDateSelect?.(date)
}

const dict = useDictionary();
const days = getMonthDays(currentDate)
const weekDays = dict.events.weekDays.split(", ")

return (
<div className={`w-full ${className}`}>
<div {...handlers} className="select-none">
<div className="flex items-center justify-center pb-4 space-x-3">
<button onClick={handlePreviousMonth} className="text-primary hover:bg-gray-50">
<span className="material-symbols-outlined text-3xl">arrow_back</span>
</button>
<h2 className="text-2xl font-title font-medium">{currentDate.toLocaleString(useLang(), { month: "long" }).charAt(0).toUpperCase() + currentDate.toLocaleString(useLang(), { month: "long" }).slice(1)} {currentDate.getFullYear()}</h2>
<button onClick={handleNextMonth} className="text-primary hover:bg-gray-50">
<span className="material-symbols-outlined text-3xl">arrow_forward</span>
</button>
</div>

<div>
<div className="grid grid-cols-7 mb-2">
{weekDays.map((day) => (
<div key={day} className="text-center text-sm text-gray-500">
{day}
</div>
))}
</div>
<div className="grid grid-cols-7 gap-y-2">
{days.map((date, index) => {
const isCurrentMonth = date.getMonth() === currentDate.getMonth()
const hasEvent = events.some((event: Event) => {
const eventDate = new Date(event.start)
if (event.end) {
return isWithinRange(date, eventDate, new Date(event.end))
}
return isSameDay(eventDate, date)
})

return (
<button
key={index}
onClick={() => handleDateSelect(date)}
className="relative flex flex-col items-center"
>
<span
className={`text-center w-10 h-10 flex items-center justify-center rounded-full
${!isCurrentMonth ? "text-gray-400" : ""}
hover:bg-gray-50`}
>
{date.getDate()}
</span>
{hasEvent && <span className="absolute bottom-0.5 w-1.5 h-1.5 rounded-full bg-primary" />}
</button>
)
})}
</div>
</div>
</div>
</div>
)
}
45 changes: 45 additions & 0 deletions src/components/event-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client"

import type { EventCardProps } from "../lib/types"
import { getMonthAbbreviation, getDay } from "../lib/utils"
import { useLang } from "@/contexts/dictionary-provider"

export function EventCard({ event }: EventCardProps) {
const lang = useLang()
const month = getMonthAbbreviation(event.start, lang)
const day = getDay(event.start)
const time = new Date(event.start).toLocaleTimeString(lang, { hour: '2-digit', minute: '2-digit' })

return (
<div className="flex gap-4 items-start p-4 border-b border-black/20">
<div className="bg-black bg-opacity-[6%] rounded-xl p-2 text-center w-[4.5rem]">
<div className="text-primary text-sm font-medium">{month}</div>
<div className="text-2xl font-bold">{day}</div>
</div>
<div className="flex-1">
<h3 className="text-xl font-bold mb-4">{event.title}</h3>
<div className="space-y-2 text-base text-gray-600">
{time && (
<div className="flex items-center gap-2">
<span className="material-symbols-outlined">calendar_month</span>
{time}
{event.end && <span>• {event.end.toLocaleDateString(lang)}</span>}
</div>
)}
<div className="flex items-center gap-2">
<span className="material-symbols-outlined">location_on</span>
{event.place}
</div>
{event.link && (
<div className="flex items-center gap-2 text-primary w-[80%]">
<span className="material-symbols-outlined">link</span>
<a href={event.link} target="_blank" rel="noopener noreferrer" className="hover:underline truncate">
{event.link.split("://")[1]}
</a>
</div>
)}
</div>
</div>
</div>
)
}
12 changes: 12 additions & 0 deletions src/components/event-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { EventListProps } from "../lib/types"
import { EventCard } from "./event-card"

export function EventList({ events }: EventListProps) {
return (
<div className="space-y-6">
{events.map((event, index) => (
<EventCard key={index} event={event} />
))}
</div>
)
}
5 changes: 5 additions & 0 deletions src/internationalization/dictionaries/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
"description": "The departments at CeSIUM handle various aspects of the organization, from event planning and partnership management to communication and software development."
}
},
"events": {
"title": "Events",
"warning": "For now, the list is over. This list only shows events organized by CeSIUM. To see more events, such as parties or holidays, visit the Calendarium. Stay updated on our Instagram.",
"weekDays": "Sun, Mon, Tue, Wed, Thu, Fri, Sat"
},
"social_media": "Social media",
"socials": [
{ "name": "Facebook", "url": "https://www.facebook.com/cesiuminho" },
Expand Down
5 changes: 5 additions & 0 deletions src/internationalization/dictionaries/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
"description": "Os departamentos do CeSIUM são responsáveis por diferentes áreas de atuação do núcleo, desde a organização de eventos, à gestão de parcerias, passando pela comunicação e pelo desenvolvimento de software."
}
},
"events": {
"title": "Eventos",
"warning": "Por agora a lista acabou. Esta lista só mostra eventos organizados pelo CeSIUM. Para veres mais eventos, como festas ou feriados, visita o Calendarium. Mantém-te a par das novidades no nosso Instagram.",
"weekDays": "Dom, Seg, Ter, Qua, Qui, Sex, Sab"
},
"social_media": "Redes sociais",
"socials": [
{ "name": "Facebook", "url": "https://www.facebook.com/cesiuminho" },
Expand Down
14 changes: 14 additions & 0 deletions src/lib/api/getEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import axios from 'axios';
import { type Event } from "@/lib/types"

const getEvents = async () => {
try {
const response = await axios.get<Event[]>('https://calendario.cesium.di.uminho.pt/api/transfer/events');
return Array.isArray(response.data) ? response.data : [response.data];
} catch (error) {
console.error('Error fetching events:', error);
throw error;
}
};

export default getEvents;
27 changes: 27 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,30 @@ export type TeamData = Team[];
export interface MemberInfo extends Member {
imageUrl: string;
}

export interface Event {
title: string,
place?: string,
link?: string,
start: Date,
end: Date
}

export interface EventsPageProps {
events: Event[]
}

export interface EventCardProps {
event: Event
}

export interface EventListProps {
events: Event[]
}

export interface CalendarProps {
events: Event[]
onDateSelect?: (date: Date) => void
onEventClick?: (event: Event) => void
className?: string
}
Loading