Skip to content
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
This project aims to allow you to model multi-list works council election representation outcomes given the latest revision of the works constitution act.

We implement the dhondt (aka greatest divisors) method to determine seat distribution for the popular vote, and to measure the gender quotas to determine any additional seat distribution that is needed.

It is likely this still does not work in several edge cases, so please seek a union representative, labor lawyer, etc for proper legal answers to questions about the works constitution act elections.


![Demo app with columns](public/demo.png)
## Setup Requirements
This requires node 18 and `pnpm`
Expand Down
65 changes: 57 additions & 8 deletions src/components/App/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ form,
flex-direction: row;
}

#workplace-info .input-control > input {
width: 90px;
}
#workplace-info .input-control {
justify-content: right;
}
.input-control > label,
.input-control > input,
.input-control > .error,
Expand All @@ -44,9 +50,8 @@ form,
}

.input-control > label {
/* text-align: right; */
text-align: right;
font-weight: 400;
width: 220px;
}

@media (max-width: 650px) {
Expand All @@ -57,7 +62,7 @@ form,

.input-control {
display: flex;
flex-direction: column;
/* flex-direction: column; */
& > label {
width: 100%;
}
Expand All @@ -69,19 +74,25 @@ form,
}
}

form .input-control .error {
.error, .warning, .success {
padding: 8px;
margin: 6px;
font-size: 1rem;
font-style: oblique;
}
.error {
color: red;
}

form .input-control .warning {
font-size: 1rem;
font-style: oblique;
.warning {
color: #8f6b11;
background-color: lightyellow;
}

.success {
background-color: lightgreen;
color: darkgreen;
}

.App-logo {
height: 40vmin;
pointer-events: none;
Expand All @@ -101,3 +112,41 @@ form .input-control .warning {
transform: rotate(360deg);
}
}

@media (min-width: 1024px) {
#tray {
display: grid;
grid-gap: 6px;
}

#workplace-info, #candidate-lists {
}

#workplace-info {
max-width: 360px;
}

#candidate-lists {
justify-content: left;
overflow: scroll;
grid-column: 2;
}

}

#legend {
margin-left: 10px;
label {
margin-right: 12px;
}
.legend-label {
padding: 6px;
font-size: 1rem;
}
.legend-block {
vertical-align: middle;
display: inline-block;
width: 1rem;
height: 1rem;
}
}
142 changes: 93 additions & 49 deletions src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { WorkplaceInfo } from "../WorkplaceInfo";
import { tallyAndValidateLists } from "../../lib/listData";

import "./App.css";
import { getColor } from "src/utilities/getColor";
import { ElectionResults } from "../Election/ElectionResults";

