From a276fcc96397280122dac30b8bacba48c0f38727 Mon Sep 17 00:00:00 2001 From: matthias-luger Date: Sat, 9 May 2026 03:27:56 +0200 Subject: [PATCH 1/4] Add /mayor/[year] and /mayor/all pages to display mayor data --- app/mayor/[year]/page.tsx | 28 ++++ app/mayor/all/page.tsx | 22 +++ components/Mayor/AllMayors.tsx | 33 +++++ components/Mayor/MayorDetails.module.css | 105 +++++++++++++++ components/Mayor/MayorDetails.tsx | 107 +++++++++++++++ components/Mayor/MayorDetailsDisplay.tsx | 162 +++++++++++++++++++++++ components/Mayor/YearlyMayor.tsx | 28 ++++ 7 files changed, 485 insertions(+) create mode 100644 app/mayor/[year]/page.tsx create mode 100644 app/mayor/all/page.tsx create mode 100644 components/Mayor/AllMayors.tsx create mode 100644 components/Mayor/MayorDetails.module.css create mode 100644 components/Mayor/MayorDetails.tsx create mode 100644 components/Mayor/MayorDetailsDisplay.tsx create mode 100644 components/Mayor/YearlyMayor.tsx diff --git a/app/mayor/[year]/page.tsx b/app/mayor/[year]/page.tsx new file mode 100644 index 00000000..f0dce296 --- /dev/null +++ b/app/mayor/[year]/page.tsx @@ -0,0 +1,28 @@ +import { Metadata } from "next"; +import { YearlyMayor } from "../../../components/Mayor/YearlyMayor"; +import { Container } from "react-bootstrap"; +import NavBar from "../../../components/NavBar/NavBar"; +import Search from "../../../components/Search/Search"; + +export const metadata: Metadata = { + title: "Mayor Flips", + description: "A list of items that are affected by the current or upcoming mayor.", +}; + +type Props = { + params: { + year: string; + } +} + +export default function MayorPage({ params }: Props) { + return ( + + + +

Mayor Data for Year {params.year}

+
+ +
+ ); +} diff --git a/app/mayor/all/page.tsx b/app/mayor/all/page.tsx new file mode 100644 index 00000000..7c8d36bb --- /dev/null +++ b/app/mayor/all/page.tsx @@ -0,0 +1,22 @@ +import { Metadata } from "next"; +import { AllMayors } from "../../../components/Mayor/AllMayors"; +import { Container } from "react-bootstrap"; +import NavBar from "../../../components/NavBar/NavBar"; +import Search from "../../../components/Search/Search"; + +export const metadata: Metadata = { + title: "All Mayor Flips", + description: "A list of all mayor flips from the last 5 years.", +}; + +export default function AllMayorPage() { + return ( + + + +

All Mayors

+
+ +
+ ); +} diff --git a/components/Mayor/AllMayors.tsx b/components/Mayor/AllMayors.tsx new file mode 100644 index 00000000..dc6b5c07 --- /dev/null +++ b/components/Mayor/AllMayors.tsx @@ -0,0 +1,33 @@ +"use client"; +import { useMemo } from "react"; +import { useGetApiMayor } from "../../api/_generated/skyApi"; +import { MayorDetailsDisplay } from "./MayorDetailsDisplay"; + +const FIVE_YEARS_IN_MS = 5 * 365 * 24 * 60 * 60 * 1000; +const NOW = new Date() +const FIVE_YEARS_AGO = new Date(Date.now() - FIVE_YEARS_IN_MS); +NOW.setHours(0, 0, 0, 0); +FIVE_YEARS_AGO.setHours(0, 0, 0, 0); + +export function AllMayors() { + const { from, to } = useMemo(() => { + return { + from: FIVE_YEARS_AGO.toISOString(), + to: NOW.toISOString() + }; + }, []); + + const { data: mayorData, isLoading, error } = useGetApiMayor({ from, to }); + + if (isLoading) { + return
Loading...
; + } + + if (error) { + return
Error loading mayor data
; + } + + const elections = mayorData?.data ? (Array.isArray(mayorData.data) ? mayorData.data : [mayorData.data]) : []; + + return ; +} \ No newline at end of file diff --git a/components/Mayor/MayorDetails.module.css b/components/Mayor/MayorDetails.module.css new file mode 100644 index 00000000..bb488001 --- /dev/null +++ b/components/Mayor/MayorDetails.module.css @@ -0,0 +1,105 @@ +.container { + padding: 2rem; + max-width: 1200px; + margin: 2rem auto; + background-color: #1a1a1a; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + color: #f0f0f0; +} + +.container h1 { + text-align: center; + color: #00aaff; + margin-bottom: 1rem; +} + +.container h2 { + text-align: center; + color: #00aaff; + margin-bottom: 2rem; +} + +.table { + width: 100%; + border-collapse: collapse; + margin-top: 2rem; +} + +.table th, .table td { + border: 1px solid #333; + padding: 12px 15px; + text-align: left; +} + +.table th { + background-color: #003366; + color: #ffffff; + font-weight: bold; + text-transform: uppercase; +} + +.table tr:nth-child(even) { + background-color: #2a2a2a; +} + +.table tr:hover { + background-color: #3a3a3a; +} + +.yearLinks { + display: flex; + justify-content: center; + gap: 1rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.yearLinks a { + text-decoration: none; + color: #f0f0f0; + background-color: #0055a4; + padding: 0.5rem 1rem; + border-radius: 5px; + transition: background-color 0.3s ease; +} + +.yearLinks a:hover { + background-color: #0077cc; + text-decoration: none; +} + +.customAccordion { + --bs-accordion-bg: #1a1a1a; + --bs-accordion-color: #f0f0f0; + --bs-accordion-border-color: #333; + --bs-accordion-active-bg: #222; + --bs-accordion-active-color: #00aaff; + --bs-accordion-btn-focus-box-shadow: none; +} + +.customAccordion :global(.accordion-button) { + font-weight: 500; +} + +.customAccordion :global(.accordion-button:not(.collapsed)::after) { + filter: invert(1); +} + +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .table th, .table td { + padding: 8px; + } + + .yearLinks { + gap: 0.5rem; + } + + .yearLinks a { + padding: 0.4rem 0.8rem; + } +} diff --git a/components/Mayor/MayorDetails.tsx b/components/Mayor/MayorDetails.tsx new file mode 100644 index 00000000..78eef8e9 --- /dev/null +++ b/components/Mayor/MayorDetails.tsx @@ -0,0 +1,107 @@ +"use client"; +import { useGetApiMayor, useGetApiMayorYear } from "../../api/_generated/skyApi"; +import styles from "./MayorDetails.module.css"; +import Link from "next/link"; +import { Accordion, Card, Col, Container, Row } from "react-bootstrap"; +import { CoflnetSkyMayorModelsModelElectionPeriod } from "../../api/_generated/skyApi.schemas"; + +type Props = { + year?: string; +} + +function MayorElectionPeriod({ election }: { election: CoflnetSkyMayorModelsModelElectionPeriod }) { + return ( + + + + Election in year {election.year} + + + + + + +
Candidates
+
    + {election.candidates?.map(candidate => ( +
  • {candidate.name}
  • + ))} +
+ + +
Winner
+ {election.winner ? ( +
+

Name: {election.winner.name}

+

Key: {election.winner.key}

+
Perks:
+
    + {election.winner.perks?.map(perk => ( +
  • {perk.name}: {perk.description}
  • + ))} +
+
+ ) :

No winner declared.

} + +
+
+
+
+ ) +} + + +export default function MayorDetails({ year }: Props) { + const { data: mayorData, isLoading: isMayorLoading, error: mayorError } = useGetApiMayor(undefined, + { + query: { + enabled: !year, + }, + } + ); + + const { data: mayorYearData, isLoading: isMayorYearLoading, error: mayorYearError } = useGetApiMayorYear( + parseInt(year || new Date().getFullYear().toString(), 10), + { + query: { + enabled: !!year, + }, + } + ); + + const isLoading = isMayorLoading || isMayorYearLoading; + const error = mayorError || mayorYearError; + const data = year ? mayorYearData : mayorData; + + if (isLoading) { + return
Loading...
; + } + + if (error) { + return
Error loading mayor data
; + } + + const elections = data?.data ? (Array.isArray(data.data) ? data.data : [data.data]) : []; + const years = Array.from(new Set(elections.map(e => e.year))); + + return ( + +

Mayor Details

+ {year &&

Year: {year}

} + +
+ {years.map(y => ( + + {y} + + ))} +
+ + + {elections.map((election, index) => ( + + ))} + +
+ ); +} diff --git a/components/Mayor/MayorDetailsDisplay.tsx b/components/Mayor/MayorDetailsDisplay.tsx new file mode 100644 index 00000000..f3b65142 --- /dev/null +++ b/components/Mayor/MayorDetailsDisplay.tsx @@ -0,0 +1,162 @@ +"use client"; +import { useState, useRef, useMemo } from "react"; +import styles from "./MayorDetails.module.css"; +import { Accordion, Card, Col, Row, ListGroup, Badge, Form, Button, InputGroup } from "react-bootstrap"; +import { CoflnetSkyMayorModelsModelElectionPeriod } from "../../api/_generated/skyApi.schemas"; +import { getMinecraftColorCodedElement } from "../../utils/Formatter"; + +type MayorElectionPeriodProps = { + election: CoflnetSkyMayorModelsModelElectionPeriod; + isSingleYear?: boolean; +} + +function MayorElectionPeriod({ election, isSingleYear }: MayorElectionPeriodProps) { + const initialCandidateKey = election.winner?.key ?? election.candidates?.[0]?.key ?? null; + const [selectedCandidateKey, setSelectedCandidateKey] = useState(initialCandidateKey); + + const selectedCandidate = election.candidates?.find(c => c.key === selectedCandidateKey) ?? election.winner; + + const renderContent = () => ( +
+ + +
Candidates
+ + {election.candidates?.map(candidate => ( + setSelectedCandidateKey(candidate.key ?? null)} + className={`px-3 py-2 mb-2 rounded border ${selectedCandidateKey === candidate.key ? 'bg-primary text-white border-primary shadow' : 'bg-transparent text-light border-secondary opacity-75'}`} + style={{ cursor: 'pointer', transition: 'all 0.2s ease' }} + > +
+ {candidate.name} + {election.winner?.key === candidate.key && ( + Winner + )} +
+
+ ))} +
+ + +
+ {selectedCandidateKey === election.winner?.key ? "Election Winner" : "Candidate Details"} +
+ {selectedCandidate ? ( +
+

{selectedCandidate.name}

+ +
Mayor Perks
+ + {selectedCandidate.perks?.map(perk => ( + + + + {perk.name} + + {getMinecraftColorCodedElement(perk.description ?? '')} + + + + + ))} + +
+ ) : ( +
+ No candidate selected or data is missing. +
+ )} + +
+
+ ); + + if (isSingleYear) { + return ( + + + {renderContent()} + + + ); + } + + return ( + + + Election in year {election.year} + + + {renderContent()} + + + ) +} + +type Props = { + elections: CoflnetSkyMayorModelsModelElectionPeriod[]; + isSingleYear?: boolean; + year?: string; +} + +export function MayorDetailsDisplay({ elections, isSingleYear, year }: Props) { + const defaultKey = elections.length > 0 ? elections[0].year.toString() : "0"; + const [activeKey, setActiveKey] = useState(defaultKey); + const searchInputRef = useRef(null); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + const searchYear = searchInputRef.current?.value; + if (searchYear) { + setActiveKey(searchYear); + const el = document.getElementById(`year-item-${searchYear}`); + if (el) { + const yOffset = -20; + const y = el.getBoundingClientRect().top + window.scrollY + yOffset; + window.scrollTo({ top: y, behavior: 'smooth' }); + } + } + }; + + const accordionItems = useMemo(() => { + return elections.map((election) => ( + + )); + }, [elections]); + + return ( +
+ {!isSingleYear && ( +
+
+ Jump to Year: + + + + +
+
+ )} + + {isSingleYear ? ( +
+ {elections.map((election) => ( + + ))} +
+ ) : ( + setActiveKey(k || "")} className={styles.customAccordion}> + {accordionItems} + + )} +
+ ); +} \ No newline at end of file diff --git a/components/Mayor/YearlyMayor.tsx b/components/Mayor/YearlyMayor.tsx new file mode 100644 index 00000000..86356aba --- /dev/null +++ b/components/Mayor/YearlyMayor.tsx @@ -0,0 +1,28 @@ +"use client"; +import { useGetApiMayorYear } from "../../api/_generated/skyApi"; +import { MayorDetailsDisplay } from "./MayorDetailsDisplay"; + +type Props = { + year: string; +}; + +export function YearlyMayor({ year }: Props) { + const yearInt = parseInt(year, 10); + const { data: mayorYearData, isLoading, error } = useGetApiMayorYear(yearInt, { + query: { + enabled: !isNaN(yearInt), + }, + }); + + if (isLoading) { + return
Loading...
; + } + + if (error) { + return
Error loading mayor data for year {year}
; + } + + const elections = mayorYearData?.data ? (Array.isArray(mayorYearData.data) ? mayorYearData.data : [mayorYearData.data]) : []; + + return ; +} \ No newline at end of file From 5d6c0e317574ee80f3f6f7641047e5a3e551da0f Mon Sep 17 00:00:00 2001 From: matthias-luger Date: Sat, 9 May 2026 03:52:02 +0200 Subject: [PATCH 2/4] fix build issue --- app/mayor/[year]/page.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/mayor/[year]/page.tsx b/app/mayor/[year]/page.tsx index f0dce296..3350ec73 100644 --- a/app/mayor/[year]/page.tsx +++ b/app/mayor/[year]/page.tsx @@ -9,20 +9,15 @@ export const metadata: Metadata = { description: "A list of items that are affected by the current or upcoming mayor.", }; -type Props = { - params: { - year: string; - } -} - -export default function MayorPage({ params }: Props) { +export default async function MayorPage({ params }: { params: { year: string } }) { + const year = params.year; return ( -

Mayor Data for Year {params.year}

+

Mayor Data for Year {year}


- +
); } From 6227d27d827826aa5b090da2641d0fd6e94ef5fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 02:00:06 +0000 Subject: [PATCH 3/4] fix invalid mayor year state Agent-Logs-Url: https://github.com/Coflnet/hypixel-react/sessions/c31a847d-2c22-475c-9018-4e6bfe4e5f46 Co-authored-by: matthias-luger <58751503+matthias-luger@users.noreply.github.com> --- app/mayor/[year]/page.tsx | 4 +-- components/Mayor/YearlyMayor.tsx | 52 ++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/app/mayor/[year]/page.tsx b/app/mayor/[year]/page.tsx index 3350ec73..7b185431 100644 --- a/app/mayor/[year]/page.tsx +++ b/app/mayor/[year]/page.tsx @@ -9,8 +9,8 @@ export const metadata: Metadata = { description: "A list of items that are affected by the current or upcoming mayor.", }; -export default async function MayorPage({ params }: { params: { year: string } }) { - const year = params.year; +export default async function MayorPage({ params }: { params: Promise<{ year: string }> }) { + const { year } = await params; return ( diff --git a/components/Mayor/YearlyMayor.tsx b/components/Mayor/YearlyMayor.tsx index 86356aba..07344238 100644 --- a/components/Mayor/YearlyMayor.tsx +++ b/components/Mayor/YearlyMayor.tsx @@ -1,28 +1,40 @@ -"use client"; -import { useGetApiMayorYear } from "../../api/_generated/skyApi"; -import { MayorDetailsDisplay } from "./MayorDetailsDisplay"; +'use client' +import { useGetApiMayorYear } from '../../api/_generated/skyApi' +import { MayorDetailsDisplay } from './MayorDetailsDisplay' type Props = { - year: string; -}; + year: string +} export function YearlyMayor({ year }: Props) { - const yearInt = parseInt(year, 10); - const { data: mayorYearData, isLoading, error } = useGetApiMayorYear(yearInt, { - query: { - enabled: !isNaN(yearInt), - }, - }); + const yearInt = Number.parseInt(year, 10) + const isValidYear = Number.isInteger(yearInt) + const { data: mayorYearData, isLoading, error } = useGetApiMayorYear( + isValidYear ? yearInt : 0, + { + query: { + enabled: isValidYear, + }, + }, + ) - if (isLoading) { - return
Loading...
; - } + if (!isValidYear) { + return
Invalid year
+ } - if (error) { - return
Error loading mayor data for year {year}
; - } + if (isLoading) { + return
Loading...
+ } - const elections = mayorYearData?.data ? (Array.isArray(mayorYearData.data) ? mayorYearData.data : [mayorYearData.data]) : []; + if (error) { + return
Error loading mayor data for year {year}
+ } - return ; -} \ No newline at end of file + const elections = mayorYearData?.data + ? Array.isArray(mayorYearData.data) + ? mayorYearData.data + : [mayorYearData.data] + : [] + + return +} From 5cdf327c1133012a2e1a10e7ac555c7d39629e62 Mon Sep 17 00:00:00 2001 From: matthias-luger Date: Sat, 9 May 2026 04:07:04 +0200 Subject: [PATCH 4/4] Revert "fix invalid mayor year state" This reverts commit 6227d27d827826aa5b090da2641d0fd6e94ef5fe. --- app/mayor/[year]/page.tsx | 4 +-- components/Mayor/YearlyMayor.tsx | 52 ++++++++++++-------------------- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/app/mayor/[year]/page.tsx b/app/mayor/[year]/page.tsx index 7b185431..3350ec73 100644 --- a/app/mayor/[year]/page.tsx +++ b/app/mayor/[year]/page.tsx @@ -9,8 +9,8 @@ export const metadata: Metadata = { description: "A list of items that are affected by the current or upcoming mayor.", }; -export default async function MayorPage({ params }: { params: Promise<{ year: string }> }) { - const { year } = await params; +export default async function MayorPage({ params }: { params: { year: string } }) { + const year = params.year; return ( diff --git a/components/Mayor/YearlyMayor.tsx b/components/Mayor/YearlyMayor.tsx index 07344238..86356aba 100644 --- a/components/Mayor/YearlyMayor.tsx +++ b/components/Mayor/YearlyMayor.tsx @@ -1,40 +1,28 @@ -'use client' -import { useGetApiMayorYear } from '../../api/_generated/skyApi' -import { MayorDetailsDisplay } from './MayorDetailsDisplay' +"use client"; +import { useGetApiMayorYear } from "../../api/_generated/skyApi"; +import { MayorDetailsDisplay } from "./MayorDetailsDisplay"; type Props = { - year: string -} + year: string; +}; export function YearlyMayor({ year }: Props) { - const yearInt = Number.parseInt(year, 10) - const isValidYear = Number.isInteger(yearInt) - const { data: mayorYearData, isLoading, error } = useGetApiMayorYear( - isValidYear ? yearInt : 0, - { - query: { - enabled: isValidYear, - }, - }, - ) + const yearInt = parseInt(year, 10); + const { data: mayorYearData, isLoading, error } = useGetApiMayorYear(yearInt, { + query: { + enabled: !isNaN(yearInt), + }, + }); - if (!isValidYear) { - return
Invalid year
- } + if (isLoading) { + return
Loading...
; + } - if (isLoading) { - return
Loading...
- } + if (error) { + return
Error loading mayor data for year {year}
; + } - if (error) { - return
Error loading mayor data for year {year}
- } + const elections = mayorYearData?.data ? (Array.isArray(mayorYearData.data) ? mayorYearData.data : [mayorYearData.data]) : []; - const elections = mayorYearData?.data - ? Array.isArray(mayorYearData.data) - ? mayorYearData.data - : [mayorYearData.data] - : [] - - return -} + return ; +} \ No newline at end of file