Skip to content

Commit abf015a

Browse files
authored
✨ feat: add resolution adoption feature with confetti display (#217)
Co-authored-by: Tade Strehk <git@strehk.eu>
1 parent 3ca0cb6 commit abf015a

File tree

10 files changed

+168
-37
lines changed

10 files changed

+168
-37
lines changed

bun.lockb

1.61 KB
Binary file not shown.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
"primereact": "^10.9.2",
3434
"quill": "^2.0.3",
3535
"react": "^19.0.0",
36+
"react-canvas-confetti": "^2.0.7",
3637
"react-dom": "^19.0.0",
38+
"react-fast-marquee": "^1.6.5",
3739
"react-flip-move": "^3.0.5",
3840
"react-responsive": "^10.0.0",
3941
"sass": "^1.85.0",

src/api/routes/committee.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,23 @@ export const committee = new Elysia({
221221
description: "Get all delegations of a committee",
222222
},
223223
},
224+
)
225+
.patch(
226+
"/committee/:committeeId/adoptResolution",
227+
async ({ params, permissions }) => {
228+
return await db.committee.update({
229+
where: {
230+
id: params.committeeId,
231+
AND: [permissions.allowDatabaseAccessTo("update").Committee],
232+
},
233+
data: {
234+
lastAdoptedResolution: new Date(),
235+
},
236+
});
237+
},
238+
{
239+
detail: {
240+
description: "Adopt a resolution in a committee",
241+
},
242+
},
224243
);

src/app/app/[conferenceId]/committee/[committeeId]/(presentation-mode)/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { StatusTimer } from "@/lib/contexts/status_timer";
1313
import { useClientSideBackendCallPoller } from "@/lib/backend/useClientSideBackendCall";
1414
import * as m from "@/paraglide/messages";
1515
import FAIcon from "@/lib/components/FAIcon";
16+
import ConfettiOnAdoption from "@/lib/components/confetti_on_adoption";
1617

