diff --git a/package.json b/package.json index 3dac749..08e019a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/[lang]/events/page.tsx b/src/app/[lang]/events/page.tsx index a1b37a4..d13f104 100644 --- a/src/app/[lang]/events/page.tsx +++ b/src/app/[lang]/events/page.tsx @@ -1,3 +1,108 @@ -export default function Events() { - return
Events
; -} +"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([]) + + 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 ( + <> +
+
+

{dict.events.title}

+
+ + Calendarium + + + + + + Instagram + + + + +
+
+
+ +
+
+
+ +
+ +
+
+
+ +

+ {dict.events.warning.split("Calendarium")[0]} + + Calendarium + + {dict.events.warning.split("Calendarium")[1]?.split("Instagram")[0]} + + Instagram + + {dict.events.warning.split("Instagram")[1]} +

+
+
+
+ + ) +} \ No newline at end of file diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx new file mode 100644 index 0000000..9c408d7 --- /dev/null +++ b/src/components/calendar.tsx @@ -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(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 ( +
+
+
+ +

{currentDate.toLocaleString(useLang(), { month: "long" }).charAt(0).toUpperCase() + currentDate.toLocaleString(useLang(), { month: "long" }).slice(1)} {currentDate.getFullYear()}

+ +
+ +
+
+ {weekDays.map((day) => ( +
+ {day} +
+ ))} +
+
+ {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 ( + + ) + })} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/event-card.tsx b/src/components/event-card.tsx new file mode 100644 index 0000000..d96c258 --- /dev/null +++ b/src/components/event-card.tsx @@ -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 ( +
+
+
{month}
+
{day}
+
+
+

{event.title}

+
+ {time && ( +
+ calendar_month + {time} + {event.end && • {event.end.toLocaleDateString(lang)}} +
+ )} +
+ location_on + {event.place} +
+ {event.link && ( + + )} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/event-list.tsx b/src/components/event-list.tsx new file mode 100644 index 0000000..047a4bd --- /dev/null +++ b/src/components/event-list.tsx @@ -0,0 +1,12 @@ +import type { EventListProps } from "../lib/types" +import { EventCard } from "./event-card" + +export function EventList({ events }: EventListProps) { + return ( +
+ {events.map((event, index) => ( + + ))} +
+ ) +} \ No newline at end of file diff --git a/src/internationalization/dictionaries/en.json b/src/internationalization/dictionaries/en.json index b8c9288..dfe6181 100644 --- a/src/internationalization/dictionaries/en.json +++ b/src/internationalization/dictionaries/en.json @@ -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" }, diff --git a/src/internationalization/dictionaries/pt.json b/src/internationalization/dictionaries/pt.json index faaf5df..79d252e 100644 --- a/src/internationalization/dictionaries/pt.json +++ b/src/internationalization/dictionaries/pt.json @@ -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" }, diff --git a/src/lib/api/getEvents.ts b/src/lib/api/getEvents.ts new file mode 100644 index 0000000..a602b19 --- /dev/null +++ b/src/lib/api/getEvents.ts @@ -0,0 +1,14 @@ +import axios from 'axios'; +import { type Event } from "@/lib/types" + +const getEvents = async () => { + try { + const response = await axios.get('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; \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index 83831a3..e7150d9 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -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 +} \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 751eb15..a6b28f0 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -120,6 +120,74 @@ function classNames(...classes: string[]) { return classes.filter(Boolean).join(" "); } +function formatEventDate(date: Date): string { + return date.toLocaleDateString("pt-BR", { + day: "numeric", + month: "long", + year: "numeric", + }) +} + +function getMonthAbbreviation(date: Date, lang: string): string { + return date.toLocaleString(lang, { month: "short" }).replace('.', '').toUpperCase(); +} + +function getDay(date: Date): number { + return date.getDate() +} + +function getDaysInMonth(date: Date): Date[] { + const year = date.getFullYear() + const month = date.getMonth() + const daysInMonth = new Date(year, month + 1, 0).getDate() + + const days: Date[] = [] + for (let day = 1; day <= daysInMonth; day++) { + days.push(new Date(year, month, day)) + } + + return days +} + +function getMonthDays(date: Date): Date[] { + const year = date.getFullYear() + const month = date.getMonth() + const firstDay = new Date(year, month, 1) + const lastDay = new Date(year, month + 1, 0) + + const days: Date[] = [] + + const daysFromPrevMonth = firstDay.getDay() + for (let i = daysFromPrevMonth; i > 0; i--) { + days.push(new Date(year, month, -i + 1)) + } + + days.push(...getDaysInMonth(date)) + + const daysFromNextMonth = 7 - lastDay.getDay() - 1 + for (let i = 1; i <= daysFromNextMonth; i++) { + days.push(new Date(year, month + 1, i)) + } + + return days +} + +function isSameDay(date1: Date, date2: Date): boolean { + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ) +} + +function isWithinRange(date: Date, start: Date, end: Date): boolean { + const normalizedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()) + const normalizedStart = new Date(start.getFullYear(), start.getMonth(), start.getDate()) + const normalizedEnd = new Date(end.getFullYear(), end.getMonth(), end.getDate()) + + return normalizedDate >= normalizedStart && normalizedDate <= normalizedEnd +} + export { generateYearRanges, generateUrlsForTeams, @@ -128,4 +196,13 @@ export { getDepartmentByName, classNames, getDepartmentMembersInfo, + formatEventDate, + getMonthAbbreviation, + getDay, + getDaysInMonth, + getMonthDays, + isSameDay, + isWithinRange }; + +