From d38685a0c1e0fa0f34651ddc93d72d2abdc9decd Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 2 Jan 2025 12:27:58 -0500 Subject: [PATCH 1/9] Started full drag and drop --- components/stats/Picklist.tsx | 385 +++++++++++------- lib/Types.ts | 2 +- lib/api/AccessLevels.ts | 4 +- lib/api/ApiUtils.ts | 9 +- lib/api/ClientApi.ts | 14 +- lib/client/CollectionId.ts | 4 +- .../[seasonSlug]/[competitonSlug]/index.tsx | 2 +- .../[seasonSlug]/[competitonSlug]/stats.tsx | 12 +- tests/lib/api/AccessLevels.test.ts | 6 +- 9 files changed, 261 insertions(+), 177 deletions(-) diff --git a/components/stats/Picklist.tsx b/components/stats/Picklist.tsx index b0eaff88..20b83fd8 100644 --- a/components/stats/Picklist.tsx +++ b/components/stats/Picklist.tsx @@ -1,112 +1,170 @@ -import { DbPicklist, Report } from "@/lib/Types"; +import { CompPicklistGroup, Report } from "@/lib/Types"; -import { useDrag, useDrop } from "react-dnd"; +import { ConnectDropTarget, useDrag, useDrop } from "react-dnd"; import { ChangeEvent, useCallback, useEffect, useState } from "react"; -import { FaArrowDown, FaArrowUp, FaPlus } from "react-icons/fa"; +import { FaPlus } from "react-icons/fa"; import ClientApi from "@/lib/api/ClientApi"; -type CardData = { - number: number; - picklistIndex?: number; -}; +const SHOW_PICKLISTS_ON_TEAM_CARDS = true; type Picklist = { index: number; name: string; - teams: CardData[]; + head: PicklistEntry | undefined; update: (picklist: Picklist) => void; }; -const Includes = (bucket: any[], item: CardData) => { - let result = false; - bucket.forEach((i: { number: number }) => { - if (i.number === item.number) { - result = true; - } - }); - - return result; +type PicklistEntry = { + number: number; + next?: PicklistEntry; + picklist?: Picklist; }; -function removeTeamFromPicklist(team: CardData, picklists: Picklist[]) { - if (team.picklistIndex === undefined) return; +function removeEntryFromItsPicklist(entry: PicklistEntry) { + if (entry.picklist) { + const picklist = entry.picklist; + if (picklist.head?.number === entry.number) { + picklist.head = picklist.head.next; + } else { + let curr: PicklistEntry | undefined = picklist.head; + while (curr) { + if (curr.next?.number === entry.number) { + curr.next = curr.next.next; + } + + curr = curr.next; + } + } + + entry.picklist = undefined; - const picklist = picklists[team.picklistIndex]; - if (!picklist) return; + picklist.update(picklist); - picklist.teams = picklist.teams.filter((t) => t.number !== team.number); - picklist.update(picklist); + return picklist; + } } function TeamCard(props: { - cardData: CardData; + entry: PicklistEntry; draggable: boolean; picklist?: Picklist; rank?: number; - lastRank?: number; width?: string; - height?: string; + preview?: boolean; }) { - const { number: teamNumber, picklistIndex: picklist } = props.cardData; + const { entry, width, rank, preview } = props; - const [{ isDragging }, dragRef] = useDrag({ + const [, dragRef] = useDrag({ type: "team", - item: props.cardData, - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), + item: () => { + return props.entry; + }, }); - function changeTeamRank(change: number) { - const picklist = props.picklist; - if ( - picklist === undefined || - props.rank === undefined || - props.lastRank === undefined - ) - return; - - const newRank = props.rank + change; - if (newRank < 0 || newRank > props.lastRank) return; - - const otherTeam = picklist.teams[newRank]; - picklist.teams[newRank] = picklist.teams[props.rank]; - picklist.teams[props.rank] = otherTeam; + // We have the useDrop for the InsertDropSite here because we need to know if the dragged item is over the insert site + const [{ isOverInsert, draggedEntry: draggedInsertEntry }, insertRef] = + useDrop({ + accept: "team", + drop: (dragged: PicklistEntry) => { + // If you're moving a card into the same spot, don't do anything + if ( + entry.number === dragged.number && + entry.picklist === dragged.picklist + ) + return; + + const picklist = removeEntryFromItsPicklist(dragged); + + // Create a copy, don't operate on the original + dragged = { + number: dragged.number, + next: entry.next, + picklist: entry.picklist, + }; + + entry.next = dragged; + + picklist?.update(picklist); + }, + collect: (monitor) => ({ + isOverInsert: monitor.isOver(), + draggedEntry: monitor.getItem(), + }), + }); - picklist.update(picklist); - } + return ( + <> +
+
void} + > +

+ {rank !== undefined ? `${rank}. ` : ""} + Team{" "} + #{entry.number} + {SHOW_PICKLISTS_ON_TEAM_CARDS && entry.picklist && ( + + {" "} + ({entry.picklist.name}) + + )} +

+
+ {rank && !preview && ( + + )} +
+ {rank && !preview && entry.next && ( + + )} + + ); +} +/** + * The useDrop is in the TeamCard component + */ +function InsertDropSite({ + rank, + entry, + isOver, + dropRef, + draggedEntry, +}: { + rank: number; + entry: PicklistEntry; + isOver: boolean; + dropRef: ConnectDropTarget; + draggedEntry: PicklistEntry; +}) { return (
void} + ref={dropRef as any} + className={`w-full ${entry.next ? "h-full" : "h-4"}`} > -

- {props.rank !== undefined ? `${props.rank + 1}. ` : ""} - Team{" "} - #{teamNumber} -

- {props.rank !== undefined && props.lastRank && props.picklist ? ( -
- {props.rank > 0 && ( - - )} - {props.rank < props.lastRank && ( - - )} + {isOver ? ( +
+
) : ( - "" +
)}
); @@ -115,21 +173,19 @@ function TeamCard(props: { function PicklistCard(props: { picklist: Picklist; picklists: Picklist[] }) { const picklist = props.picklist; - const [{ isOver }, dropRef] = useDrop({ + const [{ isOver, entry }, dropRef] = useDrop({ accept: "team", - drop: (item: CardData) => { - if (item.picklistIndex === picklist.index) return; + drop: (item: PicklistEntry) => { + removeEntryFromItsPicklist(item); - removeTeamFromPicklist(item, props.picklists); + item = { number: item.number, next: picklist.head, picklist }; + picklist.head = item; - if (!Includes(picklist.teams, item)) { - item.picklistIndex = picklist.index; - picklist.teams.push(item); - picklist.update(picklist); - } + picklist.update(picklist); }, collect: (monitor) => ({ isOver: monitor.isOver(), + entry: monitor.getItem(), }), }); @@ -139,10 +195,7 @@ function PicklistCard(props: { picklist: Picklist; picklists: Picklist[] }) { } return ( -
void} - > +
{picklist.name} - {picklist.teams.map((team, index) => ( - - ))} - {isOver &&

Drop Here!

} +
+
+ {isOver ? ( +
+ +
+ ) : ( + !picklist.head && ( +

Drop Here!

+ ) + )} +
+ {picklist.head && ( + + )} +
); } export function TeamList(props: { - teams: CardData[]; + teams: PicklistEntry[]; picklists: Picklist[]; expectedTeamCount: number; }) { - const [{ isOver }, dropRef] = useDrop({ + const [, dropRef] = useDrop({ accept: "team", - drop: (item: CardData) => { - removeTeamFromPicklist(item, props.picklists); - }, - collect: (monitor) => ({ - isOver: monitor.isOver(), - }), + drop: removeEntryFromItsPicklist, }); return (
void} + ref={dropRef as any} className="w-full h-fit flex flex-row bg-base-300 space-x-2 p-2 overflow-x-scroll" > {props.teams @@ -193,7 +260,7 @@ export function TeamList(props: { .map((team) => ( ))} @@ -210,7 +277,7 @@ export default function PicklistScreen(props: { teams: number[]; reports: Report[]; expectedTeamCount: number; - picklist: DbPicklist; + picklist: CompPicklistGroup; compId: string; }) { const [picklists, setPicklists] = useState([]); @@ -225,25 +292,25 @@ export default function PicklistScreen(props: { const teams = props.teams.map((team) => ({ number: team })); - const savePicklists = useCallback( - (picklists: Picklist[]) => { - const picklistDict = picklists.reduce( - (acc, picklist) => { - acc.picklists[picklist.name] = picklist.teams.map( - (team) => team.number, - ); - return acc; - }, - { - _id: props.picklist._id, - picklists: {}, - }, - ); - - api.updatePicklist(picklistDict); - }, - [props.picklist._id], - ); + useEffect(() => { + const picklistDict = picklists.reduce( + (acc, picklist) => { + const picklistArray: number[] = []; + for (let curr = picklist.head; curr; curr = curr.next) { + picklistArray.push(curr.number); + } + + acc.picklists[picklist.name] = picklistArray; + return acc; + }, + { + _id: props.picklist._id, + picklists: {}, + }, + ); + + api.updatePicklist(picklistDict); + }, [props.picklist._id, picklists]); const updatePicklist = useCallback( (picklist: Picklist) => { @@ -256,26 +323,39 @@ export default function PicklistScreen(props: { } }); - savePicklists(newPicklists); return newPicklists; }); }, - [setPicklists, savePicklists], + [setPicklists], ); - const loadDbPicklist = useCallback( - (picklistDict: DbPicklist) => { + const loadCompPicklistGroup = useCallback( + (picklistDict: CompPicklistGroup) => { setPicklists( - Object.entries(picklistDict.picklists).map((picklist, index) => { + Object.entries(picklistDict.picklists).map(([name, teams], index) => { const newPicklist: Picklist = { index, - name: picklist[0], - teams: picklist[1].map((team: number) => ({ number: team })), + name, + head: undefined, update: updatePicklist, }; - for (const team of newPicklist.teams) { - team.picklistIndex = newPicklist.index; + if (teams.length > 0) { + let curr: PicklistEntry = { + number: teams[0], + next: undefined, + picklist: newPicklist, + }; + newPicklist.head = curr; + + for (let i = 1; i < teams.length; i++) { + curr.next = { + number: teams[i], + next: undefined, + picklist: newPicklist, + }; + curr = curr.next; + } } return newPicklist; @@ -291,10 +371,10 @@ export default function PicklistScreen(props: { setLoadingPicklists(LoadState.Loading); api.getPicklist(props.picklist?._id).then((picklist) => { if (picklist) { - loadDbPicklist(picklist); + loadCompPicklistGroup(picklist); } }); - loadDbPicklist(props.picklist); + loadCompPicklistGroup(props.picklist); setLoadingPicklists(LoadState.Loaded); }, [ @@ -303,31 +383,38 @@ export default function PicklistScreen(props: { LoadState.Loading, LoadState.Loaded, props.picklist, - loadDbPicklist, + loadCompPicklistGroup, ]); const addPicklist = () => { const newPicklist: Picklist = { index: picklists.length, name: `Picklist ${picklists.length + 1}`, - teams: [], + head: undefined, update: updatePicklist, }; const newPicklists = [...picklists, newPicklist]; - savePicklists(newPicklists); setPicklists(newPicklists); }; + const [, dropRef] = useDrop({ + accept: "team", + drop: removeEntryFromItsPicklist, + }); + return (
+ /> -
+
{loadingPicklists === LoadState.Loading ? (
@@ -344,7 +431,7 @@ export default function PicklistScreen(props: { key={picklist.index} picklist={picklist} picklists={picklists} - > + /> )) )}
@@ -354,7 +441,7 @@ export default function PicklistScreen(props: { className="max-sm:hidden btn btn-circle btn-lg btn-primary absolute right-10 bottom-[21rem] animate-pulse font-bold " onClick={addPicklist} > - + )}
diff --git a/lib/Types.ts b/lib/Types.ts index e3848d99..f8f2e2db 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -559,7 +559,7 @@ export interface EventData { oprRanking: TheBlueAlliance.OprRanking; } -export type DbPicklist = { +export type CompPicklistGroup = { _id: string; picklists: { [name: string]: number[]; diff --git a/lib/api/AccessLevels.ts b/lib/api/AccessLevels.ts index dd7e233b..9f95c0e3 100644 --- a/lib/api/AccessLevels.ts +++ b/lib/api/AccessLevels.ts @@ -1,7 +1,7 @@ import { NextApiRequest } from "next"; import { Competition, - DbPicklist, + CompPicklistGroup, Match, Pitreport, Report, @@ -404,7 +404,7 @@ namespace AccessLevels { const picklist = await ( await db - ).findObjectById( + ).findObjectById( CollectionId.Picklists, new ObjectId(picklistId), ); diff --git a/lib/api/ApiUtils.ts b/lib/api/ApiUtils.ts index 8507c08e..c2b49a26 100644 --- a/lib/api/ApiUtils.ts +++ b/lib/api/ApiUtils.ts @@ -8,7 +8,7 @@ import { Team, Report, Competition, - DbPicklist, + CompPicklistGroup, Match, Pitreport, Season, @@ -67,7 +67,10 @@ export function getCompFromSubjectiveReport( }); } -export function getCompFromPicklist(db: DbInterface, picklist: DbPicklist) { +export function getCompFromPicklist( + db: DbInterface, + picklist: CompPicklistGroup, +) { return db.findObject(CollectionId.Competitions, { picklist: picklist._id?.toString(), }); @@ -119,7 +122,7 @@ export async function getTeamFromPitReport(db: DbInterface, report: Pitreport) { export async function getTeamFromPicklist( db: DbInterface, - picklist: DbPicklist, + picklist: CompPicklistGroup, ) { return getTeamFromDocument(db, getCompFromPicklist, picklist); } diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts index fe92f229..6f8a936a 100644 --- a/lib/api/ClientApi.ts +++ b/lib/api/ClientApi.ts @@ -6,7 +6,7 @@ import { Alliance, Competition, CompetitonNameIdPair, - DbPicklist, + CompPicklistGroup, League, Match, MatchType, @@ -1235,7 +1235,7 @@ export default class ClientApi extends NextApiTemplate { getPicklistFromComp = createNextRoute< [string], - DbPicklist | undefined, + CompPicklistGroup | undefined, ApiDependencies, { comp: Competition } >({ @@ -1244,7 +1244,7 @@ export default class ClientApi extends NextApiTemplate { handler: async (req, res, { db: dbPromise }, { comp }, [compId]) => { const db = await dbPromise; - const picklist = await db.findObjectById( + const picklist = await db.findObjectById( CollectionId.Picklists, new ObjectId(comp.picklist), ); @@ -1255,9 +1255,9 @@ export default class ClientApi extends NextApiTemplate { getPicklist = createNextRoute< [string], - DbPicklist | undefined, + CompPicklistGroup | undefined, ApiDependencies, - { picklist: DbPicklist } + { picklist: CompPicklistGroup } >({ isAuthorized: (req, res, deps, [picklistId]) => AccessLevels.IfOnTeamThatOwnsPicklist(req, res, deps, picklistId), @@ -1267,10 +1267,10 @@ export default class ClientApi extends NextApiTemplate { }); updatePicklist = createNextRoute< - [DbPicklist], + [CompPicklistGroup], { result: string }, ApiDependencies, - { picklist: DbPicklist } + { picklist: CompPicklistGroup } >({ isAuthorized: (req, res, deps, [picklist]) => AccessLevels.IfOnTeamThatOwnsPicklist(req, res, deps, picklist._id), diff --git a/lib/client/CollectionId.ts b/lib/client/CollectionId.ts index c1051ad9..5eb209a3 100644 --- a/lib/client/CollectionId.ts +++ b/lib/client/CollectionId.ts @@ -9,7 +9,7 @@ import { Account, Session, Pitreport, - DbPicklist, + CompPicklistGroup, WebhookHolder, } from "../Types"; @@ -54,7 +54,7 @@ export type CollectionIdToType = : Id extends CollectionId.PitReports ? Pitreport : Id extends CollectionId.Picklists - ? DbPicklist + ? CompPicklistGroup : Id extends CollectionId.SubjectiveReports ? SubjectiveReport : Id extends CollectionId.Webhooks diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index 95d80fdf..e073c617 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -10,7 +10,7 @@ import { User, Team, Competition, - DbPicklist, + CompPicklistGroup, Season, } from "@/lib/Types"; import { useCurrentSession } from "@/lib/client/useCurrentSession"; diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx index ce8c2398..276ca4fb 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx @@ -6,7 +6,7 @@ import { Pitreport, SubjectiveReport, Report, - DbPicklist, + CompPicklistGroup, } from "@/lib/Types"; import { useState, useEffect, useCallback } from "react"; import Container from "@/components/Container"; @@ -30,7 +30,7 @@ export default function Stats(props: { pitReports: Pitreport[]; subjectiveReports: SubjectiveReport[]; competition: Competition; - picklists: DbPicklist; + picklists: CompPicklistGroup; }) { const [update, setUpdate] = useState(Date.now()); const [updating, setUpdating] = useState(false); @@ -86,11 +86,6 @@ export default function Stats(props: { Object.keys(r.robotComments).forEach((c) => teams.add(+c)), ); //+str converts to number - console.log("Reports", reports); - console.log("PitReports", pitReports); - console.log("SubjectiveReports", subjectiveReports); - console.log("Teams", teams); - return ( { }, ); - const picklists = await db.findObjectById( + const picklists = await db.findObjectById( CollectionId.Picklists, new ObjectId(resolved.competition?.picklist), ); - console.log("Found picklists:", resolved.competition?.picklist, picklists); return { props: { diff --git a/tests/lib/api/AccessLevels.test.ts b/tests/lib/api/AccessLevels.test.ts index fcafa6c9..4e06ed87 100644 --- a/tests/lib/api/AccessLevels.test.ts +++ b/tests/lib/api/AccessLevels.test.ts @@ -11,7 +11,7 @@ import { User, Report, Pitreport, - DbPicklist, + CompPicklistGroup, } from "@/lib/Types"; import { ObjectId } from "bson"; @@ -800,7 +800,7 @@ describe(`AccessLevels.${AccessLevels.IfOnTeamThatOwnsPicklist.name}`, () => { const picklist = await db.addObject( CollectionId.Picklists, - {} as any as DbPicklist, + {} as any as CompPicklistGroup, ); const comp = await db.addObject(CollectionId.Competitions, { picklist: picklist._id!.toString(), @@ -830,7 +830,7 @@ describe(`AccessLevels.${AccessLevels.IfOnTeamThatOwnsPicklist.name}`, () => { const picklist = await db.addObject( CollectionId.Picklists, - {} as any as DbPicklist, + {} as any as CompPicklistGroup, ); const comp = await db.addObject(CollectionId.Competitions, { picklist: picklist._id!.toString(), From dce61868e6e55500d8ee6dcded01b28f40da0d42 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Thu, 2 Jan 2025 20:17:50 -0500 Subject: [PATCH 2/9] More debugging --- components/stats/Picklist.tsx | 78 +++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/components/stats/Picklist.tsx b/components/stats/Picklist.tsx index 20b83fd8..af4b12d9 100644 --- a/components/stats/Picklist.tsx +++ b/components/stats/Picklist.tsx @@ -2,8 +2,9 @@ import { CompPicklistGroup, Report } from "@/lib/Types"; import { ConnectDropTarget, useDrag, useDrop } from "react-dnd"; import { ChangeEvent, useCallback, useEffect, useState } from "react"; -import { FaPlus } from "react-icons/fa"; +import { FaPlus, FaTrash } from "react-icons/fa"; import ClientApi from "@/lib/api/ClientApi"; +import { ObjectId } from "bson"; const SHOW_PICKLISTS_ON_TEAM_CARDS = true; @@ -12,12 +13,14 @@ type Picklist = { name: string; head: PicklistEntry | undefined; update: (picklist: Picklist) => void; + delete: () => void; }; type PicklistEntry = { number: number; next?: PicklistEntry; picklist?: Picklist; + id?: string; }; function removeEntryFromItsPicklist(entry: PicklistEntry) { @@ -30,6 +33,7 @@ function removeEntryFromItsPicklist(entry: PicklistEntry) { while (curr) { if (curr.next?.number === entry.number) { curr.next = curr.next.next; + break; } curr = curr.next; @@ -52,7 +56,8 @@ function TeamCard(props: { width?: string; preview?: boolean; }) { - const { entry, width, rank, preview } = props; + const { entry, width, rank, preview, picklist } = props; + if (picklist) entry.picklist = picklist; const [, dragRef] = useDrag({ type: "team", @@ -69,22 +74,32 @@ function TeamCard(props: { // If you're moving a card into the same spot, don't do anything if ( entry.number === dragged.number && + entry.next === dragged.next && entry.picklist === dragged.picklist ) return; - const picklist = removeEntryFromItsPicklist(dragged); + removeEntryFromItsPicklist(dragged); // Create a copy, don't operate on the original dragged = { number: dragged.number, next: entry.next, picklist: entry.picklist, + id: dragged.id, }; entry.next = dragged; - picklist?.update(picklist); + console.log("Entry.next:", entry.next, entry.picklist?.head?.next); + console.log("Head equal:", entry === entry.picklist?.head); + console.log( + "Picklists equal:", + entry.picklist === entry.next?.picklist && + entry.picklist === picklist, + ); + + entry.picklist?.update(entry.picklist); }, collect: (monitor) => ({ isOverInsert: monitor.isOver(), @@ -170,7 +185,7 @@ function InsertDropSite({ ); } -function PicklistCard(props: { picklist: Picklist; picklists: Picklist[] }) { +function PicklistCard(props: { picklist: Picklist }) { const picklist = props.picklist; const [{ isOver, entry }, dropRef] = useDrop({ @@ -178,7 +193,12 @@ function PicklistCard(props: { picklist: Picklist; picklists: Picklist[] }) { drop: (item: PicklistEntry) => { removeEntryFromItsPicklist(item); - item = { number: item.number, next: picklist.head, picklist }; + item = { + number: item.number, + next: picklist.head, + picklist, + id: item.id, + }; picklist.head = item; picklist.update(picklist); @@ -195,15 +215,25 @@ function PicklistCard(props: { picklist: Picklist; picklists: Picklist[] }) { } return ( -
- -

- {picklist.name} -

+
+
+ +

+ {picklist.name} +

+ +
@@ -230,7 +260,6 @@ function PicklistCard(props: { picklist: Picklist; picklists: Picklist[] }) { @@ -329,6 +358,16 @@ export default function PicklistScreen(props: { [setPicklists], ); + const deletePicklist = useCallback( + (picklist: Picklist) => { + setPicklists((old) => { + const newPicklists = old.filter((p) => p.index !== picklist.index); + return newPicklists; + }); + }, + [setPicklists], + ); + const loadCompPicklistGroup = useCallback( (picklistDict: CompPicklistGroup) => { setPicklists( @@ -338,6 +377,7 @@ export default function PicklistScreen(props: { name, head: undefined, update: updatePicklist, + delete: () => deletePicklist(newPicklist), }; if (teams.length > 0) { @@ -345,6 +385,7 @@ export default function PicklistScreen(props: { number: teams[0], next: undefined, picklist: newPicklist, + id: new ObjectId().toHexString(), }; newPicklist.head = curr; @@ -353,6 +394,7 @@ export default function PicklistScreen(props: { number: teams[i], next: undefined, picklist: newPicklist, + id: new ObjectId().toHexString(), }; curr = curr.next; } @@ -392,6 +434,7 @@ export default function PicklistScreen(props: { name: `Picklist ${picklists.length + 1}`, head: undefined, update: updatePicklist, + delete: () => deletePicklist(newPicklist), }; const newPicklists = [...picklists, newPicklist]; @@ -413,7 +456,7 @@ export default function PicklistScreen(props: {
{loadingPicklists === LoadState.Loading ? (
@@ -430,7 +473,6 @@ export default function PicklistScreen(props: { )) )} From f418b787a98b1479b889130a5b015f1b75e67c3d Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Sun, 5 Jan 2025 22:20:07 -0500 Subject: [PATCH 3/9] Picklist ordering works, added strikethroughs --- components/stats/Picklist.tsx | 162 ++++++++++++++++++++++++++++------ lib/Types.ts | 1 + lib/api/ClientApi.ts | 2 +- 3 files changed, 138 insertions(+), 27 deletions(-) diff --git a/components/stats/Picklist.tsx b/components/stats/Picklist.tsx index af4b12d9..57caf93d 100644 --- a/components/stats/Picklist.tsx +++ b/components/stats/Picklist.tsx @@ -1,12 +1,13 @@ import { CompPicklistGroup, Report } from "@/lib/Types"; - import { ConnectDropTarget, useDrag, useDrop } from "react-dnd"; import { ChangeEvent, useCallback, useEffect, useState } from "react"; -import { FaPlus, FaTrash } from "react-icons/fa"; +import { FaPlus, FaStrikethrough, FaTrash } from "react-icons/fa"; import ClientApi from "@/lib/api/ClientApi"; import { ObjectId } from "bson"; +// Be sure to disable for production! const SHOW_PICKLISTS_ON_TEAM_CARDS = true; +const SHOW_CARD_IDS = true; type Picklist = { index: number; @@ -24,9 +25,10 @@ type PicklistEntry = { }; function removeEntryFromItsPicklist(entry: PicklistEntry) { + console.log("removing", entry); if (entry.picklist) { const picklist = entry.picklist; - if (picklist.head?.number === entry.number) { + if (picklist.head?.id && picklist.head?.id === entry.id) { picklist.head = picklist.head.next; } else { let curr: PicklistEntry | undefined = picklist.head; @@ -48,6 +50,17 @@ function removeEntryFromItsPicklist(entry: PicklistEntry) { } } +function getPicklistLength(picklist: Picklist) { + let length = 0; + let curr = picklist.head; + while (curr) { + length++; + curr = curr.next; + } + + return length; +} + function TeamCard(props: { entry: PicklistEntry; draggable: boolean; @@ -55,14 +68,27 @@ function TeamCard(props: { rank?: number; width?: string; preview?: boolean; + strikeThroughs: number[]; + toggleStrikethrough?: (team: number) => void; }) { - const { entry, width, rank, preview, picklist } = props; + const { + entry, + width, + rank, + preview, + picklist, + strikeThroughs, + toggleStrikethrough, + } = props; if (picklist) entry.picklist = picklist; const [, dragRef] = useDrag({ type: "team", item: () => { - return props.entry; + return { + ...props.entry, + id: props.entry.id ?? new ObjectId().toString(), + }; }, }); @@ -83,22 +109,14 @@ function TeamCard(props: { // Create a copy, don't operate on the original dragged = { + id: dragged.id, number: dragged.number, next: entry.next, picklist: entry.picklist, - id: dragged.id, }; entry.next = dragged; - console.log("Entry.next:", entry.next, entry.picklist?.head?.next); - console.log("Head equal:", entry === entry.picklist?.head); - console.log( - "Picklists equal:", - entry.picklist === entry.next?.picklist && - entry.picklist === picklist, - ); - entry.picklist?.update(entry.picklist); }, collect: (monitor) => ({ @@ -114,10 +132,19 @@ function TeamCard(props: { className={`w-${width ?? "full"} bg-base-100 rounded-lg p-1 flex items-center justify-center border-2 border-base-100 hover:border-primary ${preview && "animate-pulse"}`} ref={dragRef as unknown as () => void} > -

+

{rank !== undefined ? `${rank}. ` : ""} Team{" "} #{entry.number} + {SHOW_CARD_IDS && ( + + .{entry.id?.slice(entry.id.length - 3)} + + )} {SHOW_PICKLISTS_ON_TEAM_CARDS && entry.picklist && ( {" "} @@ -125,6 +152,14 @@ function TeamCard(props: { )}

+ {toggleStrikethrough && ( + + )}
{rank && !preview && ( )}
@@ -142,6 +178,8 @@ function TeamCard(props: { entry={entry.next} draggable={true} picklist={props.picklist} + strikeThroughs={strikeThroughs} + toggleStrikethrough={toggleStrikethrough} /> )} @@ -157,12 +195,14 @@ function InsertDropSite({ isOver, dropRef, draggedEntry, + strikeThroughs, }: { rank: number; entry: PicklistEntry; isOver: boolean; dropRef: ConnectDropTarget; draggedEntry: PicklistEntry; + strikeThroughs: number[]; }) { return (
) : ( @@ -185,10 +226,10 @@ function InsertDropSite({ ); } -function PicklistCard(props: { picklist: Picklist }) { +function PicklistCard(props: { picklist: Picklist; strikethroughs: number[] }) { const picklist = props.picklist; - const [{ isOver, entry }, dropRef] = useDrop({ + const [{ isOver: isOverHead, entry: headEntry }, headDropRef] = useDrop({ accept: "team", drop: (item: PicklistEntry) => { removeEntryFromItsPicklist(item); @@ -209,13 +250,39 @@ function PicklistCard(props: { picklist: Picklist }) { }), }); + const [{ isOver: isOverTail, entry: tailEntry }, tailDropRef] = useDrop({ + accept: "team", + drop: (item: PicklistEntry) => { + console.log("dropped", item); + removeEntryFromItsPicklist(item); + item.picklist = picklist; + + let tail = picklist.head; + if (!tail) { + picklist.head = item; + } else { + while (tail.next) { + tail = tail.next; + } + + tail.next = item; + } + + picklist.update(picklist); + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + entry: monitor.getItem(), + }), + }); + function changeName(e: ChangeEvent) { picklist.name = e.target.value; picklist.update(picklist); } return ( -
+
- {isOver ? ( + {isOverHead ? (
) : ( @@ -261,9 +329,26 @@ function PicklistCard(props: { picklist: Picklist }) { entry={picklist.head} draggable={false} picklist={picklist} - rank={isOver ? 2 : 1} + rank={isOverHead ? 2 : 1} + strikeThroughs={props.strikethroughs} /> )} +
+ {isOverTail && ( +
+ +
+ )} +
); @@ -273,6 +358,8 @@ export function TeamList(props: { teams: PicklistEntry[]; picklists: Picklist[]; expectedTeamCount: number; + strikethroughs: number[]; + toggleStrikethrough: (team: number) => void; }) { const [, dropRef] = useDrop({ accept: "team", @@ -291,6 +378,8 @@ export function TeamList(props: { draggable={true} entry={team} key={team.number} + strikeThroughs={props.strikethroughs} + toggleStrikethrough={props.toggleStrikethrough} /> ))} {props.teams.length !== props.expectedTeamCount && ( @@ -319,6 +408,8 @@ export default function PicklistScreen(props: { const [loadingPicklists, setLoadingPicklists] = useState(LoadState.NotLoaded); + const [strikethroughs, setStrikethroughs] = useState([]); + const teams = props.teams.map((team) => ({ number: team })); useEffect(() => { @@ -335,6 +426,7 @@ export default function PicklistScreen(props: { { _id: props.picklist._id, picklists: {}, + strikethroughs, }, ); @@ -370,6 +462,8 @@ export default function PicklistScreen(props: { const loadCompPicklistGroup = useCallback( (picklistDict: CompPicklistGroup) => { + setStrikethroughs(picklistDict.strikethroughs); + setPicklists( Object.entries(picklistDict.picklists).map(([name, teams], index) => { const newPicklist: Picklist = { @@ -382,10 +476,10 @@ export default function PicklistScreen(props: { if (teams.length > 0) { let curr: PicklistEntry = { + id: new ObjectId().toString(), number: teams[0], next: undefined, picklist: newPicklist, - id: new ObjectId().toHexString(), }; newPicklist.head = curr; @@ -394,7 +488,7 @@ export default function PicklistScreen(props: { number: teams[i], next: undefined, picklist: newPicklist, - id: new ObjectId().toHexString(), + id: new ObjectId().toString(), }; curr = curr.next; } @@ -407,11 +501,24 @@ export default function PicklistScreen(props: { [updatePicklist], ); + const toggleStrikethrough = useCallback( + (team: number) => { + setStrikethroughs((old) => { + if (old.includes(team)) { + return old.filter((t) => t !== team); + } else { + return [...old, team]; + } + }); + }, + [setStrikethroughs], + ); + useEffect(() => { if (loadingPicklists !== LoadState.NotLoaded) return; setLoadingPicklists(LoadState.Loading); - api.getPicklist(props.picklist?._id).then((picklist) => { + api.getPicklistGroup(props.picklist?._id).then((picklist) => { if (picklist) { loadCompPicklistGroup(picklist); } @@ -452,11 +559,13 @@ export default function PicklistScreen(props: { teams={teams} picklists={picklists} expectedTeamCount={props.expectedTeamCount} + strikethroughs={strikethroughs} + toggleStrikethrough={toggleStrikethrough} />
{loadingPicklists === LoadState.Loading ? (
@@ -473,6 +582,7 @@ export default function PicklistScreen(props: { )) )} diff --git a/lib/Types.ts b/lib/Types.ts index f8f2e2db..99b263b6 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -564,6 +564,7 @@ export type CompPicklistGroup = { picklists: { [name: string]: number[]; }; + strikethroughs: number[]; }; /** diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts index 6f8a936a..822218d4 100644 --- a/lib/api/ClientApi.ts +++ b/lib/api/ClientApi.ts @@ -1253,7 +1253,7 @@ export default class ClientApi extends NextApiTemplate { }, }); - getPicklist = createNextRoute< + getPicklistGroup = createNextRoute< [string], CompPicklistGroup | undefined, ApiDependencies, From 70eaf61cfab9e042c265a3b7582489a29e1cf643 Mon Sep 17 00:00:00 2001 From: renatodellosso Date: Mon, 6 Jan 2025 09:58:03 -0500 Subject: [PATCH 4/9] Styling and final functionality for picklists --- components/stats/Picklist.tsx | 36 ++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/components/stats/Picklist.tsx b/components/stats/Picklist.tsx index 57caf93d..7f57bb84 100644 --- a/components/stats/Picklist.tsx +++ b/components/stats/Picklist.tsx @@ -129,7 +129,7 @@ function TeamCard(props: { <>
void} >

+
-
+
- {isOverHead ? ( + {picklist.head && isOverHead && (
- ) : ( - !picklist.head && ( -

Drop Here!

- ) )}
{picklist.head && ( @@ -335,9 +330,9 @@ function PicklistCard(props: { picklist: Picklist; strikethroughs: number[] }) { )}
- {isOverTail && ( + {isOverTail ? (
+ ) : ( + !picklist.head && ( +

+ Drop Here! +

+ ) )}
@@ -412,6 +413,7 @@ export default function PicklistScreen(props: { const teams = props.teams.map((team) => ({ number: team })); + // Save picklists useEffect(() => { const picklistDict = picklists.reduce( (acc, picklist) => { @@ -514,6 +516,7 @@ export default function PicklistScreen(props: { [setStrikethroughs], ); + // Load picklists useEffect(() => { if (loadingPicklists !== LoadState.NotLoaded) return; @@ -550,7 +553,10 @@ export default function PicklistScreen(props: { const [, dropRef] = useDrop({ accept: "team", - drop: removeEntryFromItsPicklist, + drop: (item: PicklistEntry, monitor) => { + if (monitor.didDrop()) return; // Check if another drop target handled the drop + removeEntryFromItsPicklist(item); + }, }); return ( @@ -590,7 +596,7 @@ export default function PicklistScreen(props: { {loadingPicklists !== LoadState.Loading && (