const CandidateLists = lazy(() =>
import("../CandidateLists").then(({ CandidateLists }) => ({
Expand Down Expand Up @@ -126,55 +128,97 @@ export function App({ setLocale }: Props) {

return (
<div className="App">
<h1>
<FormattedMessage id="title" />
<span>
<button onClick={() => setLocale("en")}>en</button>
<button onClick={() => setLocale("de")}>de</button>
<button onClick={() => setLocale("ar")}>ar</button>
</span>
</h1>
<h2>
<FormattedMessage id="workplaceInfo.header" />
</h2>
<WorkplaceInfo actions={actions} data={data} />
<h2>
Candidate Lists&nbsp;
<button
aria-label="toggle horizontal list display"
onClick={() => setListDisplay(ListDisplay.horizontal)}
disabled={listDisplay === ListDisplay.horizontal}
>
horizontal
</button>
<button
aria-label="Toggle vertical list display"
onClick={() => setListDisplay(ListDisplay.vertical)}
disabled={listDisplay === ListDisplay.vertical}
>
vertical
</button>
</h2>
<CandidateLists
columns={1}
data={{ totalWorkers }}
handle
onChange={setLists}
minorityGender={minorityGender}
listData={listData}
onRemoveColumn={(columnId) => {
delete lists[columnId];
}}
vertical={listDisplay === ListDisplay.vertical}
wrapperStyle={() => ({
maxWidth: 400,
})}
/>
{totalWorkers > 0 && (
<>
<div className="form"></div>
</>
)}
<div id="tray">
<div id="workplace-info">
<h1>
<FormattedMessage id="title" />
</h1>
<div>
<button onClick={() => setLocale("en")}>en</button>
<button onClick={() => setLocale("de")}>de</button>
<button onClick={() => setLocale("ar")}>ar</button>
</div>
<h2>
<FormattedMessage id="workplaceInfo.header" />
</h2>
<WorkplaceInfo
actions={actions}
data={{
numMen: data.numMen,
numNonBinary: data.numNonBinary,
numWomen: data.numWomen,
}}
/>
<ElectionResults data={data} />
</div>
<div id="candidate-lists">
<header>
<span
style={{
display: "inline-block",
fontSize: "28.8px",
margin: "16px 0px",
padding: "4px 0px",
}}
>
<FormattedMessage id="candidateLists.header" />
</span>

<button
aria-label="toggle horizontal list display"
onClick={() => setListDisplay(ListDisplay.horizontal)}
disabled={listDisplay === ListDisplay.horizontal}
>
horizontal
</button>
<button
aria-label="Toggle vertical list display"
onClick={() => setListDisplay(ListDisplay.vertical)}
disabled={listDisplay === ListDisplay.vertical}
>
vertical
</button>
<span id="legend">
<span
className="legend-block"
style={{
backgroundColor: getColor({ isPopularlyElected: true }),
}}
/>
<span className="legend-label">Popularly Elected </span>
<span
className="legend-block"
style={{
backgroundColor: getColor({ isOverflowElected: true }),
}}
/>
<span className="legend-label">List Overflow</span>
<span
className="legend-block"
style={{
backgroundColor: getColor({ isGenderQuotaElected: true }),
}}
/>
<span className="legend-label">Gender Quota</span>
</span>
</header>
<CandidateLists
columns={1}
data={{ totalWorkers }}
handle
onChange={setLists}
minorityGender={minorityGender}
listData={listData}
onRemoveColumn={(columnId) => {
delete lists[columnId];
}}
vertical={listDisplay === ListDisplay.vertical}
wrapperStyle={() => ({
maxWidth: 360,
})}
/>
</div>
</div>
</div>
);
}
30 changes: 19 additions & 11 deletions src/components/CandidateLists/CandidateList.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { SortableContext } from "@dnd-kit/sortable";
import classNames from "classnames";
import { genderArray, GenderEnum, Items, ListDataItem, ListItem } from "../../types";
import {
genderArray,
GenderEnum,
Items,
ListDataItem,
ListItem,
} from "../../types";
import { Container } from "../Container";
import { SortableItem } from "../SortableItem/SortableItem";
import { DroppableContainer } from "../DroppableContainer";

import { UniqueIdentifier } from "@dnd-kit/core";

import { SortingStrategy } from "@dnd-kit/sortable";
import { SortingStrategy } from "@dnd-kit/sortable";

import styles from './CandidateList.module.css'
import styles from "./CandidateList.module.css";
import { ListVotesForm } from "./ListVotesForm";

type CandidateListProps = {
Expand All @@ -24,12 +30,12 @@ type CandidateListProps = {
totalWorkers: number;
getMemberIndex: (id: UniqueIdentifier) => number;
handleAddItem: (listId: UniqueIdentifier) => void;
handleRemoveColumn: (listId: UniqueIdentifier) => void
handleRemoveItem: (index: number, listId: UniqueIdentifier) => void
handleRenameList: (listId: UniqueIdentifier, name: string) => void
handleRemoveColumn: (listId: UniqueIdentifier) => void;
handleRemoveItem: (index: number, listId: UniqueIdentifier) => void;
handleRenameList: (listId: UniqueIdentifier, name: string) => void;
wrapperStyle({ index }: { index: number }): React.CSSProperties;
scrollable?: boolean,
columns?: number
scrollable?: boolean;
columns?: number;
};

export function CandidateList({
Expand All @@ -49,7 +55,7 @@ export function CandidateList({
handleRemoveItem,
handleRenameList,
getMemberIndex,
wrapperStyle
wrapperStyle,
}: CandidateListProps) {
let data: ListDataItem | null = listData;
return (
Expand All @@ -69,6 +75,8 @@ export function CandidateList({
const status = {
isPopularlyElected: data?.popularlyElectedMembers.includes(index),
isOverflowElected: data?.overflowElectedMembers.includes(index),
isGenderQuotaElected:
data?.genderOverflowElectedMembers.includes(index),
};

return (
Expand Down Expand Up @@ -140,11 +148,11 @@ export function CandidateList({
list={list}
/>
)}
{data && data.listDistribution ? (
{data && data.popularListDistribution ? (
<>
<div className="input-control">
<label>Seat Distribution (raw)</label>
<span className="cell">{data.listDistribution}</span>
<span className="cell">{data.popularListDistribution}</span>
</div>
{minorityGender && (
<div className="input-control">
Expand Down
2 changes: 1 addition & 1 deletion src/components/Draggable/Draggable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const Draggable = forwardRef<HTMLButtonElement, Props>(
style={
{
...style,
'--translate-x': `${transform?.x ?? 0}px`,
// '--translate-x': `${transform?.x ?? 0}px`,
'--translate-y': `${transform?.y ?? 0}px`,
} as React.CSSProperties
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Droppable/Droppable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface Props {

export function Droppable({children, id, dragging}: Props) {
const {isOver, setNodeRef} = useDroppable({
id,
id
});

return (
Expand Down
1 change: 1 addition & 0 deletions src/components/DroppableContainer/DroppableContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function ListEditForm({ list, onChange }: ListEditFormProps) {
autoFocus
defaultValue={list.name}
onChange={(e) => onChange && onChange(e.target.value)}
style={{ maxWidth: 200 }}
/>
</form>
);
Expand Down
Loading