Skip to content

Commit

Permalink
Implement chairperson and participant views
Browse files Browse the repository at this point in the history
This allows a meeting's secretary/chair to a current motion at the
/m/:meetingId/chair route and for others to view when that changes at
the /m/:meetingId/participant route instantly.

Shout-out to this user on GitHub for the optimal system-font CSS: picturepan2/spectre#666 (comment)

Resolves #1
  • Loading branch information
WillieCubed committed Jan 9, 2023
1 parent 0d3fe5e commit ff13720
Show file tree
Hide file tree
Showing 15 changed files with 1,192 additions and 43 deletions.
824 changes: 814 additions & 10 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"@types/node": "^18.11.17",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"firebase": "^9.15.0",
"formik": "^2.2.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.5.0",
Expand Down
Binary file added public/fonts/inter-typeface.ttf
Binary file not shown.
47 changes: 37 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@

import { Route, Routes } from 'react-router-dom';
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import "./App.css";
import ChairDashboard from "./modules/chair/ChairDashboard";
import LandingPage from "./modules/common/LandingPage";
import ParticipantDisplay from "./modules/participant/components/ParticipantDisplay";
import IndexRoute from "./routes/IndexRoute";
import MeetingRoute from "./routes/MeetingRoute";

const ROUTES = [
{
path: "/m/:meetingId/",
element: <MeetingRoute />,
children: [
{
path: "chair/",
element: <ChairDashboard />,
},
{
path: "participant/",
element: <ParticipantDisplay />,
},
],
},
{
path: "/",
element: <IndexRoute />,
children: [
{
index: true,
element: <LandingPage />,
},
],
},
];

const router = createBrowserRouter(ROUTES);

function App() {
return (
<div className="min-h-screen">
<Routes>
<Route path="/">
{/* <Home /> */}
</Route>
<Route path="/app">{/* <About /> */}</Route>
<Route path="/chair">{/* <Dashboard /> */}</Route>
</Routes>
<div className="min-h-screen bg-primary-2">
<RouterProvider router={router} />
</div>
);
}
Expand Down
12 changes: 4 additions & 8 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import React from "react";
import "./index.css";
import { createRoot } from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter as Router } from "react-router-dom";

import "./index.css";

import { createRoot } from 'react-dom/client';
const container = document.getElementById('root');
const container = document.getElementById("root");
const root = createRoot(container!);
root.render(
<React.StrictMode>
<Router>
<App />
</Router>
<App />
</React.StrictMode>
);

Expand Down
16 changes: 16 additions & 0 deletions src/lib/firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { initializeApp } from "firebase/app";
import { getDatabase } from "firebase/database";

const options = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.REACT_APP_DATABASE_URL,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID,
};

const firebase = initializeApp(options);

export const db = getDatabase(firebase);
147 changes: 147 additions & 0 deletions src/modules/chair/ChairDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { ref, child, set, push, onValue } from "@firebase/database";
import { Field, Form, Formik, FormikHelpers, FormikValues } from "formik";
import React from "react";
import { useNavigate, useParams } from "react-router-dom";
import { db } from "../../lib/firebase";

export default function ChairDashboard() {
const navigate = useNavigate();
const { meetingId } = useParams();

const [newMotion, setNewMotion] = React.useState("");

const [currentMotion, setCurrentMotion] = React.useState("");

React.useEffect(() => {
const meetingRef = ref(db, `/meetings/${meetingId}`);
const unsubscribe = onValue(meetingRef, (snapshot) => {
const val = snapshot.val() as MeetingInfo;
setCurrentMotion(val.currentMotion);
});
return () => {
unsubscribe();
};
}, []);

const handleSubmit = (data: MeetingInfo, startNow: boolean) => {
const meetingRef = ref(db, `/meetings/${meetingId}`);
push(meetingRef, data).then((newRef) => {
const meetingId = newRef.key;
navigate(`/chair/${meetingId}`);
});
};

/**
* Send the current motion in the text box to the server.
*
* @param motion The new motion to set
*/
const updateCurrentMotion = () => {
const meetingRef = ref(db, `/meetings/${meetingId}`);
set(meetingRef, {
currentMotion: newMotion,
})
.then(() => {
// Reset after successful update
setNewMotion("");
})
.catch((error) => {
console.error(error);
});
};

const handleFormUpdate = (event: React.ChangeEvent<HTMLInputElement>) => {
const content = event.currentTarget.value;
setNewMotion(content);
};

const shouldDisableButton = newMotion.trim() === "";

return (
<main>
<section className="absolute bottom-0 left-0 right-0 bg-primary-1">
<div className="p-4 mx-auto mx-6xl">
<div className="text-xl font-semibold">Update current motion</div>
<div className="flex mt-2">
<input
className="flex-1 p-2 rounded-md"
type="text"
name="motion"
id="motion"
value={newMotion}
onChange={handleFormUpdate}
placeholder={currentMotion}
/>
<button
className="ml-2 p-2 rounded-md bg-white disabled:bg-gray-200"
disabled={shouldDisableButton}
onClick={updateCurrentMotion}
>
Update
</button>
</div>
</div>
</section>
{/* <div className="max-w-4xl mx-auto p-4">
<CreateMeetingDialog onSubmit={handleSubmit} />
</div> */}
</main>
);
}