1718
export default function CommitteePresentationMode({
1819
params,
@@ -143,6 +144,11 @@ export default function CommitteePresentationMode({
143144
onClick={() => setRemSize(remSize + 1)}
144145
/>
145146
</div>
147+
<ConfettiOnAdoption
148+
adoptionDate={committeeData?.lastAdoptedResolution}
149+
title={committeeData?.agendaItems.find((x) => x.isActive)?.title}
150+
committee={committeeData?.name}
151+
/>
146152
</>
147153
);
148154
}

src/app/app/[conferenceId]/committee/[committeeId]/chair/dashboard/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { useSpeakersListMiniature } from "@/lib/contexts/speakers_list_miniature";
2121
import Button from "@/lib/components/Button";
2222
import * as m from "@/paraglide/messages";
23+
import ResolutionAdoption from "@/lib/components/dashboard/resolution_adoption";
2324

2425
export default function ChairDashboardPage() {
2526
const conferenceId = useContext(ConferenceIdContext);
@@ -107,6 +108,7 @@ export default function ChairDashboardPage() {
107108
/>
108109
</div>
109110
</ConfigWrapper>
111+
<ResolutionAdoption />
110112
</div>
111113
</div>
112114
</ScrollPanel>

src/app/app/[conferenceId]/committee/[committeeId]/chair/layout.tsx

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
import Navbar from "@/lib/navbar/Navbar";
1010
import NavbarButton from "@/lib/navbar/NavbarButton";
1111
import * as m from "@/paraglide/messages";
12+
import ConfettiOnAdoption from "@/lib/components/confetti_on_adoption";
13+
import { CommitteeDataContext } from "@/lib/contexts/committee_data";
1214

1315
export default function Chair_Pages_Layout({
1416
children,
@@ -20,10 +22,10 @@ export default function Chair_Pages_Layout({
2022
<SpeakersListMiniatureProvider>
2123
<SpeakersListMiniature />
2224
{/* <MessageCountProvider> */}
23-
<div className="flex h-screen w-screen overflow-hidden bg-white text-primary-100 shadow-md dark:bg-primary-100 dark:text-primary-900">
24-
<ChairNavbar />
25-
{children}
26-
</div>
25+
<div className="flex h-screen w-screen overflow-hidden bg-white text-primary-100 shadow-md dark:bg-primary-100 dark:text-primary-900">
26+
<ChairNavbar />
27+
{children}
28+
</div>
2729
{/* </MessageCountProvider> */}
2830
</SpeakersListMiniatureProvider>
2931
</>
@@ -32,53 +34,61 @@ export default function Chair_Pages_Layout({
3234

3335
function ChairNavbar() {
3436
const { messageCount } = useContext(MessageCountContext);
37+
const committeeData = useContext(CommitteeDataContext);
3538

3639
return (
37-
<Navbar>
38-
<NavbarButton
39-
icon="rocket-launch"
40-
link="../../../hub/team/committees"
41-
title={m.hub()}
42-
/>
43-
<div className="h-4" />
44-
<NavbarButton
45-
icon="square-sliders"
46-
link={"./dashboard"}
47-
title={m.configurations()}
48-
/>
49-
<NavbarButton
50-
icon="users-line"
51-
link={"./attendees"}
52-
title={m.attendees()}
53-
/>
54-
<NavbarButton
55-
icon="podium"
56-
link={"./speakers"}
57-
title={m.speakersList()}
58-
/>
59-
{/* <NavButton TODO add Voting page
40+
<>
41+
<Navbar>
42+
<NavbarButton
43+
icon="rocket-launch"
44+
link="../../../hub/team/committees"
45+
title={m.hub()}
46+
/>
47+
<div className="h-4" />
48+
<NavbarButton
49+
icon="square-sliders"
50+
link={"./dashboard"}
51+
title={m.configurations()}
52+
/>
53+
<NavbarButton
54+
icon="users-line"
55+
link={"./attendees"}
56+
title={m.attendees()}
57+
/>
58+
<NavbarButton
59+
icon="podium"
60+
link={"./speakers"}
61+
title={m.speakersList()}
62+
/>
63+
{/* <NavButton TODO add Voting page
6064
icon="poll-people"
6165
link={"./voting"}
6266
title={LL.navbar.VOTING()}
6367
/> */}
64-
<NavbarButton
65-
icon="chalkboard"
66-
link={"./whiteboard"}
67-
title={m.whiteboard()}
68-
/>
69-
{/* <NavbarButton
68+
<NavbarButton
69+
icon="chalkboard"
70+
link={"./whiteboard"}
71+
title={m.whiteboard()}
72+
/>
73+
{/* <NavbarButton
7074
icon="inbox"
7175
link={"./inbox"}
7276
title={m.inbox()}
7377
badge={messageCount ?? 0}
7478
/> */}
75-
{/* <NavButton TODO add Resolution Editor page
79+
{/* <NavButton TODO add Resolution Editor page
7680
icon="scroll"
7781
link={"./resolutions"}
7882
title={LL.navbar.RESOLUTIONS()}
7983
/> */}
80-
<div className="flex-1" />
81-
{/* <ExternalLinks /> */}
82-
</Navbar>
84+
<div className="flex-1" />
85+
{/* <ExternalLinks /> */}
86+
</Navbar>
87+
<ConfettiOnAdoption
88+
adoptionDate={committeeData?.lastAdoptedResolution}
89+
title={committeeData?.agendaItems.find((x) => x.isActive)?.title}
90+
committee={committeeData?.name}
91+
/>
92+
</>
8393
);
8494
}

src/app/app/[conferenceId]/hub/team/committees/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { conferenceRoleTranslation } from "@/lib/translationUtils";
1111
import { languageTag } from "@/paraglide/runtime";
1212
import { LargeFlag } from "@/lib/components/Flag";
1313
import * as m from "@/paraglide/messages";
14+
import ConfettiOnAdoption from "@/lib/components/confetti_on_adoption";
1415

1516
export default function ChairHub() {
1617
const conferenceId = useContext(ConferenceIdContext);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useEffect, useState } from "react";
2+
import Fireworks from "react-canvas-confetti/dist/presets/fireworks";
3+
import Realistic from "react-canvas-confetti/dist/presets/realistic";
4+
import Marquee from "react-fast-marquee";
5+
6+
/**
7+
* This Component is used whenever a section or widget has no data to display.
8+
* It displays a ban icon and a (custom) title.
9+
*/
10+
11+
export default function ConfettiOnAdoption({
12+
adoptionDate,
13+
title = "Unbekannt",
14+
committee = "Unbekannt",
15+
durationSeconds = 60,
16+
}: {
17+
adoptionDate?: Date | null;
18+
title?: string;
19+
committee?: string;
20+
durationSeconds?: number;
21+
}) {
22+
const [timeDifference, setTimeDifference] = useState<number>();
23+
24+
useEffect(() => {
25+
if (adoptionDate) {
26+
const interval = setInterval(() => {
27+
setTimeDifference(
28+
new Date().getTime() - new Date(adoptionDate).getTime(),
29+
);
30+
}, 500);
31+
return () => clearInterval(interval);
32+
}
33+
}, [adoptionDate, timeDifference]);
34+
35+
if (!adoptionDate || !timeDifference) return null;
36+
if (timeDifference > durationSeconds * 1000) return null;
37+
return (
38+
<div className="pointer-events-none fixed top-0 right-0 bottom-0 left-0 z-50 flex flex-col items-center justify-center">
39+
<Realistic autorun={{ speed: 0.7 }} />
40+
<div className="fixed top-2/3 right-0 left-0 flex h-32 items-center justify-center bg-primary-200 font-mono text-3xl font-bold text-white">
41+
<Marquee autoFill speed={120}>
42+
<span className="mx-18">+++</span>Resolution zum Thema
43+
<span className="mx-4 italic">"{title}"</span>im Gremium{" "}
44+
<span className="mx-4 italic">{committee}</span> verabschiedet
45+
</Marquee>
46+
</div>
47+
</div>
48+
);
49+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from "react";
2+
import ConfigWrapper from "./chair/config_wrapper";
3+
import Button from "../Button";
4+
import { useParams } from "next/navigation";
5+
import { backend } from "@/lib/backend/clientsideBackend";
6+
7+
/**
8+
* This Component is used in the Dashboard. It shows the current,
9+
* and the next step in the debate process according to the rules of procedure.
10+
*/
11+
12+
export default function ResolutionAdoption() {
13+
const { conferenceId, committeeId } = useParams();
14+
15+
const adoptResolution = () => {
16+
backend
17+
.conference({ conferenceId: conferenceId as string })
18+
.committee({ committeeId: committeeId as string })
19+
.adoptResolution.patch();
20+
};
21+
return (
22+
<>
23+
<ConfigWrapper
24+
title="Resolution Verabschiedet"
25+
description="Verkündet die Verabschiedung der Resolution mit Konfetti. Es kann bis zu 5 Sekunden dauern, bis die Verabschiedung verkündet wird."
26+
>
27+
<Button
28+
faIcon="party-horn"
29+
label="Verabschiedung verkünden"
30+
onClick={() => adoptResolution()}
31+
className="w-full"
32+
/>
33+
</ConfigWrapper>
34+
</>
35+
);
36+
}

src/lib/components/navigation-hub/committee_grid.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "@/lib/contexts/committee_data";
1313
import { StatusTimerProvider } from "@/lib/contexts/status_timer";
1414
import FAIcon from "../FAIcon";
15+
import ConfettiOnAdoption from "../confetti_on_adoption";
1516

1617
type CommitteeArray = Awaited<
1718
ReturnType<ReturnType<(typeof backend)["conference"]>["committee"]["get"]>
@@ -206,6 +207,11 @@ function CommitteeCard({
206207
</div>
207208
</SmallInfoCard>
208209
</Link>
210+
<ConfettiOnAdoption
211+
adoptionDate={committee.lastAdoptedResolution}
212+
committee={committee.name}
213+
title={committee.agendaItems.find((x) => x.isActive)?.title}
214+
/>
209215
</StatusTimerProvider>
210216
</CommitteeDataProvider>
211217
</CommitteeIdContext.Provider>

0 commit comments

Comments
 (0)