interface CreateMeetingDialogProps {
onSubmit: (data: MeetingInfo, startNow: boolean) => void;
}

function CreateMeetingDialog({ onSubmit }: CreateMeetingDialogProps) {
const handleSubmit = (
values: MeetingInfo,
actions: FormikHelpers<MeetingInfo>
) => {
onSubmit(values, true);
};

return (
<div className="p-8 bg-primary-1 rounded-lg">
<Formik
initialValues={{
title: "Just another meeting",
startTime: "",
endTime: "",
agendaUrl: "",
currentMotion: "",
}}
onSubmit={handleSubmit}
>
<Form>
<div className="py-2">
<label className="text-xl font-bold" htmlFor="title">
Meeting Name
</label>
<Field className="block p-2 rounded-md" id="title" name="title" />
</div>
<div>
<label htmlFor="startTime">Start Time</label>
<Field name="startTime" type="date" />
<label htmlFor="endTime">End Time</label>
<Field name="endTime" type="date" />
</div>
<label htmlFor="agendaUrl">Agenda</label>
<div>
<button className="p-2 bg-primary-4 rounded-md" type="submit">
Start now
</button>
</div>
</Form>
</Formik>
</div>
);
}

type MeetingInfo = {
title: string;
startTime: string;
endTime: string;
agendaUrl: string;
currentMotion: string;
};
30 changes: 30 additions & 0 deletions src/modules/common/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Link } from "react-router-dom";
import IconButton from "./components/IconButton";

export default function LandingPage() {
return (
<div className="h-full px-4 xl:grid xl:grid-cols-12">
<section className="inline-block rounded-[16px] py-8 xl:col-start-2 xl:col-span-5 bg-primary-1 px-8 mt-[64px] shadow-md">
<section>
<div className="text-[96px] font-bold">ParliPro</div>
<div className="mt-4 text-[34px] font-semibold">
Presiding over meetings, done simply.
</div>
</section>
<section className="mt-[96px] space-x-4 space-y-4">
<IconButton to="/m/test/chair">Start meeting</IconButton>
<IconButton to="/m/test/participant">Join meeting</IconButton>
{/* <div className="text-md">
or open a{" "}
<Link
className="text-blue-500 hover:underline focus:underline"
to="/companion"
>
companion display
</Link>
</div> */}
</section>
</section>
</div>
);
}
26 changes: 26 additions & 0 deletions src/modules/common/components/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Link } from "react-router-dom";

interface IconButtonProps {
icon?: JSX.Element;
color?: string;
to?: string;
}

export default function IconButton({
icon,
color,
children,
to = "#",
}: React.PropsWithChildren<IconButtonProps>) {
return (
<Link
to={to}
className="inline-block p-3 space-x-4 rounded-md shadow-sm hover:shadow-md focus:shadow-md bg-red-400 text-white"
style={{
color: color,
}}
>
{children}
</Link>
);
}
39 changes: 39 additions & 0 deletions src/modules/participant/components/ParticipantDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react";
import { useParams } from "react-router-dom";
import { onValue, ref } from "firebase/database";
import { db } from "../../../lib/firebase";

type MeetingState = {
currentMotion: string;
};

export default function ParticipantDisplay() {
const [state, setState] = React.useState<MeetingState>();
const { meetingId } = useParams();

React.useEffect(() => {
const meetingRef = ref(db, `meetings/${meetingId}`);
const unsubscribe = onValue(meetingRef, (snapshot) => {
const updatedState = snapshot.val() as MeetingState;
if (!updatedState) {
return;
}
setState(updatedState);
});
return () => {
unsubscribe();
};
}, [meetingId]);

return (
<main className="p-4">
<div className="max-w-4xl mx-auto p-8 bg-primary-1 rounded-[24px]">
<div className="text-2xl font-bold">Current motion</div>
<div className="text-xl font-bold">
{state?.currentMotion ?? "Meeting not in session"}
</div>
<div className="mt-4 text-lg">Current meeting: {meetingId}</div>
</div>
</main>
);
}
6 changes: 3 additions & 3 deletions src/reportWebVitals.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ReportHandler } from 'web-vitals';
import type { ReportCallback } from "web-vitals";

const reportWebVitals = (onPerfEntry?: ReportHandler) => {
const reportWebVitals = (onPerfEntry?: ReportCallback) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
Expand Down
9 changes: 9 additions & 0 deletions src/routes/IndexRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Outlet } from "react-router-dom";

export default function IndexRoute() {
return (
<>
<Outlet />
</>
);
}
13 changes: 13 additions & 0 deletions src/routes/MeetingRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Outlet } from "react-router-dom";

/**
* Route: /m/:meetingId
*/
export default function MeetingRoute() {
return (
<>
{/* TODO: Add navigation */}
<Outlet />
</>
);
}
Loading

0 comments on commit ff13720

Please sign in to comment.