From 14f9d4b074e713e8a2db40585c18f82138810119 Mon Sep 17 00:00:00 2001 From: Pratyush Venkatakrishnan Date: Thu, 22 Jan 2026 18:03:54 -0500 Subject: [PATCH 01/89] Add RawPEClass for PE classes --- pe/s26-h1pe-orig.csv | 69 +++++++++++++++++++++++++++++++++++++++++++ pe/s26-h1pe.csv | 69 +++++++++++++++++++++++++++++++++++++++++++ src/lib/rawPEClass.ts | 34 +++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 pe/s26-h1pe-orig.csv create mode 100644 pe/s26-h1pe.csv create mode 100644 src/lib/rawPEClass.ts diff --git a/pe/s26-h1pe-orig.csv b/pe/s26-h1pe-orig.csv new file mode 100644 index 00000000..cf10e7ec --- /dev/null +++ b/pe/s26-h1pe-orig.csv @@ -0,0 +1,69 @@ +Term,Section,Title,Capacity,Day,Time,Location,Start Date,End Date,Prerequisites,Equipment,GIR Points,Swim GIR,Fee Amount +2026Q3,PE.0201-1,SCUBA Diving,18,T,6:45 PM,Alumni Wang Pool and 66-160,2/24/2026,4/14/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p. Attendance is required on the first and last day.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 +2026Q3,PE.0201-2,SCUBA Diving,18,R,6:45 PM,Alumni Wang Pool and 66-160,2/19/2026,4/9/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p. Attendance is required on the first and last day.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 +2026Q3,PE.0202-1,"Swimming, Beginner",10,MW,11:00 AM,Zesiger Teaching Pool,2/11/2026,3/18/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-2,"Swimming, Beginner",10,MW,1:00 PM,Zesiger Teaching Pool,2/11/2026,3/18/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-3,"Swimming, Beginner",10,MW,2:00 PM,Zesiger Teaching Pool,2/11/2026,3/18/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-4,"Swimming, Beginner",10,TR,11:00 AM,Zesiger Teaching Pool,2/10/2026,3/19/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-5,"Swimming, Beginner",10,TR,1:00 PM,Zesiger Teaching Pool,2/10/2026,3/19/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-6,"Swimming, Beginner",10,TR,2:00 PM,Zesiger Teaching Pool,2/10/2026,3/19/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0300-1,Ballroom,20,TR,7:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0403-1,Group Exercise - Cardio Kickboxing,20,MW,6:00 PM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,"Workout clothes, footwear and water bottle",2,N,$0.00 +2026Q3,PE.0405-1,Group Exercise - Pilates,20,TR,3:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0411-1,Group Exercise- Yoga,20,MW,8:00 AM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0411-2,Group Exercise- Yoga,20,MW,5:00 PM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0411-3,Group Exercise- Yoga,20,TR,5:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0414-1,Weight Training,16,MW,11:00 AM,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-2,Weight Training,16,MW,1:00 PM,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-3,Weight Training,16,MW,2:00 PM,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-4,Weight Training,16,TR,11:00 AM,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-5,Weight Training,16,TR,2:00 PM,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0432-1,Group Exercise- Barre Fitness,15,TR,2:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0435-1,Group Exercise- Functional Fitness,20,MW,3:00 PM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0444-1,Group Exercise- HIIT,20,TR,6:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0517-1,Fitness (Yoga)/CPR/First Aid,12,MW,4:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,"Q3 2026: Students must complete the remote asynchronous CPR content and in-person CPR and FA exam sessions to become CPR/First Aid certified and students must be able to kneel and use 2 arms to give compressions. In person class starts Tue, Feb. 17 (switch day)",Workout clothes and water bottle. Lab fee covers pocket mask and CPR and first aid certification cards,2,N,$60.00 +2026Q3,PE.0518-1,Fitness (Yoga)/Meditation,16,TR,6:00 PM,Du Pont Multi-Purpose Room,2/10/2026,3/19/2026,None,Workout clothes and a filled water bottle.,2,N,$0.00 +2026Q3,PE.0529-1,Fitness(Yoga)/Meditation (remote synchronous),15,MW,7:00 PM,Remote Synchronous,2/11/2026,3/18/2026,"This remote synchronous course requires internet access, computer (or tablet, mobile device) with a camera, microphone, and working speaker, MIT Zoom account, roughly 6 foot x 6 foot physical area clear of any objects with a standard 7 - 8 foot ceiling and non-slip floor to do physical activity, comfortable with using 'camera on' function during Zoom sessions. ","Workout clothing and filled water bottle, mat or towel. See other prerequisites.",2,N,$0.00 +2026Q3,PE.0544-1,Fitness(Strength Circuit)/Nutrition,16,MW,5:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,None,"Sneakers/footwear, comfortable workout clothing and water bottle. +",2,N,$0.00 +2026Q3,PE.0545-1,Fitness(Strength Circuit)/Resiliency,16,MW,3:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,None,Workout clothes and filled water bottle.,2,N,$0.00 +2026Q3,PE.0546-1,Fitness(Strength Circuit)/Stress Management,16,MW,6:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,None,"Sneakers/footwear, comfortable workout clothing and water bottle.",2,N,$0.00 +2026Q3,PE.0600-1,Archery,14,TR,10:00 AM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-2,Archery,14,TR,11:00 AM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-3,Archery,14,TR,1:00 PM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-4,Archery,14,TR,2:00 PM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-5,Archery,14,MW,1:00 PM,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-6,Archery,14,MW,2:00 PM,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0601-1,Badminton,16,TR,1:00 PM,Rockwell Cage South,2/10/2026,3/19/2026,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0601-2,Badminton,16,TR,2:00 PM,Rockwell Cage South,2/10/2026,3/19/2026,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0603-1,"Fencing, Sabre",16,TR,3:00 PM,Du Pont Fencing Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,Workout clothes,2,N,$15.00 +2026Q3,PE.0608-1,Pistol,14,TR,1:00 PM,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. +","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0608-2,Pistol,14,TR,2:00 PM,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. +","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0609-1,"Pistol, Intermediate",14,TR,11:00 AM,Du Pont Pistol Range,2/10/2026,3/19/2026,"Student must have successfully completed the MIT PEandW Beginner Pistol Course. Note: Student must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor. + +",2,N,$35.00 +2026Q3,PE.0612-1,"Skate, Beginner",20,MW,1:00 PM,Johnson Ice Rink 1,2/11/2026,3/18/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-2,"Skate, Beginner",20,TR,11:00 AM,Johnson Ice Rink 1,2/10/2026,3/19/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-3,"Skate, Beginner",20,TR,1:00 PM,Johnson Ice Rink 1,2/10/2026,3/19/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-4,"Skate, Beginner",20,TR,2:00 PM,Johnson Ice Rink 1,2/10/2026,3/19/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-1,"Skate, Intermediate",15,MW,1:00 PM,Johnson Ice Rink 2,2/11/2026,3/18/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-2,"Skate, Intermediate",15,TR,11:00 AM,Johnson Ice Rink 2,2/10/2026,3/19/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-3,"Skate, Intermediate",15,TR,1:00 PM,Johnson Ice Rink 2,2/10/2026,3/19/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-4,"Skate, Intermediate",15,TR,2:00 PM,Johnson Ice Rink 2,2/10/2026,3/19/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0616-1,Squash,12,MW,11:00 AM,Zesiger Squash Courts,2/11/2026,3/18/2026,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0616-2,Squash,12,TR,1:00 PM,Zesiger Squash Courts,2/10/2026,3/19/2026,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0616-3,Squash,12,TR,2:00 PM,Zesiger Squash Courts,2/10/2026,3/19/2026,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0617-1,"Squash, Intermediate",12,TR,11:00 AM,Zesiger Squash Courts,2/10/2026,3/19/2026,"Completion of Beginner Squash class or had experience in high school or club. Please email instructor at bbubna@mit.edu if you are not sure regarding your ability or if you have any questions +","Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. Racquet, ball and eye protection are provided.",2,N,$10.00 +2026Q3,PE.0626-1,Rifle,14,MW,11:00 AM,Du Pont Pistol Range,2/11/2026,3/18/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0626-2,Rifle,14,MW,1:00 PM,Du Pont Pistol Range,2/11/2026,3/18/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0626-3,Rifle,14,MW,2:00 PM,Du Pont Pistol Range,2/11/2026,3/18/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0636-1,Self-Defense for Women,20,MW,1:00 PM,Du Pont Wrestling Room,2/11/2026,3/18/2026,This is an all female course.,None,2,N,$0.00 +2026Q3,PE.0646-1,Pickleball,16,MW,11:00 AM,Rockwell Cage South,2/11/2026,3/18/2026,None,"Work out clothes, footwear, and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0657-1,Spec Tennis,16,MW,2:00 PM,Rockwell Cage South,2/11/2026,3/18/2026,None,Comfortable clothing and footwear.,2,N,$10.00 +2026Q3,PE.0701-1,Ice Hockey,20,MW,2:00 PM,Johnson Ice Rink,2/11/2026,3/18/2026,This course requires a command of forward and backward skating as well as a strong consistent stop that can be learned in beginner skate or equivalent (email instructor using physicaleducationandwellness@mit.edu address if you have questions related to your ability).,"Ice hockey skates, helmet, shin guards, gloves and hockey stick provided at rink. ",2,N,$20.00 +2026Q3,PE.0703-1,"Soccer, Beginner",15,MW,11:00 AM,Zesiger MAC Court,2/11/2026,3/18/2026,This course will be held indoors.,Court shoes recommended for indoor play. Workout clothes. ,2,N,$0.00 +2026Q3,PE.0800-1,Aikido,20,TR,1:00 PM,Du Pont Wrestling Room,2/10/2026,3/19/2026,None,Workout clothes,2,N,$0.00 +2026Q3,PE.0922-1,"Parkour, Beginner",16,F,1:15 PM,Zesiger MAC Court,2/13/2026,3/20/2026,"2/13, 2/20, 2/27, 3/6, 3/13, 3/20**(** Ends in Q4). Time: 1:15p-2:45p Registration is pending until all forms sent from PE&W office have been completed by Mon, 2/9 by 5p. Forms will be sent from the PE&W office using the student's MIT email by the close of online registration. Check SPAM folders if emails are being forwarded from an MIT email account.",Workout clothes. Court shoes recommended.,2,N,$75.00 diff --git a/pe/s26-h1pe.csv b/pe/s26-h1pe.csv new file mode 100644 index 00000000..94036bd8 --- /dev/null +++ b/pe/s26-h1pe.csv @@ -0,0 +1,69 @@ +Term,Section,Title,Capacity,Day,Start time,End time,Location,Start Date,End Date,Description,Prerequisites,Equipment,GIR Points,Swim GIR,Fee Amount +2026Q3,PE.0201-1,SCUBA Diving,18,T,6:45 PM,,Alumni Wang Pool and 66-160,2/24/2026,4/14/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. Attendance is required on the first and last day.","All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 +2026Q3,PE.0201-2,SCUBA Diving,18,R,6:45 PM,,Alumni Wang Pool and 66-160,2/19/2026,4/9/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. Attendance is required on the first and last day.","All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 +2026Q3,PE.0202-1,"Swimming, Beginner",10,MW,11:00 AM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-2,"Swimming, Beginner",10,MW,1:00 PM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-3,"Swimming, Beginner",10,MW,2:00 PM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-4,"Swimming, Beginner",10,TR,11:00 AM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-5,"Swimming, Beginner",10,TR,1:00 PM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-6,"Swimming, Beginner",10,TR,2:00 PM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0300-1,Ballroom,20,TR,7:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0403-1,Group Exercise - Cardio Kickboxing,20,MW,6:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,"Workout clothes, footwear and water bottle",2,N,$0.00 +2026Q3,PE.0405-1,Group Exercise - Pilates,20,TR,3:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0411-1,Group Exercise- Yoga,20,MW,8:00 AM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0411-2,Group Exercise- Yoga,20,MW,5:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0411-3,Group Exercise- Yoga,20,TR,5:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0414-1,Weight Training,16,MW,11:00 AM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-2,Weight Training,16,MW,1:00 PM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-3,Weight Training,16,MW,2:00 PM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-4,Weight Training,16,TR,11:00 AM,,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-5,Weight Training,16,TR,2:00 PM,,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0432-1,Group Exercise- Barre Fitness,15,TR,2:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0435-1,Group Exercise- Functional Fitness,20,MW,3:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0444-1,Group Exercise- HIIT,20,TR,6:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0517-1,Fitness (Yoga)/CPR/First Aid,12,MW,4:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,"In person class starts Tue, Feb. 17 (switch day)",Q3 2026: Students must complete the remote asynchronous CPR content and in-person CPR and FA exam sessions to become CPR/First Aid certified and students must be able to kneel and use 2 arms to give compressions.,Workout clothes and water bottle. Lab fee covers pocket mask and CPR and first aid certification cards,2,N,$60.00 +2026Q3,PE.0518-1,Fitness (Yoga)/Meditation,16,TR,6:00 PM,,Du Pont Multi-Purpose Room,2/10/2026,3/19/2026,,None,Workout clothes and a filled water bottle.,2,N,$0.00 +2026Q3,PE.0529-1,Fitness(Yoga)/Meditation (remote synchronous),15,MW,7:00 PM,,Remote Synchronous,2/11/2026,3/18/2026,,"This remote synchronous course requires internet access, computer (or tablet, mobile device) with a camera, microphone, and working speaker, MIT Zoom account, roughly 6 foot x 6 foot physical area clear of any objects with a standard 7 - 8 foot ceiling and non-slip floor to do physical activity, comfortable with using 'camera on' function during Zoom sessions. ","Workout clothing and filled water bottle, mat or towel. See other prerequisites.",2,N,$0.00 +2026Q3,PE.0544-1,Fitness(Strength Circuit)/Nutrition,16,MW,5:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,"Sneakers/footwear, comfortable workout clothing and water bottle. +",2,N,$0.00 +2026Q3,PE.0545-1,Fitness(Strength Circuit)/Resiliency,16,MW,3:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,Workout clothes and filled water bottle.,2,N,$0.00 +2026Q3,PE.0546-1,Fitness(Strength Circuit)/Stress Management,16,MW,6:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,"Sneakers/footwear, comfortable workout clothing and water bottle.",2,N,$0.00 +2026Q3,PE.0600-1,Archery,14,TR,10:00 AM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-2,Archery,14,TR,11:00 AM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-3,Archery,14,TR,1:00 PM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-4,Archery,14,TR,2:00 PM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-5,Archery,14,MW,1:00 PM,,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-6,Archery,14,MW,2:00 PM,,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0601-1,Badminton,16,TR,1:00 PM,,Rockwell Cage South,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0601-2,Badminton,16,TR,2:00 PM,,Rockwell Cage South,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0603-1,"Fencing, Sabre",16,TR,3:00 PM,,Du Pont Fencing Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,Workout clothes,2,N,$15.00 +2026Q3,PE.0608-1,Pistol,14,TR,1:00 PM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. +",,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0608-2,Pistol,14,TR,2:00 PM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. +",,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0609-1,"Pistol, Intermediate",14,TR,11:00 AM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Student must attend first 4 classes, though attendance at all classes is strongly recommended.",Student must have successfully completed the MIT PEandW Beginner Pistol Course.,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor. + +",2,N,$35.00 +2026Q3,PE.0612-1,"Skate, Beginner",20,MW,1:00 PM,,Johnson Ice Rink 1,2/11/2026,3/18/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-2,"Skate, Beginner",20,TR,11:00 AM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-3,"Skate, Beginner",20,TR,1:00 PM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-4,"Skate, Beginner",20,TR,2:00 PM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-1,"Skate, Intermediate",15,MW,1:00 PM,,Johnson Ice Rink 2,2/11/2026,3/18/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-2,"Skate, Intermediate",15,TR,11:00 AM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-3,"Skate, Intermediate",15,TR,1:00 PM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-4,"Skate, Intermediate",15,TR,2:00 PM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0616-1,Squash,12,MW,11:00 AM,,Zesiger Squash Courts,2/11/2026,3/18/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0616-2,Squash,12,TR,1:00 PM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0616-3,Squash,12,TR,2:00 PM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0617-1,"Squash, Intermediate",12,TR,11:00 AM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,"Completion of Beginner Squash class or had experience in high school or club. Please email instructor at bbubna@mit.edu if you are not sure regarding your ability or if you have any questions +","Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. Racquet, ball and eye protection are provided.",2,N,$10.00 +2026Q3,PE.0626-1,Rifle,14,MW,11:00 AM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0626-2,Rifle,14,MW,1:00 PM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0626-3,Rifle,14,MW,2:00 PM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0636-1,Self-Defense for Women,20,MW,1:00 PM,,Du Pont Wrestling Room,2/11/2026,3/18/2026,,This is an all female course.,None,2,N,$0.00 +2026Q3,PE.0646-1,Pickleball,16,MW,11:00 AM,,Rockwell Cage South,2/11/2026,3/18/2026,,None,"Work out clothes, footwear, and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0657-1,Spec Tennis,16,MW,2:00 PM,,Rockwell Cage South,2/11/2026,3/18/2026,,None,Comfortable clothing and footwear.,2,N,$10.00 +2026Q3,PE.0701-1,Ice Hockey,20,MW,2:00 PM,,Johnson Ice Rink,2/11/2026,3/18/2026,,This course requires a command of forward and backward skating as well as a strong consistent stop that can be learned in beginner skate or equivalent (email instructor using physicaleducationandwellness@mit.edu address if you have questions related to your ability).,"Ice hockey skates, helmet, shin guards, gloves and hockey stick provided at rink. ",2,N,$20.00 +2026Q3,PE.0703-1,"Soccer, Beginner",15,MW,11:00 AM,,Zesiger MAC Court,2/11/2026,3/18/2026,,This course will be held indoors.,Court shoes recommended for indoor play. Workout clothes. ,2,N,$0.00 +2026Q3,PE.0800-1,Aikido,20,TR,1:00 PM,,Du Pont Wrestling Room,2/10/2026,3/19/2026,,None,Workout clothes,2,N,$0.00 +2026Q3,PE.0922-1,"Parkour, Beginner",16,F,1:15 PM,2:45 PM,Zesiger MAC Court,2/13/2026,3/20/2026,"2/13, 2/20, 2/27, 3/6, 3/13, 3/20**(** Ends in Q4). Registration is pending until all forms sent from PE&W office have been completed by Mon, 2/9 by 5p. Forms will be sent from the PE&W office using the student's MIT email by the close of online registration. Check SPAM folders if emails are being forwarded from an MIT email account.",,Workout clothes. Court shoes recommended.,2,N,$75.00 diff --git a/src/lib/rawPEClass.ts b/src/lib/rawPEClass.ts new file mode 100644 index 00000000..d36316a3 --- /dev/null +++ b/src/lib/rawPEClass.ts @@ -0,0 +1,34 @@ +import { type RawSection } from "./rawClass"; + +export interface RawPEClass { + /** Class number; e.g., "PE.0612" */ + number: string; + /** Class name; e.g., "Skate, Beginner" */ + name: string; + + /** Timeslots and locations for each section */ + sections: RawSection[]; + /** Raw (FireRoad format) section locations/times */ + rawSections: string[]; + /** Capacity per section */ + capacity: number; + + /** Start date, in ISO 8601 format */ + startDate: string; + /** End date, in ISO 8601 format */ + endDate: string; + + /** PE points */ + points: number; + /** Satisfies swim GIR */ + swimGIR: boolean; + + /** Prereqs, no specific format */ + prereqs: string; + /** Equipment, no specific format */ + equipment: string; + /** Fee, in dollars */ + fee: number; + /** Description, no specific format */ + description: string; +} From 1786fe9bff1b3ff3a54ff83589f26a4ed8db1551 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:41:06 -0500 Subject: [PATCH 02/89] add tabs for different class types --- src/components/ClassTypes.tsx | 75 ++++++++++++++++++++++++++++++ src/lib/schema.ts | 7 +++ src/lib/state.ts | 14 +++++- src/routes/_index.tsx | 86 ++++++++++++++++------------------- 4 files changed, 135 insertions(+), 47 deletions(-) create mode 100644 src/components/ClassTypes.tsx diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx new file mode 100644 index 00000000..e61fa1ea --- /dev/null +++ b/src/components/ClassTypes.tsx @@ -0,0 +1,75 @@ +import { Button, ButtonGroup, Center, Stack } from "@chakra-ui/react"; +import { ActivityDescription } from "./ActivityDescription"; +import { ClassTable } from "./ClassTable"; +import { MatrixLink } from "./MatrixLink"; +import { PreregLink } from "./PreregLink"; +import { SelectedActivities } from "./SelectedActivities"; +import { Tooltip } from "./ui/tooltip"; +import { useContext, useState } from "react"; +import { HydrantContext } from "~/lib/hydrant"; +import { useICSExport } from "~/lib/gapi"; +import { ClassType } from "~/lib/schema"; +import { + LuCalendarArrowDown, + LuGraduationCap, + LuVolleyball, +} from "react-icons/lu"; +import type { IconType } from "react-icons/lib"; + +export const Academic = () => { + const { state } = useContext(HydrantContext); + + const [isExporting, setIsExporting] = useState(false); + // TODO: fix gcal export + const onICSExport = useICSExport( + state, + () => { + setIsExporting(false); + }, + () => { + setIsExporting(false); + }, + ); + return ( + +
+ + + + + + + +
+ + + +
+ ); +}; + +export const PEandW = () => { + return <>; +}; + +// eslint-disable-next-line react-refresh/only-export-components +export const CLASS_TYPE_COMPONENTS: Record< + ClassType, + [IconType, React.ComponentType] +> = { + [ClassType.ACADEMIC]: [LuGraduationCap, Academic], + [ClassType.PEW]: [LuVolleyball, PEandW], +}; diff --git a/src/lib/schema.ts b/src/lib/schema.ts index f698381c..0455dea7 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -1,6 +1,11 @@ import type { Activity } from "./activity"; import type { ColorScheme } from "./colors"; +export enum ClassType { + ACADEMIC = "Academic", + PEW = "PE & Wellness", +} + /** The date the content of the banner was last changed. */ export const BANNER_LAST_CHANGED = new Date("2025-11-24T17:15:00Z").valueOf(); @@ -42,6 +47,7 @@ export interface HydrantState { saveId: string; saves: Save[]; preferences: Preferences; + classType: ClassType; } /** Default React state. */ @@ -56,4 +62,5 @@ export const DEFAULT_STATE: HydrantState = { saveId: "", saves: [], preferences: DEFAULT_PREFERENCES, + classType: ClassType.ACADEMIC, }; diff --git a/src/lib/state.ts b/src/lib/state.ts index 10911642..b50e7667 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -12,7 +12,7 @@ import type { RawClass, RawTimeslot } from "./rawClass"; import { Store } from "./store"; import { sum, urldecode, urlencode } from "./utils"; import type { HydrantState, Preferences, Save } from "./schema"; -import { BANNER_LAST_CHANGED, DEFAULT_PREFERENCES } from "./schema"; +import { BANNER_LAST_CHANGED, DEFAULT_PREFERENCES, ClassType } from "./schema"; /** * Global State object. Maintains global program state (selected classes, @@ -53,6 +53,8 @@ export class State { private preferences: Preferences = DEFAULT_PREFERENCES; /** Set of starred class numbers */ private starredClasses = new Set(); + /** Current class type for UI */ + private classType = ClassType.ACADEMIC; /** React callback to update state. */ callback: ((state: HydrantState) => void) | undefined; @@ -224,6 +226,7 @@ export class State { saveId: this.saveId, saves: this.saves, preferences: this.preferences, + classType: this.classType, }); if (save) { this.storeSave(this.saveId, false); @@ -318,6 +321,15 @@ export class State { this.updateState(); } + get currentClassType(): ClassType { + return this.classType; + } + + set currentClassType(classType: ClassType) { + this.classType = classType; + this.updateState(); + } + //======================================================================== // Loading and saving diff --git a/src/routes/_index.tsx b/src/routes/_index.tsx index 9dc2135c..55cc5229 100644 --- a/src/routes/_index.tsx +++ b/src/routes/_index.tsx @@ -1,29 +1,22 @@ -import { useState, useContext } from "react"; - -import { Center, Flex, Group, Button, ButtonGroup } from "@chakra-ui/react"; -import { Tooltip } from "../components/ui/tooltip"; -import { ActivityDescription } from "../components/ActivityDescription"; +import { Center, Flex, Group, Tabs } from "@chakra-ui/react"; import { Calendar } from "../components/Calendar"; -import { ClassTable } from "../components/ClassTable"; import { LeftFooter } from "../components/Footers"; import { Header, PreferencesDialog } from "../components/Header"; import { ScheduleOption } from "../components/ScheduleOption"; import { ScheduleSwitcher } from "../components/ScheduleSwitcher"; -import { SelectedActivities } from "../components/SelectedActivities"; import { TermSwitcher } from "../components/TermSwitcher"; import { Banner } from "../components/Banner"; -import { MatrixLink } from "../components/MatrixLink"; -import { PreregLink } from "../components/PreregLink"; -import { LuCalendarArrowDown } from "react-icons/lu"; +import { CLASS_TYPE_COMPONENTS } from "~/components/ClassTypes"; import { State } from "../lib/state"; import { Term } from "../lib/dates"; -import { useICSExport } from "../lib/gapi"; import type { SemesterData } from "../lib/hydrant"; import { useHydrant, HydrantContext, fetchNoCache } from "../lib/hydrant"; import { getClosestUrlName, type LatestTermInfo } from "../lib/dates"; import type { Route } from "./+types/_index"; +import { useContext } from "react"; +import type { ClassType } from "~/lib/schema"; // eslint-disable-next-line react-refresh/only-export-components export async function clientLoader({ request }: Route.ClientLoaderArgs) { @@ -72,18 +65,6 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) { function HydrantApp() { const { state } = useContext(HydrantContext); - const [isExporting, setIsExporting] = useState(false); - // TODO: fix gcal export - const onICSExport = useICSExport( - state, - () => { - setIsExporting(false); - }, - () => { - setIsExporting(false); - }, - ); - return ( <> @@ -104,31 +85,44 @@ function HydrantApp() { -
- - - - - - - -
- - - + + + ), + )} + From 9c66bf1ff87e99f083082fe9bf9749377cf9f044 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:30:40 -0500 Subject: [PATCH 03/89] add pe parsing code --- .gitignore | 1 + pe/s26-h1pe-orig.csv | 69 ------ scrapers/pe.py | 347 +++++++++++++++++++++++++++++++ {pe => scrapers/pe}/s26-h1pe.csv | 124 +++++------ 4 files changed, 410 insertions(+), 131 deletions(-) delete mode 100644 pe/s26-h1pe-orig.csv create mode 100644 scrapers/pe.py rename {pe => scrapers/pe}/s26-h1pe.csv (86%) diff --git a/.gitignore b/.gitignore index 362b7e16..7f893e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ scrapers/catalog.json scrapers/fireroad-sem.json scrapers/fireroad-presem.json scrapers/cim.json +scrapers/pe.json public/latest.json public/i26.json diff --git a/pe/s26-h1pe-orig.csv b/pe/s26-h1pe-orig.csv deleted file mode 100644 index cf10e7ec..00000000 --- a/pe/s26-h1pe-orig.csv +++ /dev/null @@ -1,69 +0,0 @@ -Term,Section,Title,Capacity,Day,Time,Location,Start Date,End Date,Prerequisites,Equipment,GIR Points,Swim GIR,Fee Amount -2026Q3,PE.0201-1,SCUBA Diving,18,T,6:45 PM,Alumni Wang Pool and 66-160,2/24/2026,4/14/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p. Attendance is required on the first and last day.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 -2026Q3,PE.0201-2,SCUBA Diving,18,R,6:45 PM,Alumni Wang Pool and 66-160,2/19/2026,4/9/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p. Attendance is required on the first and last day.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 -2026Q3,PE.0202-1,"Swimming, Beginner",10,MW,11:00 AM,Zesiger Teaching Pool,2/11/2026,3/18/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-2,"Swimming, Beginner",10,MW,1:00 PM,Zesiger Teaching Pool,2/11/2026,3/18/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-3,"Swimming, Beginner",10,MW,2:00 PM,Zesiger Teaching Pool,2/11/2026,3/18/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-4,"Swimming, Beginner",10,TR,11:00 AM,Zesiger Teaching Pool,2/10/2026,3/19/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-5,"Swimming, Beginner",10,TR,1:00 PM,Zesiger Teaching Pool,2/10/2026,3/19/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-6,"Swimming, Beginner",10,TR,2:00 PM,Zesiger Teaching Pool,2/10/2026,3/19/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0300-1,Ballroom,20,TR,7:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0403-1,Group Exercise - Cardio Kickboxing,20,MW,6:00 PM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,"Workout clothes, footwear and water bottle",2,N,$0.00 -2026Q3,PE.0405-1,Group Exercise - Pilates,20,TR,3:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0411-1,Group Exercise- Yoga,20,MW,8:00 AM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0411-2,Group Exercise- Yoga,20,MW,5:00 PM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0411-3,Group Exercise- Yoga,20,TR,5:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0414-1,Weight Training,16,MW,11:00 AM,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-2,Weight Training,16,MW,1:00 PM,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-3,Weight Training,16,MW,2:00 PM,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-4,Weight Training,16,TR,11:00 AM,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-5,Weight Training,16,TR,2:00 PM,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0432-1,Group Exercise- Barre Fitness,15,TR,2:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0435-1,Group Exercise- Functional Fitness,20,MW,3:00 PM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0444-1,Group Exercise- HIIT,20,TR,6:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0517-1,Fitness (Yoga)/CPR/First Aid,12,MW,4:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,"Q3 2026: Students must complete the remote asynchronous CPR content and in-person CPR and FA exam sessions to become CPR/First Aid certified and students must be able to kneel and use 2 arms to give compressions. In person class starts Tue, Feb. 17 (switch day)",Workout clothes and water bottle. Lab fee covers pocket mask and CPR and first aid certification cards,2,N,$60.00 -2026Q3,PE.0518-1,Fitness (Yoga)/Meditation,16,TR,6:00 PM,Du Pont Multi-Purpose Room,2/10/2026,3/19/2026,None,Workout clothes and a filled water bottle.,2,N,$0.00 -2026Q3,PE.0529-1,Fitness(Yoga)/Meditation (remote synchronous),15,MW,7:00 PM,Remote Synchronous,2/11/2026,3/18/2026,"This remote synchronous course requires internet access, computer (or tablet, mobile device) with a camera, microphone, and working speaker, MIT Zoom account, roughly 6 foot x 6 foot physical area clear of any objects with a standard 7 - 8 foot ceiling and non-slip floor to do physical activity, comfortable with using 'camera on' function during Zoom sessions. ","Workout clothing and filled water bottle, mat or towel. See other prerequisites.",2,N,$0.00 -2026Q3,PE.0544-1,Fitness(Strength Circuit)/Nutrition,16,MW,5:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,None,"Sneakers/footwear, comfortable workout clothing and water bottle. -",2,N,$0.00 -2026Q3,PE.0545-1,Fitness(Strength Circuit)/Resiliency,16,MW,3:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,None,Workout clothes and filled water bottle.,2,N,$0.00 -2026Q3,PE.0546-1,Fitness(Strength Circuit)/Stress Management,16,MW,6:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,None,"Sneakers/footwear, comfortable workout clothing and water bottle.",2,N,$0.00 -2026Q3,PE.0600-1,Archery,14,TR,10:00 AM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-2,Archery,14,TR,11:00 AM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-3,Archery,14,TR,1:00 PM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-4,Archery,14,TR,2:00 PM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-5,Archery,14,MW,1:00 PM,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-6,Archery,14,MW,2:00 PM,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0601-1,Badminton,16,TR,1:00 PM,Rockwell Cage South,2/10/2026,3/19/2026,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0601-2,Badminton,16,TR,2:00 PM,Rockwell Cage South,2/10/2026,3/19/2026,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0603-1,"Fencing, Sabre",16,TR,3:00 PM,Du Pont Fencing Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,Workout clothes,2,N,$15.00 -2026Q3,PE.0608-1,Pistol,14,TR,1:00 PM,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. -","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0608-2,Pistol,14,TR,2:00 PM,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. -","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0609-1,"Pistol, Intermediate",14,TR,11:00 AM,Du Pont Pistol Range,2/10/2026,3/19/2026,"Student must have successfully completed the MIT PEandW Beginner Pistol Course. Note: Student must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor. - -",2,N,$35.00 -2026Q3,PE.0612-1,"Skate, Beginner",20,MW,1:00 PM,Johnson Ice Rink 1,2/11/2026,3/18/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0612-2,"Skate, Beginner",20,TR,11:00 AM,Johnson Ice Rink 1,2/10/2026,3/19/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0612-3,"Skate, Beginner",20,TR,1:00 PM,Johnson Ice Rink 1,2/10/2026,3/19/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0612-4,"Skate, Beginner",20,TR,2:00 PM,Johnson Ice Rink 1,2/10/2026,3/19/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-1,"Skate, Intermediate",15,MW,1:00 PM,Johnson Ice Rink 2,2/11/2026,3/18/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-2,"Skate, Intermediate",15,TR,11:00 AM,Johnson Ice Rink 2,2/10/2026,3/19/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-3,"Skate, Intermediate",15,TR,1:00 PM,Johnson Ice Rink 2,2/10/2026,3/19/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-4,"Skate, Intermediate",15,TR,2:00 PM,Johnson Ice Rink 2,2/10/2026,3/19/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0616-1,Squash,12,MW,11:00 AM,Zesiger Squash Courts,2/11/2026,3/18/2026,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0616-2,Squash,12,TR,1:00 PM,Zesiger Squash Courts,2/10/2026,3/19/2026,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0616-3,Squash,12,TR,2:00 PM,Zesiger Squash Courts,2/10/2026,3/19/2026,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0617-1,"Squash, Intermediate",12,TR,11:00 AM,Zesiger Squash Courts,2/10/2026,3/19/2026,"Completion of Beginner Squash class or had experience in high school or club. Please email instructor at bbubna@mit.edu if you are not sure regarding your ability or if you have any questions -","Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. Racquet, ball and eye protection are provided.",2,N,$10.00 -2026Q3,PE.0626-1,Rifle,14,MW,11:00 AM,Du Pont Pistol Range,2/11/2026,3/18/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0626-2,Rifle,14,MW,1:00 PM,Du Pont Pistol Range,2/11/2026,3/18/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0626-3,Rifle,14,MW,2:00 PM,Du Pont Pistol Range,2/11/2026,3/18/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0636-1,Self-Defense for Women,20,MW,1:00 PM,Du Pont Wrestling Room,2/11/2026,3/18/2026,This is an all female course.,None,2,N,$0.00 -2026Q3,PE.0646-1,Pickleball,16,MW,11:00 AM,Rockwell Cage South,2/11/2026,3/18/2026,None,"Work out clothes, footwear, and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0657-1,Spec Tennis,16,MW,2:00 PM,Rockwell Cage South,2/11/2026,3/18/2026,None,Comfortable clothing and footwear.,2,N,$10.00 -2026Q3,PE.0701-1,Ice Hockey,20,MW,2:00 PM,Johnson Ice Rink,2/11/2026,3/18/2026,This course requires a command of forward and backward skating as well as a strong consistent stop that can be learned in beginner skate or equivalent (email instructor using physicaleducationandwellness@mit.edu address if you have questions related to your ability).,"Ice hockey skates, helmet, shin guards, gloves and hockey stick provided at rink. ",2,N,$20.00 -2026Q3,PE.0703-1,"Soccer, Beginner",15,MW,11:00 AM,Zesiger MAC Court,2/11/2026,3/18/2026,This course will be held indoors.,Court shoes recommended for indoor play. Workout clothes. ,2,N,$0.00 -2026Q3,PE.0800-1,Aikido,20,TR,1:00 PM,Du Pont Wrestling Room,2/10/2026,3/19/2026,None,Workout clothes,2,N,$0.00 -2026Q3,PE.0922-1,"Parkour, Beginner",16,F,1:15 PM,Zesiger MAC Court,2/13/2026,3/20/2026,"2/13, 2/20, 2/27, 3/6, 3/13, 3/20**(** Ends in Q4). Time: 1:15p-2:45p Registration is pending until all forms sent from PE&W office have been completed by Mon, 2/9 by 5p. Forms will be sent from the PE&W office using the student's MIT email by the close of online registration. Check SPAM folders if emails are being forwarded from an MIT email account.",Workout clothes. Court shoes recommended.,2,N,$75.00 diff --git a/scrapers/pe.py b/scrapers/pe.py new file mode 100644 index 00000000..c0bf88a9 --- /dev/null +++ b/scrapers/pe.py @@ -0,0 +1,347 @@ +""" +Adds information from PE&W subjects, as given by DAPER. +""" + +from __future__ import annotations + +import csv +import json +import os +import time as time_c +from datetime import date, time +from typing import Literal, TypedDict + +from scrapers.fireroad import parse_section +from scrapers.utils import Term + +QUARTERS: dict[str, tuple[Term, Literal[1, 2] | None]] = { + "1": (Term.FA, 1), + "2": (Term.FA, 2), + "3": (Term.JA, None), + "4": (Term.SP, 1), + "5": (Term.SP, 2), + "6": (Term.SU, None), +} + + +class PEWFile(TypedDict): + """ + Data from CSV file representing PE&W subjects, as given by DAPER + """ + + term: str + section: str + title: str + capacity: int + days: str + start_time: str + end_time: str + location: str + start_date: str + end_date: str + description: str + prerequisites: str + equipment: str + gir_points: int + swim_gir: bool + fee_amount: str + + +class PEWSchema(TypedDict): + """ + Information expected by the frontend (see rawPEClass.ts) + """ + + number: str + name: str + sections: list[tuple[list[tuple[int, int]], str]] + rawSections: list[str] + capacity: int + startDate: str + endDate: str + points: int + swimGIR: bool + prereqs: str + equipment: str + fee: str + description: str + + +def parse_bool(value: str) -> bool: + """ + Parses bool from "Y" or "N" (or throws an error) + + Args: + value (str): The string to parse + + Raises: + ValueError: If the value is not "Y" or "N" + + Returns: + bool: The parsed boolean value + """ + if value.upper() == "Y": + return True + if value.upper() == "N": + return False + + raise ValueError(f"Invalid boolean value: {value}") + + +def read_pew_file(filepath: str) -> list[PEWFile]: + """ + Parses PE&W data from file according to a specific format from a CSV + + Args: + filepath (str): The path to the CSV file + + Returns: + list[PEWFile]: A list of PEWFile dictionaries representing the parsed data + """ + pew_data: list[PEWFile] = [] + with open(filepath, mode="r", newline="", encoding="utf-8") as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + pew_data.append( + { + "term": row["Term"], + "section": row["Section"], + "title": row["Title"], + "capacity": int(row["Capacity"]), + "days": row["Day"], + "start_time": row["Start time"], + "end_time": row["End time"], + "location": row["Location"], + "start_date": row["Start Date"], + "end_date": row["End Date"], + "description": row["Description"], + "prerequisites": row["Prerequisites"], + "equipment": row["Equipment"], + "gir_points": int(row["GIR Points"]), + "swim_gir": parse_bool(row["Swim GIR"]), + "fee_amount": row["Fee Amount"], + } + ) + return pew_data + + +def term_to_semester_year(term_str: str) -> tuple[int, Term, Literal[1, 2] | None]: + """ + Converts a term string to a Term enum and semester half + along with the academic year. + + Args: + term_str (str): The term string in the format "YYYYQ" + where Q is the quarter number + + Returns: + tuple[int, Term, Literal[1, 2] | None]: A tuple containing + the year, Term enum, and semester half + + Raises: + ValueError: If the term string format is invalid + + >>> term_to_semester_year("2026Q2") + (2025, , 2) + + >>> term_to_semester_year("2026Q3") + (2026, , None) + + >>> term_to_semester_year("2026Q4") + (2026, , 1) + """ + + # Validate term string format + if len(term_str) != 6 or term_str[4] != "Q" or term_str[5] not in QUARTERS: + raise ValueError(f"Invalid term string format: {term_str}") + + year = int(term_str[:4]) + quarter = term_str[5] + term, semester = QUARTERS[quarter] + + if term == Term.FA: + year -= 1 # Fall term belongs to the previous academic year + + return (year, term, semester) + + +def split_section_code(section_code: str) -> tuple[str, str]: + """ + Splits a section code into its subject and number components. + + Args: + section_code (str): The section code in the format "PE.0201-1" + + Returns: + tuple[str, str]: A tuple containing the subject and number components + + Raises: + ValueError: If the section code format is invalid + + >>> split_section_code("PE.0201-1") + ('PE.0201', '1') + + >>> split_section_code("PE.0613-4") + ('PE.0613', '4') + """ + if "-" not in section_code: + raise ValueError(f"Invalid section code format: {section_code}") + subject, number = section_code.rsplit("-", 1) + return subject, number + + +def parse_date(date_str: str) -> date: + """ + Parses a date string in the format "MM/DD/YYYY" to a datetime.date object. + + Args: + date_str (str): The date string in the format "MM/DD/YYYY" + + Returns: + datetime.date: The parsed date object + + Raises: + ValueError: If the date string format is invalid + + >>> parse_date("9/1/2023") + datetime.date(2023, 9, 1) + + >>> parse_date("12/31/2024") + datetime.date(2024, 12, 31) + """ + month, day, year = map(int, date_str.split("/")) + return date(year, month, day) + + +def parse_times_to_raw_section( + start_time: str, end_time: str, days: str, location: str +) -> str: + """ + Parses times from CVS to format from Fireroad, for compatibility. + + Args: + start_time (str): Start time of the class + end_time (str): End time of the class (or empty string for default) + days (str): Days the class meets + location (str): Location of the class + + Returns: + str: Formatted raw section string + """ + start_c = time_c.strptime(start_time, "%I:%M %p") + start = time(start_c.tm_hour, start_c.tm_min) + + if end_time: + end_c = time_c.strptime(end_time, "%I:%M %p") + end = time(end_c.tm_hour, end_c.tm_min) + else: + end = time( + start.hour + 1, start.minute + ) # default to 1 hour if no end time given + + start_raw_time = ( + f"{12 - ((- start.hour) % 12)}" f"{'.30' if start.minute > 29 else ''}" + ) + end_raw_time = ( + f"{12 - ((- end.hour) % 12)}" + f"{'.30' if end.minute > 29 else ''}" + f"{' PM' if end.hour >= 17 else ''}" + ) + evening = "1" if end.hour >= 17 else "0" + + return f"{location}/{days}/{evening}/{start_raw_time}-{end_raw_time}" + + +def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[str, PEWSchema]: + """ + Converts PEWFile dictionaries to a standardized schema dictionary. + + Args: + pe_file (PEWFile): The PEWFile dictionary to convert + + Returns: + dict: A dictionary representing the standardized schema + """ + results: dict[str, PEWSchema] = {} + + for pe_row in pe_rows: + subject_num, _ = split_section_code(pe_row["section"]) + current_results = results.get(subject_num) + + if current_results: + # ensure all data in current_results (except for section info) are the same + assert current_results["name"] == pe_row["title"] + assert current_results["capacity"] == pe_row["capacity"] + assert current_results["points"] == pe_row["gir_points"] + assert current_results["swimGIR"] == pe_row["swim_gir"] + assert current_results["prereqs"] == pe_row["prerequisites"] + assert current_results["equipment"] == pe_row["equipment"] + assert current_results["fee"] == pe_row["fee_amount"] + assert current_results["description"] == pe_row["description"] + + raw_section = parse_times_to_raw_section( + pe_row["start_time"], + pe_row["end_time"], + pe_row["days"], + pe_row["location"], + ) + section = parse_section(raw_section) + + current_results["rawSections"].append(raw_section) + current_results["sections"].append(section) + + results[subject_num] = current_results + else: + raw_section = parse_times_to_raw_section( + pe_row["start_time"], + pe_row["end_time"], + pe_row["days"], + pe_row["location"], + ) + section = parse_section(raw_section) + + results[subject_num] = { + "number": subject_num, + "name": pe_row["title"], + "sections": [section], + "rawSections": [raw_section], + "capacity": pe_row["capacity"], + "startDate": parse_date(pe_row["start_date"]).isoformat(), + "endDate": parse_date(pe_row["end_date"]).isoformat(), + "points": pe_row["gir_points"], + "swimGIR": pe_row["swim_gir"], + "prereqs": pe_row["prerequisites"], + "equipment": pe_row["equipment"], + "fee": pe_row["fee_amount"], + "description": pe_row["description"], + } + + return results + + +def run(): + """ + Main entry point for PE data + """ + + # get list of csv files in the pe data directory + pe_folder = os.path.join(os.path.dirname(__file__), "pe") + pe_files = os.listdir(pe_folder) + + pe_files_data = [] + for pe_file in pe_files: + if pe_file.endswith(".csv"): + # process the data as needed + pe_files_data.extend(read_pew_file(os.path.join(pe_folder, pe_file))) + + pe_data = pe_rows_to_schema(pe_files_data) + + fname = os.path.join(os.path.dirname(__file__), "pe.json") + with open(fname, "w", encoding="utf-8") as pe_output_file: + json.dump(pe_data, pe_output_file, ensure_ascii=False, indent=4) + + return pe_data + + +if __name__ == "__main__": + run() diff --git a/pe/s26-h1pe.csv b/scrapers/pe/s26-h1pe.csv similarity index 86% rename from pe/s26-h1pe.csv rename to scrapers/pe/s26-h1pe.csv index 94036bd8..38ac288b 100644 --- a/pe/s26-h1pe.csv +++ b/scrapers/pe/s26-h1pe.csv @@ -1,69 +1,69 @@ Term,Section,Title,Capacity,Day,Start time,End time,Location,Start Date,End Date,Description,Prerequisites,Equipment,GIR Points,Swim GIR,Fee Amount -2026Q3,PE.0201-1,SCUBA Diving,18,T,6:45 PM,,Alumni Wang Pool and 66-160,2/24/2026,4/14/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. Attendance is required on the first and last day.","All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 -2026Q3,PE.0201-2,SCUBA Diving,18,R,6:45 PM,,Alumni Wang Pool and 66-160,2/19/2026,4/9/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. Attendance is required on the first and last day.","All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 -2026Q3,PE.0202-1,"Swimming, Beginner",10,MW,11:00 AM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-2,"Swimming, Beginner",10,MW,1:00 PM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-3,"Swimming, Beginner",10,MW,2:00 PM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-4,"Swimming, Beginner",10,TR,11:00 AM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-5,"Swimming, Beginner",10,TR,1:00 PM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-6,"Swimming, Beginner",10,TR,2:00 PM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0300-1,Ballroom,20,TR,7:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0403-1,Group Exercise - Cardio Kickboxing,20,MW,6:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,"Workout clothes, footwear and water bottle",2,N,$0.00 -2026Q3,PE.0405-1,Group Exercise - Pilates,20,TR,3:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0411-1,Group Exercise- Yoga,20,MW,8:00 AM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0411-2,Group Exercise- Yoga,20,MW,5:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0411-3,Group Exercise- Yoga,20,TR,5:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0414-1,Weight Training,16,MW,11:00 AM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-2,Weight Training,16,MW,1:00 PM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-3,Weight Training,16,MW,2:00 PM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-4,Weight Training,16,TR,11:00 AM,,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-5,Weight Training,16,TR,2:00 PM,,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0432-1,Group Exercise- Barre Fitness,15,TR,2:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0435-1,Group Exercise- Functional Fitness,20,MW,3:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0444-1,Group Exercise- HIIT,20,TR,6:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0517-1,Fitness (Yoga)/CPR/First Aid,12,MW,4:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,"In person class starts Tue, Feb. 17 (switch day)",Q3 2026: Students must complete the remote asynchronous CPR content and in-person CPR and FA exam sessions to become CPR/First Aid certified and students must be able to kneel and use 2 arms to give compressions.,Workout clothes and water bottle. Lab fee covers pocket mask and CPR and first aid certification cards,2,N,$60.00 -2026Q3,PE.0518-1,Fitness (Yoga)/Meditation,16,TR,6:00 PM,,Du Pont Multi-Purpose Room,2/10/2026,3/19/2026,,None,Workout clothes and a filled water bottle.,2,N,$0.00 -2026Q3,PE.0529-1,Fitness(Yoga)/Meditation (remote synchronous),15,MW,7:00 PM,,Remote Synchronous,2/11/2026,3/18/2026,,"This remote synchronous course requires internet access, computer (or tablet, mobile device) with a camera, microphone, and working speaker, MIT Zoom account, roughly 6 foot x 6 foot physical area clear of any objects with a standard 7 - 8 foot ceiling and non-slip floor to do physical activity, comfortable with using 'camera on' function during Zoom sessions. ","Workout clothing and filled water bottle, mat or towel. See other prerequisites.",2,N,$0.00 +2026Q3,PE.0201-1,SCUBA Diving,18,T,6:45 PM,,Alumni Wang Pool and 66-160,2/24/2026,4/14/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. Attendance is required on the first and last day.","All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 +2026Q3,PE.0201-2,SCUBA Diving,18,R,6:45 PM,,Alumni Wang Pool and 66-160,2/19/2026,4/9/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. Attendance is required on the first and last day.","All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 +2026Q3,PE.0202-1,"Swimming, Beginner",10,MW,11:00 AM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-2,"Swimming, Beginner",10,MW,1:00 PM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-3,"Swimming, Beginner",10,MW,2:00 PM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-4,"Swimming, Beginner",10,TR,11:00 AM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-5,"Swimming, Beginner",10,TR,1:00 PM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-6,"Swimming, Beginner",10,TR,2:00 PM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0300-1,Ballroom,20,TR,7:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0403-1,Group Exercise - Cardio Kickboxing,20,MW,6:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,"Workout clothes, footwear and water bottle",2,N,$0.00 +2026Q3,PE.0405-1,Group Exercise - Pilates,20,TR,3:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0411-1,Group Exercise- Yoga,20,MW,8:00 AM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0411-2,Group Exercise- Yoga,20,MW,5:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0411-3,Group Exercise- Yoga,20,TR,5:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0414-1,Weight Training,16,MW,11:00 AM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-2,Weight Training,16,MW,1:00 PM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-3,Weight Training,16,MW,2:00 PM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-4,Weight Training,16,TR,11:00 AM,,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-5,Weight Training,16,TR,2:00 PM,,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0432-1,Group Exercise- Barre Fitness,15,TR,2:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0435-1,Group Exercise- Functional Fitness,20,MW,3:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0444-1,Group Exercise- HIIT,20,TR,6:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0517-1,Fitness (Yoga)/CPR/First Aid,12,MW,4:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,"In person class starts Tue, Feb. 17 (switch day)",Q3 2026: Students must complete the remote asynchronous CPR content and in-person CPR and FA exam sessions to become CPR/First Aid certified and students must be able to kneel and use 2 arms to give compressions.,Workout clothes and water bottle. Lab fee covers pocket mask and CPR and first aid certification cards,2,N,$60.00 +2026Q3,PE.0518-1,Fitness (Yoga)/Meditation,16,TR,6:00 PM,,Du Pont Multi-Purpose Room,2/10/2026,3/19/2026,,None,Workout clothes and a filled water bottle.,2,N,$0.00 +2026Q3,PE.0529-1,Fitness(Yoga)/Meditation (remote synchronous),15,MW,7:00 PM,,Remote Synchronous,2/11/2026,3/18/2026,,"This remote synchronous course requires internet access, computer (or tablet, mobile device) with a camera, microphone, and working speaker, MIT Zoom account, roughly 6 foot x 6 foot physical area clear of any objects with a standard 7 - 8 foot ceiling and non-slip floor to do physical activity, comfortable with using 'camera on' function during Zoom sessions. ","Workout clothing and filled water bottle, mat or towel. See other prerequisites.",2,N,$0.00 2026Q3,PE.0544-1,Fitness(Strength Circuit)/Nutrition,16,MW,5:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,"Sneakers/footwear, comfortable workout clothing and water bottle. -",2,N,$0.00 -2026Q3,PE.0545-1,Fitness(Strength Circuit)/Resiliency,16,MW,3:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,Workout clothes and filled water bottle.,2,N,$0.00 -2026Q3,PE.0546-1,Fitness(Strength Circuit)/Stress Management,16,MW,6:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,"Sneakers/footwear, comfortable workout clothing and water bottle.",2,N,$0.00 -2026Q3,PE.0600-1,Archery,14,TR,10:00 AM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-2,Archery,14,TR,11:00 AM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-3,Archery,14,TR,1:00 PM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-4,Archery,14,TR,2:00 PM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-5,Archery,14,MW,1:00 PM,,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-6,Archery,14,MW,2:00 PM,,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0601-1,Badminton,16,TR,1:00 PM,,Rockwell Cage South,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0601-2,Badminton,16,TR,2:00 PM,,Rockwell Cage South,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0603-1,"Fencing, Sabre",16,TR,3:00 PM,,Du Pont Fencing Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,Workout clothes,2,N,$15.00 +",2,N,$0.00 +2026Q3,PE.0545-1,Fitness(Strength Circuit)/Resiliency,16,MW,3:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,Workout clothes and filled water bottle.,2,N,$0.00 +2026Q3,PE.0546-1,Fitness(Strength Circuit)/Stress Management,16,MW,6:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,"Sneakers/footwear, comfortable workout clothing and water bottle.",2,N,$0.00 +2026Q3,PE.0600-1,Archery,14,TR,10:00 AM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-2,Archery,14,TR,11:00 AM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-3,Archery,14,TR,1:00 PM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-4,Archery,14,TR,2:00 PM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-5,Archery,14,MW,1:00 PM,,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-6,Archery,14,MW,2:00 PM,,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0601-1,Badminton,16,TR,1:00 PM,,Rockwell Cage South,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0601-2,Badminton,16,TR,2:00 PM,,Rockwell Cage South,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0603-1,"Fencing, Sabre",16,TR,3:00 PM,,Du Pont Fencing Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,Workout clothes,2,N,$15.00 2026Q3,PE.0608-1,Pistol,14,TR,1:00 PM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. -",,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +",,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 2026Q3,PE.0608-2,Pistol,14,TR,2:00 PM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. -",,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +",,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 2026Q3,PE.0609-1,"Pistol, Intermediate",14,TR,11:00 AM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Student must attend first 4 classes, though attendance at all classes is strongly recommended.",Student must have successfully completed the MIT PEandW Beginner Pistol Course.,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor. -",2,N,$35.00 -2026Q3,PE.0612-1,"Skate, Beginner",20,MW,1:00 PM,,Johnson Ice Rink 1,2/11/2026,3/18/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0612-2,"Skate, Beginner",20,TR,11:00 AM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0612-3,"Skate, Beginner",20,TR,1:00 PM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0612-4,"Skate, Beginner",20,TR,2:00 PM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-1,"Skate, Intermediate",15,MW,1:00 PM,,Johnson Ice Rink 2,2/11/2026,3/18/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-2,"Skate, Intermediate",15,TR,11:00 AM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-3,"Skate, Intermediate",15,TR,1:00 PM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-4,"Skate, Intermediate",15,TR,2:00 PM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0616-1,Squash,12,MW,11:00 AM,,Zesiger Squash Courts,2/11/2026,3/18/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0616-2,Squash,12,TR,1:00 PM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0616-3,Squash,12,TR,2:00 PM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +",2,N,$35.00 +2026Q3,PE.0612-1,"Skate, Beginner",20,MW,1:00 PM,,Johnson Ice Rink 1,2/11/2026,3/18/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-2,"Skate, Beginner",20,TR,11:00 AM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-3,"Skate, Beginner",20,TR,1:00 PM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-4,"Skate, Beginner",20,TR,2:00 PM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-1,"Skate, Intermediate",15,MW,1:00 PM,,Johnson Ice Rink 2,2/11/2026,3/18/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-2,"Skate, Intermediate",15,TR,11:00 AM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-3,"Skate, Intermediate",15,TR,1:00 PM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-4,"Skate, Intermediate",15,TR,2:00 PM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0616-1,Squash,12,MW,11:00 AM,,Zesiger Squash Courts,2/11/2026,3/18/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0616-2,Squash,12,TR,1:00 PM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0616-3,Squash,12,TR,2:00 PM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 2026Q3,PE.0617-1,"Squash, Intermediate",12,TR,11:00 AM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,"Completion of Beginner Squash class or had experience in high school or club. Please email instructor at bbubna@mit.edu if you are not sure regarding your ability or if you have any questions -","Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. Racquet, ball and eye protection are provided.",2,N,$10.00 -2026Q3,PE.0626-1,Rifle,14,MW,11:00 AM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0626-2,Rifle,14,MW,1:00 PM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0626-3,Rifle,14,MW,2:00 PM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0636-1,Self-Defense for Women,20,MW,1:00 PM,,Du Pont Wrestling Room,2/11/2026,3/18/2026,,This is an all female course.,None,2,N,$0.00 -2026Q3,PE.0646-1,Pickleball,16,MW,11:00 AM,,Rockwell Cage South,2/11/2026,3/18/2026,,None,"Work out clothes, footwear, and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0657-1,Spec Tennis,16,MW,2:00 PM,,Rockwell Cage South,2/11/2026,3/18/2026,,None,Comfortable clothing and footwear.,2,N,$10.00 -2026Q3,PE.0701-1,Ice Hockey,20,MW,2:00 PM,,Johnson Ice Rink,2/11/2026,3/18/2026,,This course requires a command of forward and backward skating as well as a strong consistent stop that can be learned in beginner skate or equivalent (email instructor using physicaleducationandwellness@mit.edu address if you have questions related to your ability).,"Ice hockey skates, helmet, shin guards, gloves and hockey stick provided at rink. ",2,N,$20.00 -2026Q3,PE.0703-1,"Soccer, Beginner",15,MW,11:00 AM,,Zesiger MAC Court,2/11/2026,3/18/2026,,This course will be held indoors.,Court shoes recommended for indoor play. Workout clothes. ,2,N,$0.00 -2026Q3,PE.0800-1,Aikido,20,TR,1:00 PM,,Du Pont Wrestling Room,2/10/2026,3/19/2026,,None,Workout clothes,2,N,$0.00 -2026Q3,PE.0922-1,"Parkour, Beginner",16,F,1:15 PM,2:45 PM,Zesiger MAC Court,2/13/2026,3/20/2026,"2/13, 2/20, 2/27, 3/6, 3/13, 3/20**(** Ends in Q4). Registration is pending until all forms sent from PE&W office have been completed by Mon, 2/9 by 5p. Forms will be sent from the PE&W office using the student's MIT email by the close of online registration. Check SPAM folders if emails are being forwarded from an MIT email account.",,Workout clothes. Court shoes recommended.,2,N,$75.00 +","Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. Racquet, ball and eye protection are provided.",2,N,$10.00 +2026Q3,PE.0626-1,Rifle,14,MW,11:00 AM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0626-2,Rifle,14,MW,1:00 PM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0626-3,Rifle,14,MW,2:00 PM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0636-1,Self-Defense for Women,20,MW,1:00 PM,,Du Pont Wrestling Room,2/11/2026,3/18/2026,,This is an all female course.,None,2,N,$0.00 +2026Q3,PE.0646-1,Pickleball,16,MW,11:00 AM,,Rockwell Cage South,2/11/2026,3/18/2026,,None,"Work out clothes, footwear, and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0657-1,Spec Tennis,16,MW,2:00 PM,,Rockwell Cage South,2/11/2026,3/18/2026,,None,Comfortable clothing and footwear.,2,N,$10.00 +2026Q3,PE.0701-1,Ice Hockey,20,MW,2:00 PM,,Johnson Ice Rink,2/11/2026,3/18/2026,,This course requires a command of forward and backward skating as well as a strong consistent stop that can be learned in beginner skate or equivalent (email instructor using physicaleducationandwellness@mit.edu address if you have questions related to your ability).,"Ice hockey skates, helmet, shin guards, gloves and hockey stick provided at rink. ",2,N,$20.00 +2026Q3,PE.0703-1,"Soccer, Beginner",15,MW,11:00 AM,,Zesiger MAC Court,2/11/2026,3/18/2026,,This course will be held indoors.,Court shoes recommended for indoor play. Workout clothes. ,2,N,$0.00 +2026Q3,PE.0800-1,Aikido,20,TR,1:00 PM,,Du Pont Wrestling Room,2/10/2026,3/19/2026,,None,Workout clothes,2,N,$0.00 +2026Q3,PE.0922-1,"Parkour, Beginner",16,F,1:15 PM,2:45 PM,Zesiger MAC Court,2/13/2026,3/20/2026,"2/13, 2/20, 2/27, 3/6, 3/13, 3/20**(** Ends in Q4). Registration is pending until all forms sent from PE&W office have been completed by Mon, 2/9 by 5p. Forms will be sent from the PE&W office using the student's MIT email by the close of online registration. Check SPAM folders if emails are being forwarded from an MIT email account.",,Workout clothes. Court shoes recommended.,2,N,$75.00 From 381d7bac5b722ad51369ea03ae39fb2c099cddd2 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:11:49 -0500 Subject: [PATCH 04/89] add pe data to package --- .gitignore | 5 +- scrapers/__main__.py | 3 ++ scrapers/package.py | 23 ++++++--- scrapers/pe.py | 116 +++++++++++++++++++++++++++++++++---------- 4 files changed, 112 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 7f893e9f..286ff862 100644 --- a/.gitignore +++ b/.gitignore @@ -26,10 +26,9 @@ dist-ssr # artifacts scrapers/catalog.json -scrapers/fireroad-sem.json -scrapers/fireroad-presem.json +scrapers/fireroad-*.json scrapers/cim.json -scrapers/pe.json +scrapers/pe-*.json public/latest.json public/i26.json diff --git a/scrapers/__main__.py b/scrapers/__main__.py index 99223002..b1df69cb 100644 --- a/scrapers/__main__.py +++ b/scrapers/__main__.py @@ -11,6 +11,7 @@ from .cim import run as cim_run from .fireroad import run as fireroad_run from .package import run as package_run +from .pe import run as pe_run def run(): @@ -25,6 +26,8 @@ def run(): catalog_run() print("=== Update CI-M data ===") cim_run() + print("=== Update PE data ===") + pe_run() print("=== Packaging ===") package_run() diff --git a/scrapers/package.py b/scrapers/package.py index ab5fec4f..cf0c2684 100644 --- a/scrapers/package.py +++ b/scrapers/package.py @@ -19,7 +19,8 @@ from collections.abc import Iterable from typing import Any -from .utils import get_term_info +from scrapers.pe import get_pe_files +from scrapers.utils import get_term_info if sys.version_info >= (3, 11): import tomllib @@ -150,11 +151,10 @@ def run() -> None: term_info = get_term_info(sem) url_name = term_info["urlName"] - obj: dict[str, dict[str, Any] | str | dict[Any, dict[str, Any]]] = { - "termInfo": term_info, - "lastUpdated": now, - "classes": courses, - } + pe_data = [] + for pe_file in get_pe_files(url_name): + if os.path.isfile(os.path.join(package_dir, pe_file)): + pe_data.append(load_json_data(pe_file)) with open( os.path.join( @@ -163,7 +163,16 @@ def run() -> None: mode="w", encoding="utf-8", ) as file: - json.dump(obj, file, separators=(",", ":")) + json.dump( + { + "termInfo": term_info, + "lastUpdated": now, + "classes": courses, + "pe": pe_data, + }, + file, + separators=(",", ":"), + ) print(f"{url_name}: got {len(courses)} courses") diff --git a/scrapers/pe.py b/scrapers/pe.py index c0bf88a9..70e0f8ee 100644 --- a/scrapers/pe.py +++ b/scrapers/pe.py @@ -14,13 +14,13 @@ from scrapers.fireroad import parse_section from scrapers.utils import Term -QUARTERS: dict[str, tuple[Term, Literal[1, 2] | None]] = { - "1": (Term.FA, 1), - "2": (Term.FA, 2), - "3": (Term.JA, None), - "4": (Term.SP, 1), - "5": (Term.SP, 2), - "6": (Term.SU, None), +# ask DAPER how they represent summer... +QUARTERS: dict[int, tuple[Term, Literal[1, 2] | None]] = { + 1: (Term.FA, 1), + 2: (Term.FA, 2), + 3: (Term.SP, 1), + 4: (Term.SP, 2), + 5: (Term.JA, None), } @@ -65,6 +65,7 @@ class PEWSchema(TypedDict): equipment: str fee: str description: str + quarter: int def parse_bool(value: str) -> bool: @@ -125,6 +126,35 @@ def read_pew_file(filepath: str) -> list[PEWFile]: return pew_data +def get_year_quarter(term_str: str) -> tuple[int, int]: + """ + Extracts the quarter from a term string. + + Args: + term_str (str): The term string in the format "YYYYQ" + + Returns: + tuple[int, int]: The year and quarter extracted from the term string + + Raises: + ValueError: If the term string format is invalid + + >>> get_year_quarter("2026Q2") + (2026, 2) + + >>> get_year_quarter("2026Q3") + (2026, 3) + """ + + # Validate term string format + if len(term_str) != 6 or term_str[4] != "Q" or int(term_str[5]) not in QUARTERS: + raise ValueError(f"Invalid term string format: {term_str}") + + year = int(term_str[:4]) + quarter = int(term_str[5]) + return year, quarter + + def term_to_semester_year(term_str: str) -> tuple[int, Term, Literal[1, 2] | None]: """ Converts a term string to a Term enum and semester half @@ -138,9 +168,6 @@ def term_to_semester_year(term_str: str) -> tuple[int, Term, Literal[1, 2] | Non tuple[int, Term, Literal[1, 2] | None]: A tuple containing the year, Term enum, and semester half - Raises: - ValueError: If the term string format is invalid - >>> term_to_semester_year("2026Q2") (2025, , 2) @@ -151,12 +178,7 @@ def term_to_semester_year(term_str: str) -> tuple[int, Term, Literal[1, 2] | Non (2026, , 1) """ - # Validate term string format - if len(term_str) != 6 or term_str[4] != "Q" or term_str[5] not in QUARTERS: - raise ValueError(f"Invalid term string format: {term_str}") - - year = int(term_str[:4]) - quarter = term_str[5] + year, quarter = get_year_quarter(term_str) term, semester = QUARTERS[quarter] if term == Term.FA: @@ -252,7 +274,7 @@ def parse_times_to_raw_section( return f"{location}/{days}/{evening}/{start_raw_time}-{end_raw_time}" -def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[str, PEWSchema]: +def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[int, dict[str, PEWSchema]]: """ Converts PEWFile dictionaries to a standardized schema dictionary. @@ -260,13 +282,21 @@ def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[str, PEWSchema]: pe_file (PEWFile): The PEWFile dictionary to convert Returns: - dict: A dictionary representing the standardized schema + dict: A dictionary representing the standardized schema, + keyed by quarter and subject number """ - results: dict[str, PEWSchema] = {} + results: dict[int, dict[str, PEWSchema]] = {} for pe_row in pe_rows: + _, quarter = get_year_quarter(pe_row["term"]) + + term_results = results.get(quarter) + if term_results is None: + term_results = {} + results[quarter] = term_results + subject_num, _ = split_section_code(pe_row["section"]) - current_results = results.get(subject_num) + current_results = term_results.get(subject_num) if current_results: # ensure all data in current_results (except for section info) are the same @@ -290,7 +320,7 @@ def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[str, PEWSchema]: current_results["rawSections"].append(raw_section) current_results["sections"].append(section) - results[subject_num] = current_results + term_results[subject_num] = current_results else: raw_section = parse_times_to_raw_section( pe_row["start_time"], @@ -300,7 +330,7 @@ def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[str, PEWSchema]: ) section = parse_section(raw_section) - results[subject_num] = { + term_results[subject_num] = { "number": subject_num, "name": pe_row["title"], "sections": [section], @@ -314,11 +344,44 @@ def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[str, PEWSchema]: "equipment": pe_row["equipment"], "fee": pe_row["fee_amount"], "description": pe_row["description"], + "quarter": quarter, } + results[quarter] = term_results return results +def get_pe_files(url_name: str) -> list[str]: + """ + Gets the list of parsed PE files for a given urlName. + + Args: + url_name (str): The urlName to get PE files for + + Returns: + list[str]: The list of PE files for the term + + >>> get_pe_files("f26") + ['pe-q1.json', 'pe-q2.json'] + + >>> get_pe_files("i26") + ['pe-q3.json'] + """ + + assert url_name[0] in ("f", "i", "s", "m"), "Invalid urlName format" + + quarter = { + "f": [1, 2], # Fall + "s": [3, 4], # Spring + "i": [5], # IAP + "m": [], # Summer + }[url_name[0]] + + files = [f"pe-q{q}.json" for q in quarter] + + return files + + def run(): """ Main entry point for PE data @@ -336,9 +399,12 @@ def run(): pe_data = pe_rows_to_schema(pe_files_data) - fname = os.path.join(os.path.dirname(__file__), "pe.json") - with open(fname, "w", encoding="utf-8") as pe_output_file: - json.dump(pe_data, pe_output_file, ensure_ascii=False, indent=4) + for quarter, quarter_data in pe_data.items(): + print(f"Processed PE data for quarter {quarter}: {len(quarter_data)} subjects") + fname = os.path.join(os.path.dirname(__file__), f"pe-q{quarter}.json") + + with open(fname, "w", encoding="utf-8") as pe_output_file: + json.dump(quarter_data, pe_output_file, ensure_ascii=False, indent=4) return pe_data From c6e6afdaaffe39d9c0f1220143ffc0c8c181ff9b Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:12:51 -0500 Subject: [PATCH 05/89] restore original --- scrapers/archive/s26-h1pe-orig.csv | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 scrapers/archive/s26-h1pe-orig.csv diff --git a/scrapers/archive/s26-h1pe-orig.csv b/scrapers/archive/s26-h1pe-orig.csv new file mode 100644 index 00000000..8525b26b --- /dev/null +++ b/scrapers/archive/s26-h1pe-orig.csv @@ -0,0 +1,69 @@ +Term,Section,Title,Capacity,Day,Time,Location,Start Date,End Date,Prerequisites,Equipment,GIR Points,Swim GIR,Fee Amount +2026Q3,PE.0201-1,SCUBA Diving,18,T,6:45 PM,Alumni Wang Pool and 66-160,2/24/2026,4/14/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p. Attendance is required on the first and last day.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 +2026Q3,PE.0201-2,SCUBA Diving,18,R,6:45 PM,Alumni Wang Pool and 66-160,2/19/2026,4/9/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p. Attendance is required on the first and last day.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 +2026Q3,PE.0202-1,"Swimming, Beginner",10,MW,11:00 AM,Zesiger Teaching Pool,2/11/2026,3/18/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-2,"Swimming, Beginner",10,MW,1:00 PM,Zesiger Teaching Pool,2/11/2026,3/18/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-3,"Swimming, Beginner",10,MW,2:00 PM,Zesiger Teaching Pool,2/11/2026,3/18/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-4,"Swimming, Beginner",10,TR,11:00 AM,Zesiger Teaching Pool,2/10/2026,3/19/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-5,"Swimming, Beginner",10,TR,1:00 PM,Zesiger Teaching Pool,2/10/2026,3/19/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-6,"Swimming, Beginner",10,TR,2:00 PM,Zesiger Teaching Pool,2/10/2026,3/19/2026,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0300-1,Ballroom,20,TR,7:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0403-1,Group Exercise - Cardio Kickboxing,20,MW,6:00 PM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,"Workout clothes, footwear and water bottle",2,N,$0.00 +2026Q3,PE.0405-1,Group Exercise - Pilates,20,TR,3:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0411-1,Group Exercise- Yoga,20,MW,8:00 AM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0411-2,Group Exercise- Yoga,20,MW,5:00 PM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0411-3,Group Exercise- Yoga,20,TR,5:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0414-1,Weight Training,16,MW,11:00 AM,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-2,Weight Training,16,MW,1:00 PM,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-3,Weight Training,16,MW,2:00 PM,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-4,Weight Training,16,TR,11:00 AM,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-5,Weight Training,16,TR,2:00 PM,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0432-1,Group Exercise- Barre Fitness,15,TR,2:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0435-1,Group Exercise- Functional Fitness,20,MW,3:00 PM,Du Pont T Club Lounge,2/11/2026,3/18/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0444-1,Group Exercise- HIIT,20,TR,6:00 PM,Du Pont T Club Lounge,2/10/2026,3/19/2026,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0517-1,Fitness (Yoga)/CPR/First Aid,12,MW,4:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,"Q3 2026: Students must complete the remote asynchronous CPR content and in-person CPR and FA exam sessions to become CPR/First Aid certified and students must be able to kneel and use 2 arms to give compressions. In person class starts Tue, Feb. 17 (switch day)",Workout clothes and water bottle. Lab fee covers pocket mask and CPR and first aid certification cards,2,N,$60.00 +2026Q3,PE.0518-1,Fitness (Yoga)/Meditation,16,TR,6:00 PM,Du Pont Multi-Purpose Room,2/10/2026,3/19/2026,None,Workout clothes and a filled water bottle.,2,N,$0.00 +2026Q3,PE.0529-1,Fitness(Yoga)/Meditation (remote synchronous),15,MW,7:00 PM,Remote Synchronous,2/11/2026,3/18/2026,"This remote synchronous course requires internet access, computer (or tablet, mobile device) with a camera, microphone, and working speaker, MIT Zoom account, roughly 6 foot x 6 foot physical area clear of any objects with a standard 7 - 8 foot ceiling and non-slip floor to do physical activity, comfortable with using 'camera on' function during Zoom sessions. ","Workout clothing and filled water bottle, mat or towel. See other prerequisites.",2,N,$0.00 +2026Q3,PE.0544-1,Fitness(Strength Circuit)/Nutrition,16,MW,5:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,None,"Sneakers/footwear, comfortable workout clothing and water bottle. +",2,N,$0.00 +2026Q3,PE.0545-1,Fitness(Strength Circuit)/Resiliency,16,MW,3:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,None,Workout clothes and filled water bottle.,2,N,$0.00 +2026Q3,PE.0546-1,Fitness(Strength Circuit)/Stress Management,16,MW,6:00 PM,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,None,"Sneakers/footwear, comfortable workout clothing and water bottle.",2,N,$0.00 +2026Q3,PE.0600-1,Archery,14,TR,10:00 AM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-2,Archery,14,TR,11:00 AM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-3,Archery,14,TR,1:00 PM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-4,Archery,14,TR,2:00 PM,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-5,Archery,14,MW,1:00 PM,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-6,Archery,14,MW,2:00 PM,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0601-1,Badminton,16,TR,1:00 PM,Rockwell Cage South,2/10/2026,3/19/2026,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0601-2,Badminton,16,TR,2:00 PM,Rockwell Cage South,2/10/2026,3/19/2026,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0603-1,"Fencing, Sabre",16,TR,3:00 PM,Du Pont Fencing Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,Workout clothes,2,N,$15.00 +2026Q3,PE.0608-1,Pistol,14,TR,1:00 PM,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. +","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0608-2,Pistol,14,TR,2:00 PM,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. +","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0609-1,"Pistol, Intermediate",14,TR,11:00 AM,Du Pont Pistol Range,2/10/2026,3/19/2026,"Student must have successfully completed the MIT PEandW Beginner Pistol Course. Note: Student must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor. + +",2,N,$35.00 +2026Q3,PE.0612-1,"Skate, Beginner",20,MW,1:00 PM,Johnson Ice Rink 1,2/11/2026,3/18/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-2,"Skate, Beginner",20,TR,11:00 AM,Johnson Ice Rink 1,2/10/2026,3/19/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-3,"Skate, Beginner",20,TR,1:00 PM,Johnson Ice Rink 1,2/10/2026,3/19/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-4,"Skate, Beginner",20,TR,2:00 PM,Johnson Ice Rink 1,2/10/2026,3/19/2026,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-1,"Skate, Intermediate",15,MW,1:00 PM,Johnson Ice Rink 2,2/11/2026,3/18/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-2,"Skate, Intermediate",15,TR,11:00 AM,Johnson Ice Rink 2,2/10/2026,3/19/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-3,"Skate, Intermediate",15,TR,1:00 PM,Johnson Ice Rink 2,2/10/2026,3/19/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-4,"Skate, Intermediate",15,TR,2:00 PM,Johnson Ice Rink 2,2/10/2026,3/19/2026,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0616-1,Squash,12,MW,11:00 AM,Zesiger Squash Courts,2/11/2026,3/18/2026,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0616-2,Squash,12,TR,1:00 PM,Zesiger Squash Courts,2/10/2026,3/19/2026,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0616-3,Squash,12,TR,2:00 PM,Zesiger Squash Courts,2/10/2026,3/19/2026,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0617-1,"Squash, Intermediate",12,TR,11:00 AM,Zesiger Squash Courts,2/10/2026,3/19/2026,"Completion of Beginner Squash class or had experience in high school or club. Please email instructor at bbubna@mit.edu if you are not sure regarding your ability or if you have any questions +","Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. Racquet, ball and eye protection are provided.",2,N,$10.00 +2026Q3,PE.0626-1,Rifle,14,MW,11:00 AM,Du Pont Pistol Range,2/11/2026,3/18/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0626-2,Rifle,14,MW,1:00 PM,Du Pont Pistol Range,2/11/2026,3/18/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0626-3,Rifle,14,MW,2:00 PM,Du Pont Pistol Range,2/11/2026,3/18/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0636-1,Self-Defense for Women,20,MW,1:00 PM,Du Pont Wrestling Room,2/11/2026,3/18/2026,This is an all female course.,None,2,N,$0.00 +2026Q3,PE.0646-1,Pickleball,16,MW,11:00 AM,Rockwell Cage South,2/11/2026,3/18/2026,None,"Work out clothes, footwear, and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0657-1,Spec Tennis,16,MW,2:00 PM,Rockwell Cage South,2/11/2026,3/18/2026,None,Comfortable clothing and footwear.,2,N,$10.00 +2026Q3,PE.0701-1,Ice Hockey,20,MW,2:00 PM,Johnson Ice Rink,2/11/2026,3/18/2026,This course requires a command of forward and backward skating as well as a strong consistent stop that can be learned in beginner skate or equivalent (email instructor using physicaleducationandwellness@mit.edu address if you have questions related to your ability).,"Ice hockey skates, helmet, shin guards, gloves and hockey stick provided at rink. ",2,N,$20.00 +2026Q3,PE.0703-1,"Soccer, Beginner",15,MW,11:00 AM,Zesiger MAC Court,2/11/2026,3/18/2026,This course will be held indoors.,Court shoes recommended for indoor play. Workout clothes. ,2,N,$0.00 +2026Q3,PE.0800-1,Aikido,20,TR,1:00 PM,Du Pont Wrestling Room,2/10/2026,3/19/2026,None,Workout clothes,2,N,$0.00 +2026Q3,PE.0922-1,"Parkour, Beginner",16,F,1:15 PM,Zesiger MAC Court,2/13/2026,3/20/2026,"2/13, 2/20, 2/27, 3/6, 3/13, 3/20**(** Ends in Q4). Time: 1:15p-2:45p Registration is pending until all forms sent from PE&W office have been completed by Mon, 2/9 by 5p. Forms will be sent from the PE&W office using the student's MIT email by the close of online registration. Check SPAM folders if emails are being forwarded from an MIT email account.",Workout clothes. Court shoes recommended.,2,N,$75.00 From c41a4f073059c6a54a0af5eb00f3ce6d4940bc74 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:16:48 -0500 Subject: [PATCH 06/89] start slapping the two together --- scrapers/pe.py | 2 +- src/lib/hydrant.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scrapers/pe.py b/scrapers/pe.py index 70e0f8ee..23f12274 100644 --- a/scrapers/pe.py +++ b/scrapers/pe.py @@ -404,7 +404,7 @@ def run(): fname = os.path.join(os.path.dirname(__file__), f"pe-q{quarter}.json") with open(fname, "w", encoding="utf-8") as pe_output_file: - json.dump(quarter_data, pe_output_file, ensure_ascii=False, indent=4) + json.dump(quarter_data, pe_output_file) return pe_data diff --git a/src/lib/hydrant.ts b/src/lib/hydrant.ts index e5eff8a4..f0bc4cb7 100644 --- a/src/lib/hydrant.ts +++ b/src/lib/hydrant.ts @@ -6,11 +6,13 @@ import type { RawClass } from "../lib/rawClass"; import type { HydrantState } from "../lib/schema"; import { DEFAULT_STATE } from "../lib/schema"; import type { State } from "../lib/state"; +import type { RawPEClass } from "./rawPEClass"; export interface SemesterData { classes: Record; lastUpdated: string; termInfo: TermInfo; + pe?: RawPEClass[]; } /** Fetch from the url, which is JSON of type T. */ From cd1caab0b6802bd90573c9ea364c27d6898e454f Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:19:56 -0500 Subject: [PATCH 07/89] fix type --- src/lib/hydrant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/hydrant.ts b/src/lib/hydrant.ts index f0bc4cb7..6c67ebb6 100644 --- a/src/lib/hydrant.ts +++ b/src/lib/hydrant.ts @@ -12,7 +12,7 @@ export interface SemesterData { classes: Record; lastUpdated: string; termInfo: TermInfo; - pe?: RawPEClass[]; + pe?: Record[]; } /** Fetch from the url, which is JSON of type T. */ From ad0125f89a5fc18870e0a949d600beb5604c0f92 Mon Sep 17 00:00:00 2001 From: Pratyush Venkatakrishnan Date: Thu, 22 Jan 2026 21:23:40 -0500 Subject: [PATCH 08/89] Standardize quarter naming convention --- scrapers/pe/s26-h1pe.csv | 69 ---------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 scrapers/pe/s26-h1pe.csv diff --git a/scrapers/pe/s26-h1pe.csv b/scrapers/pe/s26-h1pe.csv deleted file mode 100644 index 38ac288b..00000000 --- a/scrapers/pe/s26-h1pe.csv +++ /dev/null @@ -1,69 +0,0 @@ -Term,Section,Title,Capacity,Day,Start time,End time,Location,Start Date,End Date,Description,Prerequisites,Equipment,GIR Points,Swim GIR,Fee Amount -2026Q3,PE.0201-1,SCUBA Diving,18,T,6:45 PM,,Alumni Wang Pool and 66-160,2/24/2026,4/14/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. Attendance is required on the first and last day.","All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 -2026Q3,PE.0201-2,SCUBA Diving,18,R,6:45 PM,,Alumni Wang Pool and 66-160,2/19/2026,4/9/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. Attendance is required on the first and last day.","All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 -2026Q3,PE.0202-1,"Swimming, Beginner",10,MW,11:00 AM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-2,"Swimming, Beginner",10,MW,1:00 PM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-3,"Swimming, Beginner",10,MW,2:00 PM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-4,"Swimming, Beginner",10,TR,11:00 AM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-5,"Swimming, Beginner",10,TR,1:00 PM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0202-6,"Swimming, Beginner",10,TR,2:00 PM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 -2026Q3,PE.0300-1,Ballroom,20,TR,7:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0403-1,Group Exercise - Cardio Kickboxing,20,MW,6:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,"Workout clothes, footwear and water bottle",2,N,$0.00 -2026Q3,PE.0405-1,Group Exercise - Pilates,20,TR,3:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0411-1,Group Exercise- Yoga,20,MW,8:00 AM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0411-2,Group Exercise- Yoga,20,MW,5:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0411-3,Group Exercise- Yoga,20,TR,5:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0414-1,Weight Training,16,MW,11:00 AM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-2,Weight Training,16,MW,1:00 PM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-3,Weight Training,16,MW,2:00 PM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-4,Weight Training,16,TR,11:00 AM,,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0414-5,Weight Training,16,TR,2:00 PM,,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0432-1,Group Exercise- Barre Fitness,15,TR,2:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,Workout clothes and water bottle.,2,N,$0.00 -2026Q3,PE.0435-1,Group Exercise- Functional Fitness,20,MW,3:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0444-1,Group Exercise- HIIT,20,TR,6:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 -2026Q3,PE.0517-1,Fitness (Yoga)/CPR/First Aid,12,MW,4:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,"In person class starts Tue, Feb. 17 (switch day)",Q3 2026: Students must complete the remote asynchronous CPR content and in-person CPR and FA exam sessions to become CPR/First Aid certified and students must be able to kneel and use 2 arms to give compressions.,Workout clothes and water bottle. Lab fee covers pocket mask and CPR and first aid certification cards,2,N,$60.00 -2026Q3,PE.0518-1,Fitness (Yoga)/Meditation,16,TR,6:00 PM,,Du Pont Multi-Purpose Room,2/10/2026,3/19/2026,,None,Workout clothes and a filled water bottle.,2,N,$0.00 -2026Q3,PE.0529-1,Fitness(Yoga)/Meditation (remote synchronous),15,MW,7:00 PM,,Remote Synchronous,2/11/2026,3/18/2026,,"This remote synchronous course requires internet access, computer (or tablet, mobile device) with a camera, microphone, and working speaker, MIT Zoom account, roughly 6 foot x 6 foot physical area clear of any objects with a standard 7 - 8 foot ceiling and non-slip floor to do physical activity, comfortable with using 'camera on' function during Zoom sessions. ","Workout clothing and filled water bottle, mat or towel. See other prerequisites.",2,N,$0.00 -2026Q3,PE.0544-1,Fitness(Strength Circuit)/Nutrition,16,MW,5:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,"Sneakers/footwear, comfortable workout clothing and water bottle. -",2,N,$0.00 -2026Q3,PE.0545-1,Fitness(Strength Circuit)/Resiliency,16,MW,3:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,Workout clothes and filled water bottle.,2,N,$0.00 -2026Q3,PE.0546-1,Fitness(Strength Circuit)/Stress Management,16,MW,6:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,"Sneakers/footwear, comfortable workout clothing and water bottle.",2,N,$0.00 -2026Q3,PE.0600-1,Archery,14,TR,10:00 AM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-2,Archery,14,TR,11:00 AM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-3,Archery,14,TR,1:00 PM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-4,Archery,14,TR,2:00 PM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-5,Archery,14,MW,1:00 PM,,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0600-6,Archery,14,MW,2:00 PM,,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 -2026Q3,PE.0601-1,Badminton,16,TR,1:00 PM,,Rockwell Cage South,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0601-2,Badminton,16,TR,2:00 PM,,Rockwell Cage South,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0603-1,"Fencing, Sabre",16,TR,3:00 PM,,Du Pont Fencing Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,Workout clothes,2,N,$15.00 -2026Q3,PE.0608-1,Pistol,14,TR,1:00 PM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. -",,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0608-2,Pistol,14,TR,2:00 PM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. -",,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0609-1,"Pistol, Intermediate",14,TR,11:00 AM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Student must attend first 4 classes, though attendance at all classes is strongly recommended.",Student must have successfully completed the MIT PEandW Beginner Pistol Course.,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor. - -",2,N,$35.00 -2026Q3,PE.0612-1,"Skate, Beginner",20,MW,1:00 PM,,Johnson Ice Rink 1,2/11/2026,3/18/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0612-2,"Skate, Beginner",20,TR,11:00 AM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0612-3,"Skate, Beginner",20,TR,1:00 PM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0612-4,"Skate, Beginner",20,TR,2:00 PM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-1,"Skate, Intermediate",15,MW,1:00 PM,,Johnson Ice Rink 2,2/11/2026,3/18/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-2,"Skate, Intermediate",15,TR,11:00 AM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-3,"Skate, Intermediate",15,TR,1:00 PM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0613-4,"Skate, Intermediate",15,TR,2:00 PM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 -2026Q3,PE.0616-1,Squash,12,MW,11:00 AM,,Zesiger Squash Courts,2/11/2026,3/18/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0616-2,Squash,12,TR,1:00 PM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0616-3,Squash,12,TR,2:00 PM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0617-1,"Squash, Intermediate",12,TR,11:00 AM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,"Completion of Beginner Squash class or had experience in high school or club. Please email instructor at bbubna@mit.edu if you are not sure regarding your ability or if you have any questions -","Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. Racquet, ball and eye protection are provided.",2,N,$10.00 -2026Q3,PE.0626-1,Rifle,14,MW,11:00 AM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0626-2,Rifle,14,MW,1:00 PM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0626-3,Rifle,14,MW,2:00 PM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 -2026Q3,PE.0636-1,Self-Defense for Women,20,MW,1:00 PM,,Du Pont Wrestling Room,2/11/2026,3/18/2026,,This is an all female course.,None,2,N,$0.00 -2026Q3,PE.0646-1,Pickleball,16,MW,11:00 AM,,Rockwell Cage South,2/11/2026,3/18/2026,,None,"Work out clothes, footwear, and a filled water bottle. ",2,N,$10.00 -2026Q3,PE.0657-1,Spec Tennis,16,MW,2:00 PM,,Rockwell Cage South,2/11/2026,3/18/2026,,None,Comfortable clothing and footwear.,2,N,$10.00 -2026Q3,PE.0701-1,Ice Hockey,20,MW,2:00 PM,,Johnson Ice Rink,2/11/2026,3/18/2026,,This course requires a command of forward and backward skating as well as a strong consistent stop that can be learned in beginner skate or equivalent (email instructor using physicaleducationandwellness@mit.edu address if you have questions related to your ability).,"Ice hockey skates, helmet, shin guards, gloves and hockey stick provided at rink. ",2,N,$20.00 -2026Q3,PE.0703-1,"Soccer, Beginner",15,MW,11:00 AM,,Zesiger MAC Court,2/11/2026,3/18/2026,,This course will be held indoors.,Court shoes recommended for indoor play. Workout clothes. ,2,N,$0.00 -2026Q3,PE.0800-1,Aikido,20,TR,1:00 PM,,Du Pont Wrestling Room,2/10/2026,3/19/2026,,None,Workout clothes,2,N,$0.00 -2026Q3,PE.0922-1,"Parkour, Beginner",16,F,1:15 PM,2:45 PM,Zesiger MAC Court,2/13/2026,3/20/2026,"2/13, 2/20, 2/27, 3/6, 3/13, 3/20**(** Ends in Q4). Registration is pending until all forms sent from PE&W office have been completed by Mon, 2/9 by 5p. Forms will be sent from the PE&W office using the student's MIT email by the close of online registration. Check SPAM folders if emails are being forwarded from an MIT email account.",,Workout clothes. Court shoes recommended.,2,N,$75.00 From 56051b2ffa636c1c030b8632fdced72d20c6a81d Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:26:33 -0500 Subject: [PATCH 09/89] also this --- src/lib/rawPEClass.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/rawPEClass.ts b/src/lib/rawPEClass.ts index d36316a3..8b3be9e7 100644 --- a/src/lib/rawPEClass.ts +++ b/src/lib/rawPEClass.ts @@ -31,4 +31,6 @@ export interface RawPEClass { fee: number; /** Description, no specific format */ description: string; + /** Quarter of class */ + quarter: number; } From 693a42340779bfea3401ce9a798cad5ce6afa65a Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:26:59 -0500 Subject: [PATCH 10/89] put all of these together (will add PE ones rn) --- src/components/ButtonsLinks.tsx | 82 +++++++++++++++++++++++++++++++++ src/components/ClassTypes.tsx | 3 +- src/components/Header.tsx | 2 +- src/components/MatrixLink.tsx | 39 ---------------- src/components/PreregLink.tsx | 38 --------------- src/components/SIPBLogo.tsx | 14 ------ 6 files changed, 84 insertions(+), 94 deletions(-) create mode 100644 src/components/ButtonsLinks.tsx delete mode 100644 src/components/MatrixLink.tsx delete mode 100644 src/components/PreregLink.tsx delete mode 100644 src/components/SIPBLogo.tsx diff --git a/src/components/ButtonsLinks.tsx b/src/components/ButtonsLinks.tsx new file mode 100644 index 00000000..ba4f9e09 --- /dev/null +++ b/src/components/ButtonsLinks.tsx @@ -0,0 +1,82 @@ +import { useContext } from "react"; + +import { Class } from "../lib/class"; +import { HydrantContext } from "../lib/hydrant"; + +import { LuMessagesSquare, LuClipboardCopy } from "react-icons/lu"; +import { Tooltip } from "./ui/tooltip"; +import { Link } from "react-router"; +import { Button, Image, Link as ChakraLink } from "@chakra-ui/react"; + +import sipbLogo from "../assets/simple-fuzzball.png"; + +/** A link to SIPB Matrix's class group chat importer UI */ +export function MatrixLink() { + const { + state: { selectedActivities }, + } = useContext(HydrantContext); + + // reference: https://github.com/gabrc52/class_group_chats/tree/main/src/routes/import + const matrixLink = `https://matrix.mit.edu/classes/import?via=Hydrant${selectedActivities + .filter((activity) => activity instanceof Class) + .map((cls) => `&class=${cls.number}`) + .join("")}`; + + return ( + + + + ); +} + +/** A link to SIPB Matrix's class group chat importer UI */ +export function PreregLink() { + const { + state: { selectedActivities }, + } = useContext(HydrantContext); + + // reference: https://github.com/gabrc52/class_group_chats/tree/main/src/routes/import + const preregLink = `https://student.mit.edu/cgi-bin/sfprwtrm.sh?${selectedActivities + .filter((activity) => activity instanceof Class) + .map((cls) => cls.number) + .join(",")}`; + + return ( + + + + ); +} + +export function SIPBLogo() { + return ( + + + by SIPB + SIPB Logo + + + ); +} diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx index e61fa1ea..5211679a 100644 --- a/src/components/ClassTypes.tsx +++ b/src/components/ClassTypes.tsx @@ -1,8 +1,7 @@ import { Button, ButtonGroup, Center, Stack } from "@chakra-ui/react"; import { ActivityDescription } from "./ActivityDescription"; import { ClassTable } from "./ClassTable"; -import { MatrixLink } from "./MatrixLink"; -import { PreregLink } from "./PreregLink"; +import { MatrixLink, PreregLink } from "./ButtonsLinks"; import { SelectedActivities } from "./SelectedActivities"; import { Tooltip } from "./ui/tooltip"; import { useContext, useState } from "react"; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index a06f0bd0..c82e8be8 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -24,7 +24,7 @@ import { HydrantContext } from "../lib/hydrant"; import logo from "../assets/logo.svg"; import logoDark from "../assets/logo-dark.svg"; import hydraAnt from "../assets/hydraAnt.png"; -import { SIPBLogo } from "./SIPBLogo"; +import { SIPBLogo } from "./ButtonsLinks"; export function PreferencesDialog() { const { state, hydrantState } = useContext(HydrantContext); diff --git a/src/components/MatrixLink.tsx b/src/components/MatrixLink.tsx deleted file mode 100644 index 53be5853..00000000 --- a/src/components/MatrixLink.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useContext } from "react"; - -import { Class } from "../lib/class"; -import { HydrantContext } from "../lib/hydrant"; - -import { LuMessagesSquare } from "react-icons/lu"; -import { Tooltip } from "./ui/tooltip"; -import { Link } from "react-router"; -import { Button } from "@chakra-ui/react"; - -/** A link to SIPB Matrix's class group chat importer UI */ -export function MatrixLink() { - const { - state: { selectedActivities }, - } = useContext(HydrantContext); - - // reference: https://github.com/gabrc52/class_group_chats/tree/main/src/routes/import - const matrixLink = `https://matrix.mit.edu/classes/import?via=Hydrant${selectedActivities - .filter((activity) => activity instanceof Class) - .map((cls) => `&class=${cls.number}`) - .join("")}`; - - return ( - - - - ); -} diff --git a/src/components/PreregLink.tsx b/src/components/PreregLink.tsx deleted file mode 100644 index 3579da50..00000000 --- a/src/components/PreregLink.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Class } from "../lib/class"; -import { LuClipboardCopy } from "react-icons/lu"; - -import { Tooltip } from "./ui/tooltip"; -import { useContext } from "react"; -import { HydrantContext } from "../lib/hydrant"; -import { Link } from "react-router"; -import { Button } from "@chakra-ui/react"; - -/** A link to SIPB Matrix's class group chat importer UI */ -export function PreregLink() { - const { - state: { selectedActivities }, - } = useContext(HydrantContext); - - // reference: https://github.com/gabrc52/class_group_chats/tree/main/src/routes/import - const preregLink = `https://student.mit.edu/cgi-bin/sfprwtrm.sh?${selectedActivities - .filter((activity) => activity instanceof Class) - .map((cls) => cls.number) - .join(",")}`; - - return ( - - - - ); -} diff --git a/src/components/SIPBLogo.tsx b/src/components/SIPBLogo.tsx deleted file mode 100644 index 85a45705..00000000 --- a/src/components/SIPBLogo.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Image, Link as ChakraLink } from "@chakra-ui/react"; -import { Link } from "react-router"; -import sipbLogo from "../assets/simple-fuzzball.png"; - -export function SIPBLogo() { - return ( - - - by SIPB - SIPB Logo - - - ); -} From db5b6a09d25243497a5b3469c3c161e66350199c Mon Sep 17 00:00:00 2001 From: Pratyush Venkatakrishnan Date: Thu, 22 Jan 2026 21:31:38 -0500 Subject: [PATCH 11/89] Actually add the file back --- scrapers/pe/pe-q3.csv | 69 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 scrapers/pe/pe-q3.csv diff --git a/scrapers/pe/pe-q3.csv b/scrapers/pe/pe-q3.csv new file mode 100644 index 00000000..38ac288b --- /dev/null +++ b/scrapers/pe/pe-q3.csv @@ -0,0 +1,69 @@ +Term,Section,Title,Capacity,Day,Start time,End time,Location,Start Date,End Date,Description,Prerequisites,Equipment,GIR Points,Swim GIR,Fee Amount +2026Q3,PE.0201-1,SCUBA Diving,18,T,6:45 PM,,Alumni Wang Pool and 66-160,2/24/2026,4/14/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. Attendance is required on the first and last day.","All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 +2026Q3,PE.0201-2,SCUBA Diving,18,R,6:45 PM,,Alumni Wang Pool and 66-160,2/19/2026,4/9/2026,"Q3 2026.Tue Dates: 2/24, 3/3, 3/10, 3/17**, 3/31**, 4/7**, 4/14** (** Ends in Q4). Thu Dates: 2/19, 2/26, 3/5, 3/12, 3/19**, 4/2**, 4/9**(** Ends in Q4). Meet at Alumni Wang pool/Classroom 66-160 after. Attendance is required on the first and last day.","All participants must complete PE&W Swim/Boat test by 2/4 to register along with passing SCUBA pre-test on day 1, able to lift 40 lbs and in good health. Must complete all documentation forms from PE&W to confirm registration by 2/9 @ 5p.","Bathing suit or shorts and shirt. Equipment provided by United Divers for pool sessions. A mask, booties, fins and a snorkel must be purchased for open water dives. ",4,N,$365.00 +2026Q3,PE.0202-1,"Swimming, Beginner",10,MW,11:00 AM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-2,"Swimming, Beginner",10,MW,1:00 PM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-3,"Swimming, Beginner",10,MW,2:00 PM,,Zesiger Teaching Pool,2/11/2026,3/18/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-4,"Swimming, Beginner",10,TR,11:00 AM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-5,"Swimming, Beginner",10,TR,1:00 PM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0202-6,"Swimming, Beginner",10,TR,2:00 PM,,Zesiger Teaching Pool,2/10/2026,3/19/2026,,None,Swim attire needed. Students should bring a filled water bottle and towel. It is recommended to leave your clothing/bags in a locked locker or bring with you on the pool deck.,2,Y,$20.00 +2026Q3,PE.0300-1,Ballroom,20,TR,7:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0403-1,Group Exercise - Cardio Kickboxing,20,MW,6:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,"Workout clothes, footwear and water bottle",2,N,$0.00 +2026Q3,PE.0405-1,Group Exercise - Pilates,20,TR,3:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0411-1,Group Exercise- Yoga,20,MW,8:00 AM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0411-2,Group Exercise- Yoga,20,MW,5:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0411-3,Group Exercise- Yoga,20,TR,5:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0414-1,Weight Training,16,MW,11:00 AM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-2,Weight Training,16,MW,1:00 PM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-3,Weight Training,16,MW,2:00 PM,,Du Pont Varsity Weight Room,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-4,Weight Training,16,TR,11:00 AM,,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0414-5,Weight Training,16,TR,2:00 PM,,Du Pont Varsity Weight Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0432-1,Group Exercise- Barre Fitness,15,TR,2:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,Workout clothes and water bottle.,2,N,$0.00 +2026Q3,PE.0435-1,Group Exercise- Functional Fitness,20,MW,3:00 PM,,Du Pont T Club Lounge,2/11/2026,3/18/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0444-1,Group Exercise- HIIT,20,TR,6:00 PM,,Du Pont T Club Lounge,2/10/2026,3/19/2026,,None,"Workout clothes, footwear and water bottle.",2,N,$0.00 +2026Q3,PE.0517-1,Fitness (Yoga)/CPR/First Aid,12,MW,4:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,"In person class starts Tue, Feb. 17 (switch day)",Q3 2026: Students must complete the remote asynchronous CPR content and in-person CPR and FA exam sessions to become CPR/First Aid certified and students must be able to kneel and use 2 arms to give compressions.,Workout clothes and water bottle. Lab fee covers pocket mask and CPR and first aid certification cards,2,N,$60.00 +2026Q3,PE.0518-1,Fitness (Yoga)/Meditation,16,TR,6:00 PM,,Du Pont Multi-Purpose Room,2/10/2026,3/19/2026,,None,Workout clothes and a filled water bottle.,2,N,$0.00 +2026Q3,PE.0529-1,Fitness(Yoga)/Meditation (remote synchronous),15,MW,7:00 PM,,Remote Synchronous,2/11/2026,3/18/2026,,"This remote synchronous course requires internet access, computer (or tablet, mobile device) with a camera, microphone, and working speaker, MIT Zoom account, roughly 6 foot x 6 foot physical area clear of any objects with a standard 7 - 8 foot ceiling and non-slip floor to do physical activity, comfortable with using 'camera on' function during Zoom sessions. ","Workout clothing and filled water bottle, mat or towel. See other prerequisites.",2,N,$0.00 +2026Q3,PE.0544-1,Fitness(Strength Circuit)/Nutrition,16,MW,5:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,"Sneakers/footwear, comfortable workout clothing and water bottle. +",2,N,$0.00 +2026Q3,PE.0545-1,Fitness(Strength Circuit)/Resiliency,16,MW,3:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,Workout clothes and filled water bottle.,2,N,$0.00 +2026Q3,PE.0546-1,Fitness(Strength Circuit)/Stress Management,16,MW,6:00 PM,,Du Pont Multi-Purpose Room,2/11/2026,3/18/2026,,None,"Sneakers/footwear, comfortable workout clothing and water bottle.",2,N,$0.00 +2026Q3,PE.0600-1,Archery,14,TR,10:00 AM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-2,Archery,14,TR,11:00 AM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-3,Archery,14,TR,1:00 PM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-4,Archery,14,TR,2:00 PM,,Rockwell Cage North,2/10/2026,3/19/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-5,Archery,14,MW,1:00 PM,,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0600-6,Archery,14,MW,2:00 PM,,Rockwell Cage North,2/11/2026,3/18/2026,Students must attend first 4 classes.,,"Work out clothes, footwear, and a filled water bottle. ",2,N,$15.00 +2026Q3,PE.0601-1,Badminton,16,TR,1:00 PM,,Rockwell Cage South,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0601-2,Badminton,16,TR,2:00 PM,,Rockwell Cage South,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(court shoes preferred), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0603-1,"Fencing, Sabre",16,TR,3:00 PM,,Du Pont Fencing Room,2/10/2026,3/19/2026,Students must attend first 4 classes.,,Workout clothes,2,N,$15.00 +2026Q3,PE.0608-1,Pistol,14,TR,1:00 PM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. +",,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0608-2,Pistol,14,TR,2:00 PM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Students must attend first 4 classes, though attendance at all classes is strongly recommended. +",,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0609-1,"Pistol, Intermediate",14,TR,11:00 AM,,Du Pont Pistol Range,2/10/2026,3/19/2026,"Student must attend first 4 classes, though attendance at all classes is strongly recommended.",Student must have successfully completed the MIT PEandW Beginner Pistol Course.,"Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor. + +",2,N,$35.00 +2026Q3,PE.0612-1,"Skate, Beginner",20,MW,1:00 PM,,Johnson Ice Rink 1,2/11/2026,3/18/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-2,"Skate, Beginner",20,TR,11:00 AM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-3,"Skate, Beginner",20,TR,1:00 PM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0612-4,"Skate, Beginner",20,TR,2:00 PM,,Johnson Ice Rink 1,2/10/2026,3/19/2026,,None,Skates and a helmet- provided at the rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-1,"Skate, Intermediate",15,MW,1:00 PM,,Johnson Ice Rink 2,2/11/2026,3/18/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-2,"Skate, Intermediate",15,TR,11:00 AM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-3,"Skate, Intermediate",15,TR,1:00 PM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0613-4,"Skate, Intermediate",15,TR,2:00 PM,,Johnson Ice Rink 2,2/10/2026,3/19/2026,,"Prior skate experience. Students must be able to skate forward, backward and stop.",Skates and helmet- provided at rink. Warm clothes and gloves/mittens.,2,N,$20.00 +2026Q3,PE.0616-1,Squash,12,MW,11:00 AM,,Zesiger Squash Courts,2/11/2026,3/18/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0616-2,Squash,12,TR,1:00 PM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0616-3,Squash,12,TR,2:00 PM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,None,"Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0617-1,"Squash, Intermediate",12,TR,11:00 AM,,Zesiger Squash Courts,2/10/2026,3/19/2026,,"Completion of Beginner Squash class or had experience in high school or club. Please email instructor at bbubna@mit.edu if you are not sure regarding your ability or if you have any questions +","Work out clothes, footwear(non-marking or gum soled), and a filled water bottle. Racquet, ball and eye protection are provided.",2,N,$10.00 +2026Q3,PE.0626-1,Rifle,14,MW,11:00 AM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0626-2,Rifle,14,MW,1:00 PM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0626-3,Rifle,14,MW,2:00 PM,,Du Pont Pistol Range,2/11/2026,3/18/2026,,"Students must attend first 4 classes, though attendance at all classes is strongly recommended.","Baseball style hats (old fashioned type with the brim to the front, not the rear), are mandatory. All other equipment will be provided. If students own their own protective shooting glasses and/or hearing protection they may use same after approval by the instructor.",2,N,$35.00 +2026Q3,PE.0636-1,Self-Defense for Women,20,MW,1:00 PM,,Du Pont Wrestling Room,2/11/2026,3/18/2026,,This is an all female course.,None,2,N,$0.00 +2026Q3,PE.0646-1,Pickleball,16,MW,11:00 AM,,Rockwell Cage South,2/11/2026,3/18/2026,,None,"Work out clothes, footwear, and a filled water bottle. ",2,N,$10.00 +2026Q3,PE.0657-1,Spec Tennis,16,MW,2:00 PM,,Rockwell Cage South,2/11/2026,3/18/2026,,None,Comfortable clothing and footwear.,2,N,$10.00 +2026Q3,PE.0701-1,Ice Hockey,20,MW,2:00 PM,,Johnson Ice Rink,2/11/2026,3/18/2026,,This course requires a command of forward and backward skating as well as a strong consistent stop that can be learned in beginner skate or equivalent (email instructor using physicaleducationandwellness@mit.edu address if you have questions related to your ability).,"Ice hockey skates, helmet, shin guards, gloves and hockey stick provided at rink. ",2,N,$20.00 +2026Q3,PE.0703-1,"Soccer, Beginner",15,MW,11:00 AM,,Zesiger MAC Court,2/11/2026,3/18/2026,,This course will be held indoors.,Court shoes recommended for indoor play. Workout clothes. ,2,N,$0.00 +2026Q3,PE.0800-1,Aikido,20,TR,1:00 PM,,Du Pont Wrestling Room,2/10/2026,3/19/2026,,None,Workout clothes,2,N,$0.00 +2026Q3,PE.0922-1,"Parkour, Beginner",16,F,1:15 PM,2:45 PM,Zesiger MAC Court,2/13/2026,3/20/2026,"2/13, 2/20, 2/27, 3/6, 3/13, 3/20**(** Ends in Q4). Registration is pending until all forms sent from PE&W office have been completed by Mon, 2/9 by 5p. Forms will be sent from the PE&W office using the student's MIT email by the close of online registration. Check SPAM folders if emails are being forwarded from an MIT email account.",,Workout clothes. Court shoes recommended.,2,N,$75.00 From a05d18e960a461cb1fe17ddef74f0bc7f889c54d Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:38:36 -0500 Subject: [PATCH 12/89] fix type --- src/lib/rawPEClass.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/rawPEClass.ts b/src/lib/rawPEClass.ts index 8b3be9e7..6886d3ea 100644 --- a/src/lib/rawPEClass.ts +++ b/src/lib/rawPEClass.ts @@ -28,7 +28,7 @@ export interface RawPEClass { /** Equipment, no specific format */ equipment: string; /** Fee, in dollars */ - fee: number; + fee: string; /** Description, no specific format */ description: string; /** Quarter of class */ From 8f9efb730339a16b34d07c65875d8af6c2dc3781 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:42:33 -0500 Subject: [PATCH 13/89] move export calendar --- src/components/ButtonsLinks.tsx | 44 +++++++++++++++++++++++++++++++-- src/components/ClassTypes.tsx | 44 +++------------------------------ 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/src/components/ButtonsLinks.tsx b/src/components/ButtonsLinks.tsx index ba4f9e09..7694d07a 100644 --- a/src/components/ButtonsLinks.tsx +++ b/src/components/ButtonsLinks.tsx @@ -1,15 +1,55 @@ -import { useContext } from "react"; +import { useContext, useState } from "react"; import { Class } from "../lib/class"; import { HydrantContext } from "../lib/hydrant"; +import { useICSExport } from "../lib/gapi"; -import { LuMessagesSquare, LuClipboardCopy } from "react-icons/lu"; +import { + LuMessagesSquare, + LuClipboardCopy, + LuCalendarArrowDown, +} from "react-icons/lu"; import { Tooltip } from "./ui/tooltip"; import { Link } from "react-router"; import { Button, Image, Link as ChakraLink } from "@chakra-ui/react"; import sipbLogo from "../assets/simple-fuzzball.png"; +export function ExportCalendar() { + const { state } = useContext(HydrantContext); + + const [isExporting, setIsExporting] = useState(false); + // TODO: fix gcal export + const onICSExport = useICSExport( + state, + () => { + setIsExporting(false); + }, + () => { + setIsExporting(false); + }, + ); + + return ( + + + + ); +} + /** A link to SIPB Matrix's class group chat importer UI */ export function MatrixLink() { const { diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx index 5211679a..14b511f3 100644 --- a/src/components/ClassTypes.tsx +++ b/src/components/ClassTypes.tsx @@ -1,54 +1,18 @@ -import { Button, ButtonGroup, Center, Stack } from "@chakra-ui/react"; +import { ButtonGroup, Center, Stack } from "@chakra-ui/react"; import { ActivityDescription } from "./ActivityDescription"; import { ClassTable } from "./ClassTable"; -import { MatrixLink, PreregLink } from "./ButtonsLinks"; +import { MatrixLink, PreregLink, ExportCalendar } from "./ButtonsLinks"; import { SelectedActivities } from "./SelectedActivities"; -import { Tooltip } from "./ui/tooltip"; -import { useContext, useState } from "react"; -import { HydrantContext } from "~/lib/hydrant"; -import { useICSExport } from "~/lib/gapi"; import { ClassType } from "~/lib/schema"; -import { - LuCalendarArrowDown, - LuGraduationCap, - LuVolleyball, -} from "react-icons/lu"; +import { LuGraduationCap, LuVolleyball } from "react-icons/lu"; import type { IconType } from "react-icons/lib"; export const Academic = () => { - const { state } = useContext(HydrantContext); - - const [isExporting, setIsExporting] = useState(false); - // TODO: fix gcal export - const onICSExport = useICSExport( - state, - () => { - setIsExporting(false); - }, - () => { - setIsExporting(false); - }, - ); return (
- - - + From 64e5c7245d3bcb54ccb18049e8de0fdc9473c10c Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:46:42 -0500 Subject: [PATCH 14/89] move some things out of tab --- src/components/ClassTypes.tsx | 12 +----------- src/routes/_index.tsx | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx index 14b511f3..06b88367 100644 --- a/src/components/ClassTypes.tsx +++ b/src/components/ClassTypes.tsx @@ -1,7 +1,5 @@ -import { ButtonGroup, Center, Stack } from "@chakra-ui/react"; -import { ActivityDescription } from "./ActivityDescription"; +import { Stack } from "@chakra-ui/react"; import { ClassTable } from "./ClassTable"; -import { MatrixLink, PreregLink, ExportCalendar } from "./ButtonsLinks"; import { SelectedActivities } from "./SelectedActivities"; import { ClassType } from "~/lib/schema"; import { LuGraduationCap, LuVolleyball } from "react-icons/lu"; @@ -10,16 +8,8 @@ import type { IconType } from "react-icons/lib"; export const Academic = () => { return ( -
- - - - - -
-
); }; diff --git a/src/routes/_index.tsx b/src/routes/_index.tsx index 55cc5229..6c8addc0 100644 --- a/src/routes/_index.tsx +++ b/src/routes/_index.tsx @@ -1,4 +1,4 @@ -import { Center, Flex, Group, Tabs } from "@chakra-ui/react"; +import { Center, Flex, Group, Tabs, ButtonGroup } from "@chakra-ui/react"; import { Calendar } from "../components/Calendar"; import { LeftFooter } from "../components/Footers"; import { Header, PreferencesDialog } from "../components/Header"; @@ -6,7 +6,12 @@ import { ScheduleOption } from "../components/ScheduleOption"; import { ScheduleSwitcher } from "../components/ScheduleSwitcher"; import { TermSwitcher } from "../components/TermSwitcher"; import { Banner } from "../components/Banner"; -import { CLASS_TYPE_COMPONENTS } from "~/components/ClassTypes"; +import { + MatrixLink, + PreregLink, + ExportCalendar, +} from "../components/ButtonsLinks"; +import { CLASS_TYPE_COMPONENTS } from "../components/ClassTypes"; import { State } from "../lib/state"; import { Term } from "../lib/dates"; @@ -17,6 +22,7 @@ import { getClosestUrlName, type LatestTermInfo } from "../lib/dates"; import type { Route } from "./+types/_index"; import { useContext } from "react"; import type { ClassType } from "~/lib/schema"; +import { ActivityDescription } from "~/components/ActivityDescription"; // eslint-disable-next-line react-refresh/only-export-components export async function clientLoader({ request }: Route.ClientLoaderArgs) { @@ -85,6 +91,13 @@ function HydrantApp() {
+
+ + + + + +
+ From a091480af8981ab358ce9fdd6ff7d285b5e57189 Mon Sep 17 00:00:00 2001 From: Pratyush Venkatakrishnan Date: Thu, 22 Jan 2026 21:54:52 -0500 Subject: [PATCH 15/89] Move SelectedActivities out of Academic tab --- src/components/ClassTypes.tsx | 2 -- src/routes/_index.tsx | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx index 06b88367..cd546f67 100644 --- a/src/components/ClassTypes.tsx +++ b/src/components/ClassTypes.tsx @@ -1,6 +1,5 @@ import { Stack } from "@chakra-ui/react"; import { ClassTable } from "./ClassTable"; -import { SelectedActivities } from "./SelectedActivities"; import { ClassType } from "~/lib/schema"; import { LuGraduationCap, LuVolleyball } from "react-icons/lu"; import type { IconType } from "react-icons/lib"; @@ -8,7 +7,6 @@ import type { IconType } from "react-icons/lib"; export const Academic = () => { return ( - ); diff --git a/src/routes/_index.tsx b/src/routes/_index.tsx index 6c8addc0..22cb372c 100644 --- a/src/routes/_index.tsx +++ b/src/routes/_index.tsx @@ -2,6 +2,7 @@ import { Center, Flex, Group, Tabs, ButtonGroup } from "@chakra-ui/react"; import { Calendar } from "../components/Calendar"; import { LeftFooter } from "../components/Footers"; import { Header, PreferencesDialog } from "../components/Header"; +import { SelectedActivities } from "../components/SelectedActivities"; import { ScheduleOption } from "../components/ScheduleOption"; import { ScheduleSwitcher } from "../components/ScheduleSwitcher"; import { TermSwitcher } from "../components/TermSwitcher"; @@ -98,6 +99,7 @@ function HydrantApp() { + Date: Thu, 22 Jan 2026 21:56:11 -0500 Subject: [PATCH 16/89] remove stack --- src/components/ClassTypes.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx index cd546f67..ddde2b43 100644 --- a/src/components/ClassTypes.tsx +++ b/src/components/ClassTypes.tsx @@ -1,4 +1,3 @@ -import { Stack } from "@chakra-ui/react"; import { ClassTable } from "./ClassTable"; import { ClassType } from "~/lib/schema"; import { LuGraduationCap, LuVolleyball } from "react-icons/lu"; @@ -6,9 +5,7 @@ import type { IconType } from "react-icons/lib"; export const Academic = () => { return ( - - - + ); }; From e45fb9333d2eeb12ffcae2bfce42879872310916 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:08:28 -0500 Subject: [PATCH 17/89] fix test --- scrapers/pe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrapers/pe.py b/scrapers/pe.py index 23f12274..fee583bc 100644 --- a/scrapers/pe.py +++ b/scrapers/pe.py @@ -365,7 +365,7 @@ def get_pe_files(url_name: str) -> list[str]: ['pe-q1.json', 'pe-q2.json'] >>> get_pe_files("i26") - ['pe-q3.json'] + ['pe-q5.json'] """ assert url_name[0] in ("f", "i", "s", "m"), "Invalid urlName format" From 5a8c7a3d003d2e8d0a6412cf690e7243657b33f4 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:09:13 -0500 Subject: [PATCH 18/89] change nonclass to custom activity --- src/components/ActivityButtons.tsx | 12 +- src/components/ActivityDescription.tsx | 10 +- src/components/Calendar.tsx | 6 +- src/lib/activity.ts | 6 +- src/lib/calendarSlots.ts | 8 +- src/lib/state.ts | 70 +++++----- tests/activity.test.ts | 182 ++++++++++++------------- 7 files changed, 147 insertions(+), 147 deletions(-) diff --git a/src/components/ActivityButtons.tsx b/src/components/ActivityButtons.tsx index 62f91160..4bedecd3 100644 --- a/src/components/ActivityButtons.tsx +++ b/src/components/ActivityButtons.tsx @@ -24,7 +24,7 @@ import { Checkbox } from "./ui/checkbox"; import { Field } from "./ui/field"; import { Radio, RadioGroup } from "./ui/radio"; -import type { Activity, NonClass } from "../lib/activity"; +import type { Activity, CustomActivity } from "../lib/activity"; import { Timeslot } from "../lib/activity"; import type { Class, SectionLockOption, Sections } from "../lib/class"; import { LockOption } from "../lib/class"; @@ -299,7 +299,7 @@ export function ClassButtons(props: { cls: Class }) { } /** Form to add a timeslot to a non-class. */ -function NonClassAddTime(props: { activity: NonClass }) { +function CustomActivityAddTime(props: { activity: CustomActivity }) { const { activity } = props; const { state } = useContext(HydrantContext); const [days, setDays] = useState( @@ -397,7 +397,7 @@ function NonClassAddTime(props: { activity: NonClass }) { /** * Buttons in non-class description to rename it, or add/edit/remove timeslots. */ -export function NonClassButtons(props: { activity: NonClass }) { +export function CustomActivityButtons(props: { activity: CustomActivity }) { const { activity } = props; const { state } = useContext(HydrantContext); @@ -453,7 +453,7 @@ export function NonClassButtons(props: { activity: NonClass }) { }; const onConfirmRename = () => { - state.renameNonClass(activity, name); + state.renameCustomActivity(activity, name); setIsRenaming(false); }; const onCancelRename = () => { @@ -461,7 +461,7 @@ export function NonClassButtons(props: { activity: NonClass }) { }; const onConfirmRelocating = () => { - state.relocateNonClass(activity, room); + state.relocateCustomActivity(activity, room); setIsRelocating(false); }; const onCancelRelocating = () => { @@ -534,7 +534,7 @@ export function NonClassButtons(props: { activity: NonClass }) { Click and drag on an empty time in the calendar to add the times for your activity. Or add one manually: - + ); } diff --git a/src/components/ActivityDescription.tsx b/src/components/ActivityDescription.tsx index 5dc2e9ab..b94f73cf 100644 --- a/src/components/ActivityDescription.tsx +++ b/src/components/ActivityDescription.tsx @@ -13,13 +13,13 @@ import { import { useColorModeValue } from "./ui/color-mode"; import { Tooltip } from "./ui/tooltip"; -import type { NonClass } from "../lib/activity"; +import type { CustomActivity } from "../lib/activity"; import type { Flags } from "../lib/class"; import { Class, DARK_IMAGES, getFlagImg } from "../lib/class"; import { linkClasses } from "../lib/utils"; import { HydrantContext } from "../lib/hydrant"; -import { ClassButtons, NonClassButtons } from "./ActivityButtons"; +import { ClassButtons, CustomActivityButtons } from "./ActivityButtons"; import { LuExternalLink } from "react-icons/lu"; /** A small image indicating a flag, like Spring or CI-H. */ @@ -246,13 +246,13 @@ function ClassDescription(props: { cls: Class }) { } /** Full non-class activity description, from title to timeslots. */ -function NonClassDescription(props: { activity: NonClass }) { +function CustomActivityDescription(props: { activity: CustomActivity }) { const { activity } = props; const { state } = useContext(HydrantContext); return ( - + {activity.timeslots.map((t) => ( @@ -283,6 +283,6 @@ export function ActivityDescription() { return activity instanceof Class ? ( ) : ( - + ); } diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index 370c66cf..50e25e60 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -9,7 +9,7 @@ import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import type { Activity } from "../lib/activity"; -import { NonClass, Timeslot } from "../lib/activity"; +import { CustomActivity, Timeslot } from "../lib/activity"; import { Slot } from "../lib/dates"; import { Class } from "../lib/class"; import { HydrantContext } from "../lib/hydrant"; @@ -99,9 +99,9 @@ export function Calendar() { } slotMaxTime="22:00:00" weekends={false} - selectable={viewedActivity instanceof NonClass} + selectable={viewedActivity instanceof CustomActivity} select={(e) => { - if (viewedActivity instanceof NonClass) { + if (viewedActivity instanceof CustomActivity) { state.addTimeslot( viewedActivity, Timeslot.fromStartEnd( diff --git a/src/lib/activity.ts b/src/lib/activity.ts index 69daca38..e61e2538 100644 --- a/src/lib/activity.ts +++ b/src/lib/activity.ts @@ -112,7 +112,7 @@ export class Event { } /** A non-class activity. */ -export class NonClass { +export class CustomActivity { /** ID unique over all Activities. */ readonly id: string; name = "New Activity"; @@ -191,5 +191,5 @@ export class NonClass { } } -/** Shared interface for Class and NonClass. */ -export type Activity = Class | NonClass; +/** Shared interface for Class and non-classes. */ +export type Activity = Class | CustomActivity; diff --git a/src/lib/calendarSlots.ts b/src/lib/calendarSlots.ts index f426dae4..6708764b 100644 --- a/src/lib/calendarSlots.ts +++ b/src/lib/calendarSlots.ts @@ -1,4 +1,4 @@ -import type { NonClass, Timeslot } from "./activity"; +import type { CustomActivity, Timeslot } from "./activity"; import type { Section, Sections, Class } from "./class"; /** @@ -65,12 +65,12 @@ function selectHelper( * @returns Object with: * options - list of schedule options; each schedule option is a list of all * sections in that schedule, including locked sections (but not including - * non-class activities.) + * custom activities.) * conflicts - number of conflicts in any option */ export function scheduleSlots( selectedClasses: Class[], - selectedNonClasses: NonClass[], + selectedCustomActivities: CustomActivity[], ): { options: Section[][]; conflicts: number; @@ -97,7 +97,7 @@ export function scheduleSlots( } } - for (const activity of selectedNonClasses) { + for (const activity of selectedCustomActivities) { initialSlots.push(...activity.timeslots); } diff --git a/src/lib/state.ts b/src/lib/state.ts index b50e7667..3ef88024 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -1,7 +1,7 @@ import { nanoid } from "nanoid"; import type { Timeslot, Activity } from "./activity"; -import { NonClass } from "./activity"; +import { CustomActivity } from "./activity"; import { scheduleSlots } from "./calendarSlots"; import type { Section, SectionLockOption, Sections } from "./class"; import { Class } from "./class"; @@ -41,8 +41,8 @@ export class State { private viewedActivity: Activity | undefined; /** Selected class activities. */ private selectedClasses: Class[] = []; - /** Selected non-class activities. */ - private selectedNonClasses: NonClass[] = []; + /** Selected custom activities. */ + private selectedCustomActivities: CustomActivity[] = []; /** Selected schedule option; zero-indexed. */ private selectedOption = 0; /** Currently loaded save slot, empty if none of them. */ @@ -80,7 +80,7 @@ export class State { /** All activities. */ get selectedActivities(): Activity[] { - return [...this.selectedClasses, ...this.selectedNonClasses]; + return [...this.selectedClasses, ...this.selectedCustomActivities]; } /** The color scheme. */ @@ -112,17 +112,17 @@ export class State { /** * Adds an activity, selects it, and updates. * - * @param activity - Activity to be added. If null, creates a new NonClass + * @param activity - Activity to be added. If null, creates a new CustomActivity * and adds it. */ addActivity(activity?: Activity): void { - const toAdd = activity ?? new NonClass(this.colorScheme); + const toAdd = activity ?? new CustomActivity(this.colorScheme); this.setViewedActivity(toAdd); if (this.isSelectedActivity(toAdd)) return; if (toAdd instanceof Class) { this.selectedClasses.push(toAdd); } else { - this.selectedNonClasses.push(toAdd); + this.selectedCustomActivities.push(toAdd); } this.updateActivities(); } @@ -135,7 +135,7 @@ export class State { (activity_) => activity_.id !== activity.id, ); } else { - this.selectedNonClasses = this.selectedNonClasses.filter( + this.selectedCustomActivities = this.selectedCustomActivities.filter( (activity_) => activity_.id !== activity.id, ); this.setViewedActivity(undefined); @@ -167,41 +167,41 @@ export class State { } //======================================================================== - // NonClass handlers + // CustomActivity handlers /** Rename a given non-activity. */ - renameNonClass(nonClass: NonClass, name: string): void { - const nonClass_ = this.selectedNonClasses.find( - (nonClass_) => nonClass_.id === nonClass.id, + renameCustomActivity(customActivity: CustomActivity, name: string): void { + const customActivity_ = this.selectedCustomActivities.find( + (customActivity_) => customActivity_.id === customActivity.id, ); - if (!nonClass_) return; + if (!customActivity_) return; - nonClass_.name = name; + customActivity_.name = name; this.updateState(); } /** Changes the room for a given non-class. */ - relocateNonClass(nonClass: NonClass, room: string | undefined): void { - const nonClass_ = this.selectedNonClasses.find( - (nonClass_) => nonClass_.id === nonClass.id, + relocateCustomActivity(customActivity: CustomActivity, room: string | undefined): void { + const customActivity_ = this.selectedCustomActivities.find( + (customActivity_) => customActivity_.id === customActivity.id, ); - if (!nonClass_) return; + if (!customActivity_) return; - nonClass_.room = room; + customActivity_.room = room; this.updateState(); } /** Add the timeslot to currently viewed activity. */ - addTimeslot(nonClass: NonClass, slot: Timeslot): void { - nonClass.addTimeslot(slot); + addTimeslot(customActivity: CustomActivity, slot: Timeslot): void { + customActivity.addTimeslot(slot); this.updateActivities(); } /** Remove all equal timeslots from currently viewed activity. */ - removeTimeslot(nonClass: NonClass, slot: Timeslot): void { - nonClass.removeTimeslot(slot); + removeTimeslot(customActivity: CustomActivity, slot: Timeslot): void { + customActivity.removeTimeslot(slot); this.updateActivities(); } @@ -251,7 +251,7 @@ export class State { */ updateActivities(save = true): void { chooseColors(this.selectedActivities, this.colorScheme); - const result = scheduleSlots(this.selectedClasses, this.selectedNonClasses); + const result = scheduleSlots(this.selectedClasses, this.selectedCustomActivities); this.options = result.options; this.conflicts = result.conflicts; this.selectOption(); @@ -269,10 +269,10 @@ export class State { !this.isSelectedActivity(cls) && (cls.sections.length === 0 || (this.selectedClasses.length === 0 && - this.selectedNonClasses.length === 0) || + this.selectedCustomActivities.length === 0) || scheduleSlots( this.selectedClasses.concat([cls]), - this.selectedNonClasses, + this.selectedCustomActivities, ).conflicts === this.conflicts) ); } @@ -336,7 +336,7 @@ export class State { /** Clear (almost) all program state. This doesn't clear class state. */ reset(): void { this.selectedClasses = []; - this.selectedNonClasses = []; + this.selectedCustomActivities = []; this.selectedOption = 0; } @@ -344,8 +344,8 @@ export class State { deflate() { return [ this.selectedClasses.map((cls) => cls.deflate()), - this.selectedNonClasses.length > 0 - ? this.selectedNonClasses.map((nonClass) => nonClass.deflate()) + this.selectedCustomActivities.length > 0 + ? this.selectedCustomActivities.map((customActivity) => customActivity.deflate()) : null, this.selectedOption, ]; @@ -364,7 +364,7 @@ export class State { ): void { if (!obj) return; this.reset(); - const [classes, nonClasses, selectedOption] = obj as [ + const [classes, customActivities, selectedOption] = obj as [ (string | number | string[])[][], (string | RawTimeslot[])[][] | null, number | undefined, @@ -378,11 +378,11 @@ export class State { cls.inflate(deflated); this.selectedClasses.push(cls); } - if (nonClasses) { - for (const deflated of nonClasses) { - const nonClass = new NonClass(this.colorScheme); - nonClass.inflate(deflated); - this.selectedNonClasses.push(nonClass); + if (customActivities) { + for (const deflated of customActivities) { + const customActivity = new CustomActivity(this.colorScheme); + customActivity.inflate(deflated); + this.selectedCustomActivities.push(customActivity); } } this.selectedOption = selectedOption ?? 0; diff --git a/tests/activity.test.ts b/tests/activity.test.ts index e54dbdb1..ec5beb22 100644 --- a/tests/activity.test.ts +++ b/tests/activity.test.ts @@ -1,5 +1,5 @@ import { expect, test, describe } from "vitest"; -import { Timeslot, NonClass, Event } from "../src/lib/activity"; +import { Timeslot, CustomActivity, Event } from "../src/lib/activity"; import { Slot } from "../src/lib/dates"; import { COLOR_SCHEME_LIGHT, @@ -121,13 +121,13 @@ describe("Timeslot", () => { }); test("Event.eventInputs", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT_CONTRAST); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT_CONTRAST); const myHexCode = "#611917"; // randomly generated hex code const myTitle = "y8g0i81"; // random keysmashes const myRoom = "ahouttoanhontjanota"; - myNonClass.backgroundColor = myHexCode; + myCustomActivity.backgroundColor = myHexCode; const myEvent: Event = new Event( - myNonClass, + myCustomActivity, myTitle, [new Timeslot(6, 7), new Timeslot(57, 10)], myRoom, @@ -142,7 +142,7 @@ test("Event.eventInputs", () => { backgroundColor: myHexCode, borderColor: myHexCode, room: myRoom, - activity: myNonClass, + activity: myCustomActivity, }, { textColor: "#ffffff", @@ -152,74 +152,74 @@ test("Event.eventInputs", () => { backgroundColor: myHexCode, borderColor: myHexCode, room: myRoom, - activity: myNonClass, + activity: myCustomActivity, }, ]); }); -describe("NonClass", () => { - describe("NonClass.constructor", () => { +describe("CustomActivity", () => { + describe("CustomActivity.constructor", () => { const nanoidRegex = /^[A-Za-z0-9-_]{8}$/; test("COLOR_SCHEME_LIGHT", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); - expect(nanoidRegex.test(myNonClass.id)).toBeTruthy(); - expect(myNonClass.backgroundColor).toBe("#4A5568"); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + expect(nanoidRegex.test(myCustomActivity.id)).toBeTruthy(); + expect(myCustomActivity.backgroundColor).toBe("#4A5568"); }); test("COLOR_SCHEME_DARK", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_DARK); - expect(nanoidRegex.test(myNonClass.id)).toBeTruthy(); - expect(myNonClass.backgroundColor).toBe("#CBD5E0"); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_DARK); + expect(nanoidRegex.test(myCustomActivity.id)).toBeTruthy(); + expect(myCustomActivity.backgroundColor).toBe("#CBD5E0"); }); test("COLOR_SCHEME_LIGHT_CONTRAST", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT_CONTRAST); - expect(nanoidRegex.test(myNonClass.id)).toBeTruthy(); - expect(myNonClass.backgroundColor).toBe("#4A5568"); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT_CONTRAST); + expect(nanoidRegex.test(myCustomActivity.id)).toBeTruthy(); + expect(myCustomActivity.backgroundColor).toBe("#4A5568"); }); test("COLOR_SCHEME_DARK_CONTRAST", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_DARK_CONTRAST); - expect(nanoidRegex.test(myNonClass.id)).toBeTruthy(); - expect(myNonClass.backgroundColor).toBe("#CBD5E0"); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_DARK_CONTRAST); + expect(nanoidRegex.test(myCustomActivity.id)).toBeTruthy(); + expect(myCustomActivity.backgroundColor).toBe("#CBD5E0"); }); }); - describe("NonClass.buttonName", () => { + describe("CustomActivity.buttonName", () => { /** Partition on this.name: changed, not changed */ - test("NonClass.name not changed", () => { - expect(new NonClass(COLOR_SCHEME_LIGHT).buttonName).toBe("New Activity"); + test("CustomActivity.name not changed", () => { + expect(new CustomActivity(COLOR_SCHEME_LIGHT).buttonName).toBe("New Activity"); }); - test("NonClass.name changed", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); + test("CustomActivity.name changed", () => { + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); const myString = "lorem ipsum dolor sit amet"; - myNonClass.name = myString; - expect(myNonClass.buttonName).toBe(myString); + myCustomActivity.name = myString; + expect(myCustomActivity.buttonName).toBe(myString); }); }); - describe("NonClass.hours", () => { + describe("CustomActivity.hours", () => { /** Partition on timeslot count: 0, 1, >1 */ test("0 timeslots", () => { - expect(new NonClass(COLOR_SCHEME_LIGHT).hours).toBe(0); + expect(new CustomActivity(COLOR_SCHEME_LIGHT).hours).toBe(0); }); test("1 timeslot", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); - myNonClass.timeslots = [new Timeslot(4, 5)]; - expect(myNonClass.hours).toBe(5 / 2); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + myCustomActivity.timeslots = [new Timeslot(4, 5)]; + expect(myCustomActivity.hours).toBe(5 / 2); }); test("multiple timeslots", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); - myNonClass.timeslots = [new Timeslot(2, 7), new Timeslot(11, 5)]; - expect(myNonClass.hours).toBe(6); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + myCustomActivity.timeslots = [new Timeslot(2, 7), new Timeslot(11, 5)]; + expect(myCustomActivity.hours).toBe(6); }); }); - test("NonClass.events", () => { + test("CustomActivity.events", () => { // arbitrary random constants const myName = "r57t68y9u"; const myTimeslots: Timeslot[] = [ @@ -228,90 +228,90 @@ describe("NonClass", () => { new Timeslot(21, 32), ]; const myRoom = "ahuotiyuwiq"; - // constructing and testing `myNonClass` - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_DARK); - myNonClass.name = myName; - myNonClass.timeslots = myTimeslots; - myNonClass.room = myRoom; - expect(myNonClass.events).toStrictEqual([ - new Event(myNonClass, myName, myTimeslots, myRoom), + // constructing and testing `myCustomActivity` + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_DARK); + myCustomActivity.name = myName; + myCustomActivity.timeslots = myTimeslots; + myCustomActivity.room = myRoom; + expect(myCustomActivity.events).toStrictEqual([ + new Event(myCustomActivity, myName, myTimeslots, myRoom), ]); }); - describe("NonClass.addTimeslot", () => { + describe("CustomActivity.addTimeslot", () => { /** Partition: * - slot matches existing timeslot, doesn't add * - slot extends over multiple days, doesn't add * - slot is valid, adds */ test("adds valid slot", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); const myTimeslot: Timeslot = new Timeslot(1, 1); - myNonClass.addTimeslot(myTimeslot); - expect(myNonClass.timeslots).toStrictEqual([myTimeslot]); + myCustomActivity.addTimeslot(myTimeslot); + expect(myCustomActivity.timeslots).toStrictEqual([myTimeslot]); }); test("doesn't add existing slot", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); const myTimeslot: Timeslot = new Timeslot(4, 6); - myNonClass.timeslots = [myTimeslot]; - myNonClass.addTimeslot(myTimeslot); - expect(myNonClass.timeslots).toStrictEqual([myTimeslot]); + myCustomActivity.timeslots = [myTimeslot]; + myCustomActivity.addTimeslot(myTimeslot); + expect(myCustomActivity.timeslots).toStrictEqual([myTimeslot]); }); test("doesn't add multi-day slot", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); - myNonClass.addTimeslot(new Timeslot(42, 69)); - expect(myNonClass.timeslots).toStrictEqual([]); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + myCustomActivity.addTimeslot(new Timeslot(42, 69)); + expect(myCustomActivity.timeslots).toStrictEqual([]); }); }); - describe("NonClass.removeTimeslot", () => { + describe("CustomActivity.removeTimeslot", () => { /** * Partition: - * - NonClass.timeslots (before call): empty, nonempty with match, nonempty without match + * - CustomActivity.timeslots (before call): empty, nonempty with match, nonempty without match */ - test("removing timeslot from empty NonClass", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); - myNonClass.removeTimeslot(new Timeslot(1, 1)); - expect(myNonClass.timeslots).toStrictEqual([]); + test("removing timeslot from empty CustomActivity", () => { + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + myCustomActivity.removeTimeslot(new Timeslot(1, 1)); + expect(myCustomActivity.timeslots).toStrictEqual([]); }); - test("remove matching timeslot from nonempty NonClass", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); + test("remove matching timeslot from nonempty CustomActivity", () => { + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); const myTimeslots: Timeslot[] = [ new Timeslot(1, 2), new Timeslot(4, 3), new Timeslot(42, 4), ]; - myNonClass.timeslots = myTimeslots; - myNonClass.removeTimeslot(new Timeslot(1, 2)); - expect(myNonClass.timeslots).toStrictEqual([ + myCustomActivity.timeslots = myTimeslots; + myCustomActivity.removeTimeslot(new Timeslot(1, 2)); + expect(myCustomActivity.timeslots).toStrictEqual([ new Timeslot(4, 3), new Timeslot(42, 4), ]); }); - test("remove non-matching timeslot from nonempty NonClass", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); + test("remove non-matching timeslot from nonempty CustomActivity", () => { + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); const myTimeslots: Timeslot[] = [ new Timeslot(1, 2), new Timeslot(4, 3), new Timeslot(42, 4), ]; - myNonClass.timeslots = myTimeslots; - myNonClass.removeTimeslot(new Timeslot(1, 1)); - expect(myNonClass.timeslots).toStrictEqual(myTimeslots); + myCustomActivity.timeslots = myTimeslots; + myCustomActivity.removeTimeslot(new Timeslot(1, 1)); + expect(myCustomActivity.timeslots).toStrictEqual(myTimeslots); }); }); - describe("NonClass.deflate", () => { + describe("CustomActivity.deflate", () => { /** Partition: * - this.timeslots: empty, nonempty * - this.room: defined, undefined */ test("timeslots empty, room undefined", () => { - expect(new NonClass(COLOR_SCHEME_LIGHT).deflate()).toStrictEqual([ + expect(new CustomActivity(COLOR_SCHEME_LIGHT).deflate()).toStrictEqual([ [], "New Activity", "#4A5568", @@ -320,9 +320,9 @@ describe("NonClass", () => { }); test("timeslots empty, room defined", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); - myNonClass.room = "lorem ipsum"; - expect(myNonClass.deflate()).toStrictEqual([ + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + myCustomActivity.room = "lorem ipsum"; + expect(myCustomActivity.deflate()).toStrictEqual([ [], "New Activity", "#4A5568", @@ -331,9 +331,9 @@ describe("NonClass", () => { }); test("timeslots nonempty, room undefined", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); - myNonClass.timeslots = [new Timeslot(10, 2)]; - expect(myNonClass.deflate()).toStrictEqual([ + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + myCustomActivity.timeslots = [new Timeslot(10, 2)]; + expect(myCustomActivity.deflate()).toStrictEqual([ [[10, 2]], "New Activity", "#4A5568", @@ -342,23 +342,23 @@ describe("NonClass", () => { }); }); - describe("NonClass.inflate", () => { + describe("CustomActivity.inflate", () => { /** * Partition on first item: empty, nonempty */ test("first item empty", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); - myNonClass.inflate([[], "alpha", "#123456", "beta"]); + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + myCustomActivity.inflate([[], "alpha", "#123456", "beta"]); - expect(myNonClass.timeslots).toStrictEqual([]); - expect(myNonClass.name).toBe("alpha"); - expect(myNonClass.backgroundColor).toBe("#123456"); - expect(myNonClass.room).toBe("beta"); + expect(myCustomActivity.timeslots).toStrictEqual([]); + expect(myCustomActivity.name).toBe("alpha"); + expect(myCustomActivity.backgroundColor).toBe("#123456"); + expect(myCustomActivity.room).toBe("beta"); }); test("first item nonempty", () => { - const myNonClass: NonClass = new NonClass(COLOR_SCHEME_LIGHT); - myNonClass.inflate([ + const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + myCustomActivity.inflate([ [ [1, 2], [4, 5], @@ -368,13 +368,13 @@ describe("NonClass", () => { "delta", ]); - expect(myNonClass.timeslots).toStrictEqual([ + expect(myCustomActivity.timeslots).toStrictEqual([ new Timeslot(1, 2), new Timeslot(4, 5), ]); - expect(myNonClass.name).toBe("gamma"); - expect(myNonClass.backgroundColor).toBe("#7890AB"); - expect(myNonClass.room).toBe("delta"); + expect(myCustomActivity.name).toBe("gamma"); + expect(myCustomActivity.backgroundColor).toBe("#7890AB"); + expect(myCustomActivity.room).toBe("delta"); }); }); }); From 0b85620e641167a42a5ea090bd0a35e6aa945975 Mon Sep 17 00:00:00 2001 From: Pratyush Venkatakrishnan Date: Thu, 22 Jan 2026 22:18:33 -0500 Subject: [PATCH 19/89] Default PE prereqs to "None" --- scrapers/pe.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scrapers/pe.py b/scrapers/pe.py index fee583bc..54a2e131 100644 --- a/scrapers/pe.py +++ b/scrapers/pe.py @@ -304,7 +304,9 @@ def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[int, dict[str, PEWSchema]] assert current_results["capacity"] == pe_row["capacity"] assert current_results["points"] == pe_row["gir_points"] assert current_results["swimGIR"] == pe_row["swim_gir"] - assert current_results["prereqs"] == pe_row["prerequisites"] + assert current_results["prereqs"] == pe_row["prerequisites"] or ( + current_results["prereqs"] == "None" and pe_row["prerequisites"] == "" + ) assert current_results["equipment"] == pe_row["equipment"] assert current_results["fee"] == pe_row["fee_amount"] assert current_results["description"] == pe_row["description"] @@ -340,7 +342,7 @@ def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[int, dict[str, PEWSchema]] "endDate": parse_date(pe_row["end_date"]).isoformat(), "points": pe_row["gir_points"], "swimGIR": pe_row["swim_gir"], - "prereqs": pe_row["prerequisites"], + "prereqs": pe_row["prerequisites"] or "None", "equipment": pe_row["equipment"], "fee": pe_row["fee_amount"], "description": pe_row["description"], From 259383b43f8fa7f4efc2da9265b8f45a060ef5cd Mon Sep 17 00:00:00 2001 From: Pratyush Venkatakrishnan Date: Thu, 22 Jan 2026 22:33:34 -0500 Subject: [PATCH 20/89] Rename capacity to classSize Since we don't know the real-time capacity, only the total class size --- scrapers/pe.py | 6 +++--- src/lib/rawPEClass.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scrapers/pe.py b/scrapers/pe.py index 54a2e131..f3d6d0ed 100644 --- a/scrapers/pe.py +++ b/scrapers/pe.py @@ -56,7 +56,7 @@ class PEWSchema(TypedDict): name: str sections: list[tuple[list[tuple[int, int]], str]] rawSections: list[str] - capacity: int + classSize: int startDate: str endDate: str points: int @@ -301,7 +301,7 @@ def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[int, dict[str, PEWSchema]] if current_results: # ensure all data in current_results (except for section info) are the same assert current_results["name"] == pe_row["title"] - assert current_results["capacity"] == pe_row["capacity"] + assert current_results["classSize"] == pe_row["capacity"] assert current_results["points"] == pe_row["gir_points"] assert current_results["swimGIR"] == pe_row["swim_gir"] assert current_results["prereqs"] == pe_row["prerequisites"] or ( @@ -337,7 +337,7 @@ def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[int, dict[str, PEWSchema]] "name": pe_row["title"], "sections": [section], "rawSections": [raw_section], - "capacity": pe_row["capacity"], + "classSize": pe_row["capacity"], "startDate": parse_date(pe_row["start_date"]).isoformat(), "endDate": parse_date(pe_row["end_date"]).isoformat(), "points": pe_row["gir_points"], diff --git a/src/lib/rawPEClass.ts b/src/lib/rawPEClass.ts index 6886d3ea..ab3f404e 100644 --- a/src/lib/rawPEClass.ts +++ b/src/lib/rawPEClass.ts @@ -10,8 +10,8 @@ export interface RawPEClass { sections: RawSection[]; /** Raw (FireRoad format) section locations/times */ rawSections: string[]; - /** Capacity per section */ - capacity: number; + /** Class size (for each section) */ + classSize: number; /** Start date, in ISO 8601 format */ startDate: string; From ef10b28327f2ba1ce57158205988ee57033e5fb5 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:42:33 -0500 Subject: [PATCH 21/89] make pe class --- src/components/ActivityDescription.tsx | 10 ++++ src/components/ClassTypes.tsx | 4 +- src/lib/activity.ts | 16 +++++- src/lib/class.ts | 12 +++- src/lib/gapi.ts | 23 +++++--- src/lib/pe.ts | 40 +++++++++++++ src/lib/state.ts | 30 ++++++++-- tests/activity.test.ts | 80 +++++++++++++++++++------- 8 files changed, 177 insertions(+), 38 deletions(-) create mode 100644 src/lib/pe.ts diff --git a/src/components/ActivityDescription.tsx b/src/components/ActivityDescription.tsx index b94f73cf..c29450e0 100644 --- a/src/components/ActivityDescription.tsx +++ b/src/components/ActivityDescription.tsx @@ -21,6 +21,7 @@ import { HydrantContext } from "../lib/hydrant"; import { ClassButtons, CustomActivityButtons } from "./ActivityButtons"; import { LuExternalLink } from "react-icons/lu"; +import { PEandWellness } from "~/lib/pe"; /** A small image indicating a flag, like Spring or CI-H. */ function TypeSpan(props: { flag?: keyof Flags; title: string }) { @@ -272,6 +273,13 @@ function CustomActivityDescription(props: { activity: CustomActivity }) { ); } +/** Full PE&W class activity */ +function PEandWellnessDescription(props: { activity: PEandWellness }) { + const { activity } = props; + + return <>{activity.rawClass.name}; +} + /** Activity description, whether class or non-class. */ export function ActivityDescription() { const { hydrantState } = useContext(HydrantContext); @@ -282,6 +290,8 @@ export function ActivityDescription() { return activity instanceof Class ? ( + ) : activity instanceof PEandWellness ? ( + ) : ( ); diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx index ddde2b43..6473b570 100644 --- a/src/components/ClassTypes.tsx +++ b/src/components/ClassTypes.tsx @@ -4,9 +4,7 @@ import { LuGraduationCap, LuVolleyball } from "react-icons/lu"; import type { IconType } from "react-icons/lib"; export const Academic = () => { - return ( - - ); + return ; }; export const PEandW = () => { diff --git a/src/lib/activity.ts b/src/lib/activity.ts index e61e2538..a8e5c80e 100644 --- a/src/lib/activity.ts +++ b/src/lib/activity.ts @@ -7,6 +7,7 @@ import { fallbackColor, textColor } from "./colors"; import { Slot } from "./dates"; import type { RawTimeslot } from "./rawClass"; import { sum } from "./utils"; +import type { PEandWellness } from "./pe"; /** A period of time, spanning several Slots. */ export class Timeslot { @@ -65,6 +66,17 @@ export class Timeslot { } } +export interface ActivityClass { + id: string; + backgroundColor: string; + manualColor: boolean; + hours: number; + buttonName: string; + events: Event[]; + start?: [number, number]; + end?: [number, number]; +} + /** * A group of events to be rendered in a calendar, all of the same name, room, * and color. @@ -112,7 +124,7 @@ export class Event { } /** A non-class activity. */ -export class CustomActivity { +export class CustomActivity implements ActivityClass { /** ID unique over all Activities. */ readonly id: string; name = "New Activity"; @@ -192,4 +204,4 @@ export class CustomActivity { } /** Shared interface for Class and non-classes. */ -export type Activity = Class | CustomActivity; +export type Activity = Class | CustomActivity | PEandWellness; diff --git a/src/lib/class.ts b/src/lib/class.ts index 5067864d..38cfd84e 100644 --- a/src/lib/class.ts +++ b/src/lib/class.ts @@ -1,4 +1,4 @@ -import { Timeslot, Event } from "./activity"; +import { Timeslot, Event, type ActivityClass } from "./activity"; import type { ColorScheme } from "./colors"; import { fallbackColor } from "./colors"; import { @@ -277,7 +277,7 @@ export class Sections { } /** An entire class, e.g. 6.036, and its selected sections. */ -export class Class { +export class Class implements ActivityClass { /** * The RawClass being wrapped around. Nothing outside Class should touch * this; instead use the Class getters like cls.id, cls.number, etc. @@ -411,6 +411,14 @@ export class Class { .filter((event): event is Event => event instanceof Event); } + get start(): [number, number] | undefined { + return this.rawClass.quarterInfo?.start; + } + + get end(): [number, number] | undefined { + return this.rawClass.quarterInfo?.end; + } + /** Object of boolean properties of class, used for filtering. */ get flags(): Flags { return { diff --git a/src/lib/gapi.ts b/src/lib/gapi.ts index d56effa6..eaacc00b 100644 --- a/src/lib/gapi.ts +++ b/src/lib/gapi.ts @@ -6,6 +6,7 @@ import { tzlib_get_ical_block } from "timezones-ical-library"; import type { Activity } from "./activity"; import type { Term } from "./dates"; import type { State } from "./state"; +import { Class } from "./class"; /** Timezone string. */ const TIMEZONE = "America/New_York"; @@ -30,13 +31,21 @@ function download(filename: string, text: string) { function toICalEvents(activity: Activity, term: Term): ICalEventData[] { return activity.events.flatMap((event) => event.slots.map((slot) => { - const rawClass = - "rawClass" in event.activity ? event.activity.rawClass : undefined; - - const start = rawClass?.quarterInfo?.start; - const end = rawClass?.quarterInfo?.end; - const h1 = rawClass?.half === 1; - const h2 = rawClass?.half === 2; + const start: [number, number] | undefined = + "start" in event.activity ? event.activity.start : undefined; + const end: [number, number] | undefined = + "end" in event.activity ? event.activity.end : undefined; + + let h1 = false; + let h2 = false; + + if (event.activity instanceof Class) { + if (event.half === 1) { + h1 = true; + } else if (event.half === 2) { + h2 = true; + } + } const startDate = term.startDateFor(slot.startSlot, h2, start); const startDateEnd = term.startDateFor(slot.endSlot, h2, start); diff --git a/src/lib/pe.ts b/src/lib/pe.ts new file mode 100644 index 00000000..b20f4ee2 --- /dev/null +++ b/src/lib/pe.ts @@ -0,0 +1,40 @@ +import type { ActivityClass } from "./activity"; +import type { RawPEClass } from "./rawPEClass"; +import type { Event } from "./activity"; + +/** + * PE&W activity placeholder + */ +export class PEandWellness implements ActivityClass { + id: string; + backgroundColor: string; + manualColor = false; + readonly rawClass: RawPEClass; + + constructor(id: string, backgroundColor: string, rawClass: RawPEClass) { + this.id = id; + this.backgroundColor = backgroundColor; + this.rawClass = rawClass; + } + + /** Hours per week. */ + readonly hours = 2; + + get buttonName(): string { + return this.rawClass.name; + } + + get events(): Event[] { + return []; + } + + get start(): [number, number] { + const startDate = new Date(this.rawClass.startDate); + return [startDate.getMonth() + 1, startDate.getDate()]; + } + + get end(): [number, number] { + const endDate = new Date(this.rawClass.endDate); + return [endDate.getMonth() + 1, endDate.getDate()]; + } +} diff --git a/src/lib/state.ts b/src/lib/state.ts index 3ef88024..a7961a8f 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -13,6 +13,7 @@ import { Store } from "./store"; import { sum, urldecode, urlencode } from "./utils"; import type { HydrantState, Preferences, Save } from "./schema"; import { BANNER_LAST_CHANGED, DEFAULT_PREFERENCES, ClassType } from "./schema"; +import { PEandWellness } from "./pe"; /** * Global State object. Maintains global program state (selected classes, @@ -41,6 +42,8 @@ export class State { private viewedActivity: Activity | undefined; /** Selected class activities. */ private selectedClasses: Class[] = []; + /** Selected PE and Wellness activities. */ + private selectedPEandWellness: PEandWellness[] = []; /** Selected custom activities. */ private selectedCustomActivities: CustomActivity[] = []; /** Selected schedule option; zero-indexed. */ @@ -80,7 +83,11 @@ export class State { /** All activities. */ get selectedActivities(): Activity[] { - return [...this.selectedClasses, ...this.selectedCustomActivities]; + return [ + ...this.selectedClasses, + ...this.selectedPEandWellness, + ...this.selectedCustomActivities, + ]; } /** The color scheme. */ @@ -121,6 +128,8 @@ export class State { if (this.isSelectedActivity(toAdd)) return; if (toAdd instanceof Class) { this.selectedClasses.push(toAdd); + } else if (toAdd instanceof PEandWellness) { + this.selectedPEandWellness.push(toAdd); } else { this.selectedCustomActivities.push(toAdd); } @@ -134,6 +143,11 @@ export class State { this.selectedClasses = this.selectedClasses.filter( (activity_) => activity_.id !== activity.id, ); + } else if (activity instanceof PEandWellness) { + this.selectedPEandWellness = this.selectedPEandWellness.filter( + (activity_) => activity_.id !== activity.id, + ); + this.setViewedActivity(undefined); } else { this.selectedCustomActivities = this.selectedCustomActivities.filter( (activity_) => activity_.id !== activity.id, @@ -182,7 +196,10 @@ export class State { } /** Changes the room for a given non-class. */ - relocateCustomActivity(customActivity: CustomActivity, room: string | undefined): void { + relocateCustomActivity( + customActivity: CustomActivity, + room: string | undefined, + ): void { const customActivity_ = this.selectedCustomActivities.find( (customActivity_) => customActivity_.id === customActivity.id, ); @@ -251,7 +268,10 @@ export class State { */ updateActivities(save = true): void { chooseColors(this.selectedActivities, this.colorScheme); - const result = scheduleSlots(this.selectedClasses, this.selectedCustomActivities); + const result = scheduleSlots( + this.selectedClasses, + this.selectedCustomActivities, + ); this.options = result.options; this.conflicts = result.conflicts; this.selectOption(); @@ -345,7 +365,9 @@ export class State { return [ this.selectedClasses.map((cls) => cls.deflate()), this.selectedCustomActivities.length > 0 - ? this.selectedCustomActivities.map((customActivity) => customActivity.deflate()) + ? this.selectedCustomActivities.map((customActivity) => + customActivity.deflate(), + ) : null, this.selectedOption, ]; diff --git a/tests/activity.test.ts b/tests/activity.test.ts index ec5beb22..a38a71a9 100644 --- a/tests/activity.test.ts +++ b/tests/activity.test.ts @@ -121,7 +121,9 @@ describe("Timeslot", () => { }); test("Event.eventInputs", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT_CONTRAST); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT_CONTRAST, + ); const myHexCode = "#611917"; // randomly generated hex code const myTitle = "y8g0i81"; // random keysmashes const myRoom = "ahouttoanhontjanota"; @@ -162,25 +164,33 @@ describe("CustomActivity", () => { const nanoidRegex = /^[A-Za-z0-9-_]{8}$/; test("COLOR_SCHEME_LIGHT", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); expect(nanoidRegex.test(myCustomActivity.id)).toBeTruthy(); expect(myCustomActivity.backgroundColor).toBe("#4A5568"); }); test("COLOR_SCHEME_DARK", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_DARK); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_DARK, + ); expect(nanoidRegex.test(myCustomActivity.id)).toBeTruthy(); expect(myCustomActivity.backgroundColor).toBe("#CBD5E0"); }); test("COLOR_SCHEME_LIGHT_CONTRAST", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT_CONTRAST); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT_CONTRAST, + ); expect(nanoidRegex.test(myCustomActivity.id)).toBeTruthy(); expect(myCustomActivity.backgroundColor).toBe("#4A5568"); }); test("COLOR_SCHEME_DARK_CONTRAST", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_DARK_CONTRAST); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_DARK_CONTRAST, + ); expect(nanoidRegex.test(myCustomActivity.id)).toBeTruthy(); expect(myCustomActivity.backgroundColor).toBe("#CBD5E0"); }); @@ -189,11 +199,15 @@ describe("CustomActivity", () => { describe("CustomActivity.buttonName", () => { /** Partition on this.name: changed, not changed */ test("CustomActivity.name not changed", () => { - expect(new CustomActivity(COLOR_SCHEME_LIGHT).buttonName).toBe("New Activity"); + expect(new CustomActivity(COLOR_SCHEME_LIGHT).buttonName).toBe( + "New Activity", + ); }); test("CustomActivity.name changed", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); const myString = "lorem ipsum dolor sit amet"; myCustomActivity.name = myString; expect(myCustomActivity.buttonName).toBe(myString); @@ -207,13 +221,17 @@ describe("CustomActivity", () => { }); test("1 timeslot", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); myCustomActivity.timeslots = [new Timeslot(4, 5)]; expect(myCustomActivity.hours).toBe(5 / 2); }); test("multiple timeslots", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); myCustomActivity.timeslots = [new Timeslot(2, 7), new Timeslot(11, 5)]; expect(myCustomActivity.hours).toBe(6); }); @@ -229,7 +247,9 @@ describe("CustomActivity", () => { ]; const myRoom = "ahuotiyuwiq"; // constructing and testing `myCustomActivity` - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_DARK); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_DARK, + ); myCustomActivity.name = myName; myCustomActivity.timeslots = myTimeslots; myCustomActivity.room = myRoom; @@ -245,14 +265,18 @@ describe("CustomActivity", () => { * - slot is valid, adds */ test("adds valid slot", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); const myTimeslot: Timeslot = new Timeslot(1, 1); myCustomActivity.addTimeslot(myTimeslot); expect(myCustomActivity.timeslots).toStrictEqual([myTimeslot]); }); test("doesn't add existing slot", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); const myTimeslot: Timeslot = new Timeslot(4, 6); myCustomActivity.timeslots = [myTimeslot]; myCustomActivity.addTimeslot(myTimeslot); @@ -260,7 +284,9 @@ describe("CustomActivity", () => { }); test("doesn't add multi-day slot", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); myCustomActivity.addTimeslot(new Timeslot(42, 69)); expect(myCustomActivity.timeslots).toStrictEqual([]); }); @@ -272,13 +298,17 @@ describe("CustomActivity", () => { * - CustomActivity.timeslots (before call): empty, nonempty with match, nonempty without match */ test("removing timeslot from empty CustomActivity", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); myCustomActivity.removeTimeslot(new Timeslot(1, 1)); expect(myCustomActivity.timeslots).toStrictEqual([]); }); test("remove matching timeslot from nonempty CustomActivity", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); const myTimeslots: Timeslot[] = [ new Timeslot(1, 2), new Timeslot(4, 3), @@ -293,7 +323,9 @@ describe("CustomActivity", () => { }); test("remove non-matching timeslot from nonempty CustomActivity", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); const myTimeslots: Timeslot[] = [ new Timeslot(1, 2), new Timeslot(4, 3), @@ -320,7 +352,9 @@ describe("CustomActivity", () => { }); test("timeslots empty, room defined", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); myCustomActivity.room = "lorem ipsum"; expect(myCustomActivity.deflate()).toStrictEqual([ [], @@ -331,7 +365,9 @@ describe("CustomActivity", () => { }); test("timeslots nonempty, room undefined", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); myCustomActivity.timeslots = [new Timeslot(10, 2)]; expect(myCustomActivity.deflate()).toStrictEqual([ [[10, 2]], @@ -347,7 +383,9 @@ describe("CustomActivity", () => { * Partition on first item: empty, nonempty */ test("first item empty", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); myCustomActivity.inflate([[], "alpha", "#123456", "beta"]); expect(myCustomActivity.timeslots).toStrictEqual([]); @@ -357,7 +395,9 @@ describe("CustomActivity", () => { }); test("first item nonempty", () => { - const myCustomActivity: CustomActivity = new CustomActivity(COLOR_SCHEME_LIGHT); + const myCustomActivity: CustomActivity = new CustomActivity( + COLOR_SCHEME_LIGHT, + ); myCustomActivity.inflate([ [ [1, 2], From ca39ef51acb5b7c07791f606a2ed5efe54f62db2 Mon Sep 17 00:00:00 2001 From: Pratyush Venkatakrishnan Date: Thu, 22 Jan 2026 22:42:58 -0500 Subject: [PATCH 22/89] Begin working on PE class table Currently still displays academic classes --- src/components/ClassTypes.tsx | 3 +- src/components/PEClassTable.tsx | 563 ++++++++++++++++++++++++++++++++ src/routes/_index.tsx | 1 - 3 files changed, 565 insertions(+), 2 deletions(-) create mode 100644 src/components/PEClassTable.tsx diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx index 6473b570..9f4c2984 100644 --- a/src/components/ClassTypes.tsx +++ b/src/components/ClassTypes.tsx @@ -1,4 +1,5 @@ import { ClassTable } from "./ClassTable"; +import { PEClassTable } from "./PEClassTable"; import { ClassType } from "~/lib/schema"; import { LuGraduationCap, LuVolleyball } from "react-icons/lu"; import type { IconType } from "react-icons/lib"; @@ -8,7 +9,7 @@ export const Academic = () => { }; export const PEandW = () => { - return <>; + return ; }; // eslint-disable-next-line react-refresh/only-export-components diff --git a/src/components/PEClassTable.tsx b/src/components/PEClassTable.tsx new file mode 100644 index 00000000..a23150f1 --- /dev/null +++ b/src/components/PEClassTable.tsx @@ -0,0 +1,563 @@ +// TODO factor out common pieces between ClassTable and PEClassTable +import { + useContext, + useEffect, + useMemo, + useRef, + useState, + type Dispatch, + type ReactNode, + type SetStateAction, +} from "react"; + +import { AgGridReact } from "ag-grid-react"; +import { + ModuleRegistry, + ClientSideRowModelModule, + ValidationModule, + ExternalFilterModule, + RenderApiModule, + CellStyleModule, + themeQuartz, + type IRowNode, + type ColDef, + type Module, +} from "ag-grid-community"; + +import { + Box, + Flex, + Image, + Input, + Button, + ButtonGroup, + InputGroup, + CloseButton, +} from "@chakra-ui/react"; +import { LuPlus, LuMinus, LuSearch, LuStar } from "react-icons/lu"; +import { LabelledButton } from "./ui/button"; +import { useColorModeValue } from "./ui/color-mode"; + +import type { Class, Flags } from "../lib/class"; +import { DARK_IMAGES } from "../lib/class"; +import { classNumberMatch, classSort, simplifyString } from "../lib/utils"; +import type { TSemester } from "../lib/dates"; +import "./ClassTable.scss"; +import { HydrantContext } from "../lib/hydrant"; +import type { State } from "../lib/state"; + +const hydrantTheme = themeQuartz.withParams({ + accentColor: "var(--chakra-colors-fg)", + backgroundColor: "var(--chakra-colors-bg)", + borderColor: "var(--chakra-colors-border)", + browserColorScheme: "inherit", + fontFamily: "inherit", + foregroundColor: "var(--chakra-colors-fg)", + headerBackgroundColor: "var(--chakra-colors-bg-subtle)", + rowHoverColor: "var(--chakra-colors-color-palette-subtle)", + wrapperBorderRadius: "var(--chakra-radii-md)", +}); + +const GRID_MODULES: Module[] = [ + ClientSideRowModelModule, + ExternalFilterModule, + CellStyleModule, + RenderApiModule, + ...(import.meta.env.DEV ? [ValidationModule] : []), +]; + +ModuleRegistry.registerModules(GRID_MODULES); + +enum ColorEnum { + Muted = "ag-cell-muted-text", + Success = "ag-cell-success-text", + Warning = "ag-cell-warning-text", + Error = "ag-cell-error-text", + Normal = "ag-cell-normal-text", +} + +/** A single row in the class table. */ +interface ClassTableRow { + number: string; + size: string; + fee: string; + name: string; + class: Class; + inCharge: string; +} + +type ClassFilter = (cls?: Class) => boolean; +/** Type of filter on class list; null if no filter. */ +type SetClassFilter = Dispatch>; + +/** + * Textbox for typing in the name or number of the class to search. Maintains + * the {@link ClassFilter} that searches for a class name/number. + */ +function ClassInput(props: { + /** All rows in the class table. */ + rowData: ClassTableRow[]; + /** Callback for updating the class filter. */ + setInputFilter: SetClassFilter; +}) { + const { rowData, setInputFilter } = props; + const { state } = useContext(HydrantContext); + + // State for textbox input. + const [classInput, setClassInput] = useState(""); + const inputRef = useRef(null); + + // Search results for classes. + const searchResults = useRef< + { + numbers: string[]; + name: string; + class: Class; + }[] + >(undefined); + + const processedRows = useMemo( + () => + rowData.map((data) => { + const numbers = [data.number]; + const [, otherNumber, realName] = + /^\[(.*)\] (.*)$/.exec(data.name) ?? []; + if (otherNumber) numbers.push(otherNumber); + return { + numbers, + name: simplifyString(realName || data.name), + class: data.class, + inCharge: simplifyString(data.inCharge), + }; + }), + [rowData], + ); + + const onClassInputChange = (input: string) => { + if (input) { + const simplifyInput = simplifyString(input); + searchResults.current = processedRows.filter( + (row) => + row.numbers.some((number) => classNumberMatch(input, number)) || + row.name.includes(simplifyInput) || + row.inCharge.includes(simplifyInput), + ); + const index = new Set(searchResults.current.map((cls) => cls.numbers[0])); + setInputFilter(() => (cls?: Class) => index.has(cls?.number ?? "")); + } else { + setInputFilter(null); + } + setClassInput(input); + }; + + const onEnter = () => { + const { numbers, class: cls } = searchResults.current?.[0] ?? {}; + if ( + searchResults.current?.length === 1 || + numbers?.some((number) => classNumberMatch(number, classInput, true)) + ) { + // first check if the first result matches + state.toggleActivity(cls); + onClassInputChange(""); + } else if (state.classes.has(classInput)) { + // else check if this number exists exactly + const cls = state.classes.get(classInput); + state.toggleActivity(cls); + } + }; + + const clearButton = classInput ? ( + { + onClassInputChange(""); + inputRef.current?.focus(); + }} + me="-2" + /> + ) : undefined; + + return ( + +
{ + e.preventDefault(); + onEnter(); + }} + style={{ width: "100%", maxWidth: "30em" }} + > + } + endElement={clearButton} + width="fill-available" + > + { + onClassInputChange(e.target.value); + }} + /> + +
+
+ ); +} + +const filtersNonFlags = { + fits: (state, cls) => state.fitsSchedule(cls), + starred: (state, cls) => state.isClassStarred(cls), + new: (_, cls) => cls.new, +} satisfies Record boolean>; + +type Filter = keyof Flags | keyof typeof filtersNonFlags; +type FilterGroup = [Filter, string, ReactNode?][]; + +/** List of top filter IDs and their displayed names. */ +const CLASS_FLAGS_1: FilterGroup = [ + ["starred", "Starred", ], + ["nofee", "No fee"], + ["fits", "Fits schedule"], + ["new", "✨ New!"], +]; + +/** List of hidden filter IDs, their displayed names, and image path, if any. */ +const CLASS_FLAGS_2: FilterGroup = [ + ["nopreq", "No prereq"], + ["wizard", "🔮 Wellness Wizard"], + ["pirate", "🏴‍☠️ Pirate Certificate"], +]; + +const CLASS_FLAGS = [ + ...CLASS_FLAGS_1, + ...CLASS_FLAGS_2, +]; + +/** Div containing all the flags like "HASS". Maintains the flag filter. */ +function ClassFlags(props: { + /** Callback for updating the class filter. */ + setFlagsFilter: SetClassFilter; + /** Callback for updating the grid filter manually. */ + updateFilter: () => void; +}) { + const { setFlagsFilter, updateFilter } = props; + const { state } = useContext(HydrantContext); + + // Map from flag to whether it's on. + const [flags, setFlags] = useState>(() => { + const result = new Map(); + for (const flag of CLASS_FLAGS) { + result.set(flag, false); + } + return result; + }); + + // Show hidden flags? + const [allFlags, setAllFlags] = useState(false); + + // this callback needs to get called when the set of classes change, because + // the filter has to change as well + useEffect(() => { + state.fitsScheduleCallback = () => { + if (flags.get("fits")) { + updateFilter(); + } + }; + }, [state, flags, updateFilter]); + + const onChange = (flag: Filter, value: boolean) => { + const newFlags = new Map(flags); + newFlags.set(flag, value); + setFlags(newFlags); + + // careful! we have to wrap it with a () => because otherwise React will + // think it's an updater function instead of the actual function. + setFlagsFilter(() => (cls?: Class) => { + if (!cls) return false; + let result = true; + newFlags.forEach((value, flag) => { + if ( + value && + flag in filtersNonFlags && + !filtersNonFlags[flag as keyof typeof filtersNonFlags](state, cls) + ) { + result = false; + } else if ( + value && + !(flag in filtersNonFlags) && + !cls.flags[flag as keyof typeof cls.flags] + ) { + result = false; + } + }); + return result; + }); + }; + + const filter = useColorModeValue( + (_flags: keyof Flags) => "", + (flag: keyof Flags) => (DARK_IMAGES.includes(flag) ? "invert()" : ""), + ); + + const renderGroup = (group: FilterGroup) => { + return ( + + {group.map(([flag, label, image]) => { + const checked = flags.get(flag); + + // hide starred button if no classes starred + if ( + flag === "starred" && + state.getStarredClasses().length === 0 && + !checked + ) { + return null; + } + + return image ? ( + typeof image === "string" ? ( + // if image is a string, it's a path to an image + { + onChange(flag, !checked); + }} + title={label} + variant={checked ? "solid" : "outline"} + > + {label} + + ) : ( + // image is a react element, like an icon + + ) + ) : ( + + ); + })} + + ); + }; + + return ( + + + {renderGroup(CLASS_FLAGS_1)} + + + {allFlags && ( + <> + {renderGroup(CLASS_FLAGS_2)} + + )} + + ); +} + +const StarButton = ({ + cls, + onStarToggle, +}: { + cls: Class; + onStarToggle?: () => void; +}) => { + const { state } = useContext(HydrantContext); + const isStarred = state.isClassStarred(cls); + + return ( + + ); +}; + +/** The table of all classes, along with searching and filtering with flags. */ +export function PEClassTable() { + const { state } = useContext(HydrantContext); + const { classes } = state; + + const gridRef = useRef>(null); + + // Setup table columns + const columnDefs: ColDef[] = useMemo(() => { + const initialSort = "asc" as const; + const sortingOrder: ("asc" | "desc")[] = ["asc", "desc"]; + const sortProps = { sortable: true, unSortIcon: true, sortingOrder }; + const numberSortProps = { + // sort by number, N/A is infinity, tiebreak with class number + comparator: ( + valueA: string | undefined | null, + valueB: string | undefined | null, + nodeA: IRowNode, + nodeB: IRowNode, + ) => { + if (!nodeA.data || !nodeB.data) return 0; + const numberA = valueA === "N/A" || !valueA ? Infinity : Number(valueA); + const numberB = valueB === "N/A" || !valueB ? Infinity : Number(valueB); + return numberA !== numberB + ? numberA - numberB + : classSort(nodeA.data.number, nodeB.data.number); + }, + ...sortProps, + }; + return [ + { + headerName: "", + field: "number", + maxWidth: 49, + cellRenderer: (params: { value: string; data: ClassTableRow }) => ( + { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + gridRef.current?.api?.refreshCells({ + force: true, + columns: ["number"], + }); + }} + /> + ), + sortable: false, + cellStyle: { padding: 0 }, + }, + { + field: "number", + headerName: "Class", + comparator: classSort, + initialSort, + maxWidth: 93, + ...sortProps, + }, + { + field: "size", + maxWidth: 99, + ...numberSortProps, + }, + { + field: "fee", + maxWidth: 97, + ...numberSortProps, + }, + { + field: "name", + sortable: false, + flex: 1, + valueFormatter: (params) => + `${params.data?.class.new ? "✨ " : ""}${params.value ?? ""}`, + }, + ]; + }, [state]); + + const defaultColDef: ColDef = useMemo(() => { + return { + resizable: false, + }; + }, []); + + // Setup rows + const rowData: ClassTableRow[] = useMemo( + () => + Array.from(classes.values(), (cls) => ({ + number: cls.number, + size: cls.evals.rating.slice(0, 3), // remove the "/7.0" if exists + fee: cls.evals.hours, + name: cls.name, + class: cls, + inCharge: cls.description.inCharge, + })), + [classes], + ); + + const [inputFilter, setInputFilter] = useState(null); + const [flagsFilter, setFlagsFilter] = useState(null); + + const doesExternalFilterPass = useMemo(() => { + return (node: IRowNode) => { + if (inputFilter && !inputFilter(node.data?.class)) return false; + if (flagsFilter && !flagsFilter(node.data?.class)) return false; + return true; + }; + }, [inputFilter, flagsFilter]); + + // Need to notify grid every time we update the filter + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + gridRef.current?.api?.onFilterChanged(); + }, [doesExternalFilterPass]); + + return ( + + + gridRef.current?.api?.onFilterChanged()} + /> + + + theme={hydrantTheme} + ref={gridRef} + defaultColDef={defaultColDef} + columnDefs={columnDefs} + rowData={rowData} + suppressMovableColumns={true} + enableCellTextSelection={true} + isExternalFilterPresent={() => true} + doesExternalFilterPass={doesExternalFilterPass} + onRowClicked={(e) => { + state.setViewedActivity(e.data?.class); + }} + onRowDoubleClicked={(e) => { + state.toggleActivity(e.data?.class); + }} + // these have to be set here, not in css: + headerHeight={40} + rowHeight={40} + /> + + + ); +} diff --git a/src/routes/_index.tsx b/src/routes/_index.tsx index 22cb372c..c93f8682 100644 --- a/src/routes/_index.tsx +++ b/src/routes/_index.tsx @@ -102,7 +102,6 @@ function HydrantApp() { Date: Thu, 22 Jan 2026 22:49:43 -0500 Subject: [PATCH 23/89] Rename PEandWellness to PEClass To mirror Class --- src/components/ActivityDescription.tsx | 8 ++++---- src/lib/activity.ts | 4 ++-- src/lib/pe.ts | 2 +- src/lib/state.ts | 16 ++++++++-------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/ActivityDescription.tsx b/src/components/ActivityDescription.tsx index c29450e0..6a4447f1 100644 --- a/src/components/ActivityDescription.tsx +++ b/src/components/ActivityDescription.tsx @@ -21,7 +21,7 @@ import { HydrantContext } from "../lib/hydrant"; import { ClassButtons, CustomActivityButtons } from "./ActivityButtons"; import { LuExternalLink } from "react-icons/lu"; -import { PEandWellness } from "~/lib/pe"; +import { PEClass } from "../lib/pe"; /** A small image indicating a flag, like Spring or CI-H. */ function TypeSpan(props: { flag?: keyof Flags; title: string }) { @@ -274,7 +274,7 @@ function CustomActivityDescription(props: { activity: CustomActivity }) { } /** Full PE&W class activity */ -function PEandWellnessDescription(props: { activity: PEandWellness }) { +function PEClassDescription(props: { activity: PEClass }) { const { activity } = props; return <>{activity.rawClass.name}; @@ -290,8 +290,8 @@ export function ActivityDescription() { return activity instanceof Class ? ( - ) : activity instanceof PEandWellness ? ( - + ) : activity instanceof PEClass ? ( + ) : ( ); diff --git a/src/lib/activity.ts b/src/lib/activity.ts index a8e5c80e..d8550c1d 100644 --- a/src/lib/activity.ts +++ b/src/lib/activity.ts @@ -7,7 +7,7 @@ import { fallbackColor, textColor } from "./colors"; import { Slot } from "./dates"; import type { RawTimeslot } from "./rawClass"; import { sum } from "./utils"; -import type { PEandWellness } from "./pe"; +import type { PEClass } from "./pe"; /** A period of time, spanning several Slots. */ export class Timeslot { @@ -204,4 +204,4 @@ export class CustomActivity implements ActivityClass { } /** Shared interface for Class and non-classes. */ -export type Activity = Class | CustomActivity | PEandWellness; +export type Activity = Class | CustomActivity | PEClass; diff --git a/src/lib/pe.ts b/src/lib/pe.ts index b20f4ee2..0c599bf8 100644 --- a/src/lib/pe.ts +++ b/src/lib/pe.ts @@ -5,7 +5,7 @@ import type { Event } from "./activity"; /** * PE&W activity placeholder */ -export class PEandWellness implements ActivityClass { +export class PEClass implements ActivityClass { id: string; backgroundColor: string; manualColor = false; diff --git a/src/lib/state.ts b/src/lib/state.ts index a7961a8f..24fdc1b7 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -13,7 +13,7 @@ import { Store } from "./store"; import { sum, urldecode, urlencode } from "./utils"; import type { HydrantState, Preferences, Save } from "./schema"; import { BANNER_LAST_CHANGED, DEFAULT_PREFERENCES, ClassType } from "./schema"; -import { PEandWellness } from "./pe"; +import { PEClass } from "./pe"; /** * Global State object. Maintains global program state (selected classes, @@ -42,8 +42,8 @@ export class State { private viewedActivity: Activity | undefined; /** Selected class activities. */ private selectedClasses: Class[] = []; - /** Selected PE and Wellness activities. */ - private selectedPEandWellness: PEandWellness[] = []; + /** Selected PE and Wellness classes. */ + private selectedPEClasses: PEClass[] = []; /** Selected custom activities. */ private selectedCustomActivities: CustomActivity[] = []; /** Selected schedule option; zero-indexed. */ @@ -85,7 +85,7 @@ export class State { get selectedActivities(): Activity[] { return [ ...this.selectedClasses, - ...this.selectedPEandWellness, + ...this.selectedPEClasses, ...this.selectedCustomActivities, ]; } @@ -128,8 +128,8 @@ export class State { if (this.isSelectedActivity(toAdd)) return; if (toAdd instanceof Class) { this.selectedClasses.push(toAdd); - } else if (toAdd instanceof PEandWellness) { - this.selectedPEandWellness.push(toAdd); + } else if (toAdd instanceof PEClass) { + this.selectedPEClasses.push(toAdd); } else { this.selectedCustomActivities.push(toAdd); } @@ -143,8 +143,8 @@ export class State { this.selectedClasses = this.selectedClasses.filter( (activity_) => activity_.id !== activity.id, ); - } else if (activity instanceof PEandWellness) { - this.selectedPEandWellness = this.selectedPEandWellness.filter( + } else if (activity instanceof PEClass) { + this.selectedPEClasses = this.selectedPEClasses.filter( (activity_) => activity_.id !== activity.id, ); this.setViewedActivity(undefined); From 9608be6393a0b6ec62b5fb6773de68cc91ea062c Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:50:02 -0500 Subject: [PATCH 24/89] fix constructor --- src/lib/pe.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lib/pe.ts b/src/lib/pe.ts index b20f4ee2..08d1d510 100644 --- a/src/lib/pe.ts +++ b/src/lib/pe.ts @@ -1,27 +1,30 @@ import type { ActivityClass } from "./activity"; import type { RawPEClass } from "./rawPEClass"; import type { Event } from "./activity"; +import { fallbackColor, type ColorScheme } from "./colors"; /** * PE&W activity placeholder */ export class PEandWellness implements ActivityClass { - id: string; backgroundColor: string; manualColor = false; readonly rawClass: RawPEClass; - constructor(id: string, backgroundColor: string, rawClass: RawPEClass) { - this.id = id; - this.backgroundColor = backgroundColor; + constructor(rawClass: RawPEClass, colorScheme: ColorScheme) { this.rawClass = rawClass; + this.backgroundColor = fallbackColor(colorScheme); + } + + get id(): string { + return this.rawClass.number; } /** Hours per week. */ readonly hours = 2; get buttonName(): string { - return this.rawClass.name; + return this.rawClass.number; } get events(): Event[] { From f10f0d76581e76e67d49f8951a0cd499f57e95f7 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:56:57 -0500 Subject: [PATCH 25/89] take off without unmount --- src/routes/_index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/_index.tsx b/src/routes/_index.tsx index c93f8682..19fa0b97 100644 --- a/src/routes/_index.tsx +++ b/src/routes/_index.tsx @@ -101,7 +101,6 @@ function HydrantApp() { Date: Thu, 22 Jan 2026 23:11:44 -0500 Subject: [PATCH 26/89] grab pe classes --- src/components/ClassTypes.tsx | 20 +++++++++----- src/lib/state.ts | 8 ++++++ src/routes/_index.tsx | 51 +++++++++++++++++++---------------- src/routes/export.ts | 8 +++--- 4 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx index 9f4c2984..2965276e 100644 --- a/src/components/ClassTypes.tsx +++ b/src/components/ClassTypes.tsx @@ -13,10 +13,16 @@ export const PEandW = () => { }; // eslint-disable-next-line react-refresh/only-export-components -export const CLASS_TYPE_COMPONENTS: Record< - ClassType, - [IconType, React.ComponentType] -> = { - [ClassType.ACADEMIC]: [LuGraduationCap, Academic], - [ClassType.PEW]: [LuVolleyball, PEandW], -}; +export function classTypeComponents(termKeys: string[]) { + const obj = {} as Record; + + if (termKeys.includes("academic")) { + obj[ClassType.ACADEMIC] = [LuGraduationCap, Academic]; + } + + if (termKeys.includes("pe")) { + obj[ClassType.PEW] = [LuVolleyball, PEandW]; + } + + return obj; +} diff --git a/src/lib/state.ts b/src/lib/state.ts index 24fdc1b7..e74b4681 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -14,6 +14,7 @@ import { sum, urldecode, urlencode } from "./utils"; import type { HydrantState, Preferences, Save } from "./schema"; import { BANNER_LAST_CHANGED, DEFAULT_PREFERENCES, ClassType } from "./schema"; import { PEClass } from "./pe"; +import type { RawPEClass } from "./rawPEClass"; /** * Global State object. Maintains global program state (selected classes, @@ -22,6 +23,8 @@ import { PEClass } from "./pe"; export class State { /** Map from class number to Class object. */ classes: Map; + /** Map from class number to PEClass object. */ + peClasses: Map; /** Possible section choices. */ options: Section[][] = [[]]; /** Current number of schedule conflicts. */ @@ -66,6 +69,7 @@ export class State { constructor( rawClasses: Map, + rawPEClasses: Map, /** The current term object. */ public readonly term: Term, /** String representing last update time. */ @@ -74,10 +78,14 @@ export class State { public readonly latestUrlName: string, ) { this.classes = new Map(); + this.peClasses = new Map(); this.store = new Store(term.toString()); rawClasses.forEach((cls, number) => { this.classes.set(number, new Class(cls, this.colorScheme)); }); + rawPEClasses.forEach((cls, number) => { + this.peClasses.set(number, new PEClass(cls, this.colorScheme)); + }); this.initState(); } diff --git a/src/routes/_index.tsx b/src/routes/_index.tsx index 19fa0b97..35e4fcca 100644 --- a/src/routes/_index.tsx +++ b/src/routes/_index.tsx @@ -12,7 +12,7 @@ import { PreregLink, ExportCalendar, } from "../components/ButtonsLinks"; -import { CLASS_TYPE_COMPONENTS } from "../components/ClassTypes"; +import { classTypeComponents } from "../components/ClassTypes"; import { State } from "../lib/state"; import { Term } from "../lib/dates"; @@ -53,14 +53,16 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) { window.location.search = searchParams.toString(); } - const { classes, lastUpdated, termInfo } = await fetchNoCache( - `/${termToFetch}.json`, - ); + const { classes, lastUpdated, termInfo, pe } = + await fetchNoCache(`/${termToFetch}.json`); + const classesMap = new Map(Object.entries(classes)); + const peClassesMap = new Map(Object.entries(pe?.[0] ?? {})); return { globalState: new State( classesMap, + peClassesMap, new Term(termInfo), lastUpdated, latestTerm.semester.urlName, @@ -72,6 +74,11 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) { function HydrantApp() { const { state } = useContext(HydrantContext); + const tabs = classTypeComponents([ + ...(state.classes.size > 0 ? ["academic"] : []), + ...(state.peClasses.size > 0 ? ["pe"] : []), + ]); + return ( <> @@ -109,7 +116,7 @@ function HydrantApp() { }} > - {Object.entries(CLASS_TYPE_COMPONENTS).map(([key, [Icon]]) => ( + {Object.entries(tabs).map(([key, [Icon]]) => ( {key} @@ -117,24 +124,22 @@ function HydrantApp() { ))} - {Object.entries(CLASS_TYPE_COMPONENTS).map( - ([key, [_, Component]]) => ( - - - - ), - )} + {Object.entries(tabs).map(([key, [_, Component]]) => ( + + + + ))}
diff --git a/src/routes/export.ts b/src/routes/export.ts index 5bf56a14..c1094c95 100644 --- a/src/routes/export.ts +++ b/src/routes/export.ts @@ -33,12 +33,14 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) { const term = urlName === latestTerm.semester.urlName ? "latest" : urlName; - const { classes, lastUpdated, termInfo } = await fetchNoCache( - `/${term}.json`, - ); + const { classes, lastUpdated, termInfo, pe } = + await fetchNoCache(`/${term}.json`); const classesMap = new Map(Object.entries(classes)); + const peClassesMap = new Map(Object.entries(pe?.[0] ?? {})); + const hydrantObj = new State( classesMap, + peClassesMap, new Term(termInfo), lastUpdated, latestTerm.semester.urlName, From 532ad1472ed0ea4e6eda15470697f2b2869cadac Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 23:20:13 -0500 Subject: [PATCH 27/89] dont use tabs for older terms --- src/components/ClassTypes.tsx | 58 +++++++++++++++++++++++++++++++++-- src/routes/_index.tsx | 48 ++--------------------------- 2 files changed, 58 insertions(+), 48 deletions(-) diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx index 2965276e..0f74c1c8 100644 --- a/src/components/ClassTypes.tsx +++ b/src/components/ClassTypes.tsx @@ -1,8 +1,12 @@ +import { Tabs } from "@chakra-ui/react"; +import { useContext } from "react"; + import { ClassTable } from "./ClassTable"; import { PEClassTable } from "./PEClassTable"; import { ClassType } from "~/lib/schema"; import { LuGraduationCap, LuVolleyball } from "react-icons/lu"; import type { IconType } from "react-icons/lib"; +import { HydrantContext } from "~/lib/hydrant"; export const Academic = () => { return ; @@ -13,16 +17,64 @@ export const PEandW = () => { }; // eslint-disable-next-line react-refresh/only-export-components -export function classTypeComponents(termKeys: string[]) { +export function classTypeComponents(termKeys: ClassType[]) { const obj = {} as Record; - if (termKeys.includes("academic")) { + if (termKeys.includes(ClassType.ACADEMIC)) { obj[ClassType.ACADEMIC] = [LuGraduationCap, Academic]; } - if (termKeys.includes("pe")) { + if (termKeys.includes(ClassType.PEW)) { obj[ClassType.PEW] = [LuVolleyball, PEandW]; } return obj; } + +export const ClassTypesSwitcher = () => { + const { state } = useContext(HydrantContext); + + const tabs = classTypeComponents([ + ...(state.classes.size > 0 ? [ClassType.ACADEMIC] : []), + ...(state.peClasses.size > 0 ? [ClassType.PEW] : []), + ]); + + if (Object.keys(tabs).length > 1) return ( + { + state.currentClassType = e.value as ClassType; + }} + > + + {Object.entries(tabs).map(([key, [Icon]]) => ( + + + {key} + + ))} + + + {Object.entries(tabs).map(([key, [_, Component]]) => ( + + + + ))} + + ) + + return Object.entries(tabs).map(([_k, [_i, Component]]) => ) +}; diff --git a/src/routes/_index.tsx b/src/routes/_index.tsx index 35e4fcca..c57fa62e 100644 --- a/src/routes/_index.tsx +++ b/src/routes/_index.tsx @@ -1,4 +1,4 @@ -import { Center, Flex, Group, Tabs, ButtonGroup } from "@chakra-ui/react"; +import { Center, Flex, Group, ButtonGroup } from "@chakra-ui/react"; import { Calendar } from "../components/Calendar"; import { LeftFooter } from "../components/Footers"; import { Header, PreferencesDialog } from "../components/Header"; @@ -12,7 +12,7 @@ import { PreregLink, ExportCalendar, } from "../components/ButtonsLinks"; -import { classTypeComponents } from "../components/ClassTypes"; +import { ClassTypesSwitcher } from "../components/ClassTypes"; import { State } from "../lib/state"; import { Term } from "../lib/dates"; @@ -21,8 +21,6 @@ import { useHydrant, HydrantContext, fetchNoCache } from "../lib/hydrant"; import { getClosestUrlName, type LatestTermInfo } from "../lib/dates"; import type { Route } from "./+types/_index"; -import { useContext } from "react"; -import type { ClassType } from "~/lib/schema"; import { ActivityDescription } from "~/components/ActivityDescription"; // eslint-disable-next-line react-refresh/only-export-components @@ -72,13 +70,6 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) { /** The application entry. */ function HydrantApp() { - const { state } = useContext(HydrantContext); - - const tabs = classTypeComponents([ - ...(state.classes.size > 0 ? ["academic"] : []), - ...(state.peClasses.size > 0 ? ["pe"] : []), - ]); - return ( <> @@ -107,40 +98,7 @@ function HydrantApp() { - { - state.currentClassType = e.value as ClassType; - }} - > - - {Object.entries(tabs).map(([key, [Icon]]) => ( - - - {key} - - ))} - - - {Object.entries(tabs).map(([key, [_, Component]]) => ( - - - - ))} - +
From ad3f65905a0642ea3f821b47775525dfbd8fac16 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 23:25:47 -0500 Subject: [PATCH 28/89] simplify --- src/components/ClassTypes.tsx | 85 +++++++++++++++------------------ src/components/PEClassTable.tsx | 11 +---- src/lib/activity.ts | 10 ++-- src/lib/class.ts | 4 +- src/lib/pe.ts | 4 +- 5 files changed, 48 insertions(+), 66 deletions(-) diff --git a/src/components/ClassTypes.tsx b/src/components/ClassTypes.tsx index 0f74c1c8..c4520694 100644 --- a/src/components/ClassTypes.tsx +++ b/src/components/ClassTypes.tsx @@ -8,24 +8,16 @@ import { LuGraduationCap, LuVolleyball } from "react-icons/lu"; import type { IconType } from "react-icons/lib"; import { HydrantContext } from "~/lib/hydrant"; -export const Academic = () => { - return ; -}; - -export const PEandW = () => { - return ; -}; - // eslint-disable-next-line react-refresh/only-export-components export function classTypeComponents(termKeys: ClassType[]) { const obj = {} as Record; if (termKeys.includes(ClassType.ACADEMIC)) { - obj[ClassType.ACADEMIC] = [LuGraduationCap, Academic]; + obj[ClassType.ACADEMIC] = [LuGraduationCap, ClassTable]; } if (termKeys.includes(ClassType.PEW)) { - obj[ClassType.PEW] = [LuVolleyball, PEandW]; + obj[ClassType.PEW] = [LuVolleyball, PEClassTable]; } return obj; @@ -39,42 +31,43 @@ export const ClassTypesSwitcher = () => { ...(state.peClasses.size > 0 ? [ClassType.PEW] : []), ]); - if (Object.keys(tabs).length > 1) return ( - { - state.currentClassType = e.value as ClassType; - }} - > - - {Object.entries(tabs).map(([key, [Icon]]) => ( - - - {key} - + if (Object.keys(tabs).length > 1) + return ( + { + state.currentClassType = e.value as ClassType; + }} + > + + {Object.entries(tabs).map(([key, [Icon]]) => ( + + + {key} + + ))} + + + {Object.entries(tabs).map(([key, [_, Component]]) => ( + + + ))} - - - {Object.entries(tabs).map(([key, [_, Component]]) => ( - - - - ))} - - ) +
+ ); - return Object.entries(tabs).map(([_k, [_i, Component]]) => ) + return Object.entries(tabs).map(([_k, [_i, Component]]) => ); }; diff --git a/src/components/PEClassTable.tsx b/src/components/PEClassTable.tsx index a23150f1..00d0fb84 100644 --- a/src/components/PEClassTable.tsx +++ b/src/components/PEClassTable.tsx @@ -232,10 +232,7 @@ const CLASS_FLAGS_2: FilterGroup = [ ["pirate", "🏴‍☠️ Pirate Certificate"], ]; -const CLASS_FLAGS = [ - ...CLASS_FLAGS_1, - ...CLASS_FLAGS_2, -]; +const CLASS_FLAGS = [...CLASS_FLAGS_1, ...CLASS_FLAGS_2]; /** Div containing all the flags like "HASS". Maintains the flag filter. */ function ClassFlags(props: { @@ -379,11 +376,7 @@ function ClassFlags(props: { {allFlags ? "Less filters" : "More filters"} - {allFlags && ( - <> - {renderGroup(CLASS_FLAGS_2)} - - )} + {allFlags && <>{renderGroup(CLASS_FLAGS_2)}} ); } diff --git a/src/lib/activity.ts b/src/lib/activity.ts index d8550c1d..2a57582a 100644 --- a/src/lib/activity.ts +++ b/src/lib/activity.ts @@ -1,13 +1,11 @@ import type { EventInput } from "@fullcalendar/core"; import { nanoid } from "nanoid"; -import type { Class } from "./class"; import type { ColorScheme } from "./colors"; import { fallbackColor, textColor } from "./colors"; import { Slot } from "./dates"; import type { RawTimeslot } from "./rawClass"; import { sum } from "./utils"; -import type { PEClass } from "./pe"; /** A period of time, spanning several Slots. */ export class Timeslot { @@ -66,7 +64,8 @@ export class Timeslot { } } -export interface ActivityClass { +/** Shared interface for classes and non-classes. */ +export interface Activity { id: string; backgroundColor: string; manualColor: boolean; @@ -124,7 +123,7 @@ export class Event { } /** A non-class activity. */ -export class CustomActivity implements ActivityClass { +export class CustomActivity implements Activity { /** ID unique over all Activities. */ readonly id: string; name = "New Activity"; @@ -202,6 +201,3 @@ export class CustomActivity implements ActivityClass { } } } - -/** Shared interface for Class and non-classes. */ -export type Activity = Class | CustomActivity | PEClass; diff --git a/src/lib/class.ts b/src/lib/class.ts index 38cfd84e..21029e45 100644 --- a/src/lib/class.ts +++ b/src/lib/class.ts @@ -1,4 +1,4 @@ -import { Timeslot, Event, type ActivityClass } from "./activity"; +import { Timeslot, Event, type Activity } from "./activity"; import type { ColorScheme } from "./colors"; import { fallbackColor } from "./colors"; import { @@ -277,7 +277,7 @@ export class Sections { } /** An entire class, e.g. 6.036, and its selected sections. */ -export class Class implements ActivityClass { +export class Class implements Activity { /** * The RawClass being wrapped around. Nothing outside Class should touch * this; instead use the Class getters like cls.id, cls.number, etc. diff --git a/src/lib/pe.ts b/src/lib/pe.ts index 2f29adb3..4f33f0a6 100644 --- a/src/lib/pe.ts +++ b/src/lib/pe.ts @@ -1,4 +1,4 @@ -import type { ActivityClass } from "./activity"; +import type { Activity } from "./activity"; import type { RawPEClass } from "./rawPEClass"; import type { Event } from "./activity"; import { fallbackColor, type ColorScheme } from "./colors"; @@ -6,7 +6,7 @@ import { fallbackColor, type ColorScheme } from "./colors"; /** * PE&W activity placeholder */ -export class PEClass implements ActivityClass { +export class PEClass implements Activity { backgroundColor: string; manualColor = false; readonly rawClass: RawPEClass; From 7aee341347cb0e16620773dae58c736e18c04b9e Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Thu, 22 Jan 2026 23:57:49 -0500 Subject: [PATCH 29/89] change pe data structure --- scrapers/package.py | 11 ++++++----- scrapers/pe.py | 18 +++++++----------- src/lib/hydrant.ts | 17 ++++++++++++++++- src/lib/pe.ts | 9 +++++++++ src/lib/state.ts | 14 +++++++++----- src/routes/_index.tsx | 6 ++---- src/routes/export.ts | 5 ++--- 7 files changed, 51 insertions(+), 29 deletions(-) diff --git a/scrapers/package.py b/scrapers/package.py index cf0c2684..fd4497b8 100644 --- a/scrapers/package.py +++ b/scrapers/package.py @@ -19,7 +19,7 @@ from collections.abc import Iterable from typing import Any -from scrapers.pe import get_pe_files +from scrapers.pe import get_pe_quarters from scrapers.utils import get_term_info if sys.version_info >= (3, 11): @@ -150,11 +150,12 @@ def run() -> None: term_info = get_term_info(sem) url_name = term_info["urlName"] - - pe_data = [] - for pe_file in get_pe_files(url_name): + + pe_data = {} + for quarter in get_pe_quarters(url_name): + pe_file = f"pe-q{quarter}.json" if os.path.isfile(os.path.join(package_dir, pe_file)): - pe_data.append(load_json_data(pe_file)) + pe_data[quarter] = load_json_data(pe_file) with open( os.path.join( diff --git a/scrapers/pe.py b/scrapers/pe.py index f3d6d0ed..c15c8b3e 100644 --- a/scrapers/pe.py +++ b/scrapers/pe.py @@ -353,7 +353,7 @@ def pe_rows_to_schema(pe_rows: list[PEWFile]) -> dict[int, dict[str, PEWSchema]] return results -def get_pe_files(url_name: str) -> list[str]: +def get_pe_quarters(url_name: str) -> list[str]: """ Gets the list of parsed PE files for a given urlName. @@ -361,28 +361,24 @@ def get_pe_files(url_name: str) -> list[str]: url_name (str): The urlName to get PE files for Returns: - list[str]: The list of PE files for the term + list[str]: The list of PE quarters for the term - >>> get_pe_files("f26") - ['pe-q1.json', 'pe-q2.json'] + >>> get_pe_quarters("f26") + [1, 2] - >>> get_pe_files("i26") - ['pe-q5.json'] + >>> get_pe_quarters("i26") + [5] """ assert url_name[0] in ("f", "i", "s", "m"), "Invalid urlName format" - quarter = { + return { "f": [1, 2], # Fall "s": [3, 4], # Spring "i": [5], # IAP "m": [], # Summer }[url_name[0]] - files = [f"pe-q{q}.json" for q in quarter] - - return files - def run(): """ diff --git a/src/lib/hydrant.ts b/src/lib/hydrant.ts index 6c67ebb6..e3034fec 100644 --- a/src/lib/hydrant.ts +++ b/src/lib/hydrant.ts @@ -12,9 +12,24 @@ export interface SemesterData { classes: Record; lastUpdated: string; termInfo: TermInfo; - pe?: Record[]; + pe?: Record>; } +export const getStateMaps = ( + classes: SemesterData["classes"], + pe?: SemesterData["pe"], +) => { + const classesMap = new Map(Object.entries(classes)); + const peClassesMap = Object.entries(pe ?? {}).reduce< + Record> + >((acc, [quarter, peClasses]) => { + acc[Number(quarter)] = new Map(Object.entries(peClasses)); + return acc; + }, {}); + + return { classesMap, peClassesMap }; +}; + /** Fetch from the url, which is JSON of type T. */ export const fetchNoCache = async (url: string): Promise => { const res = await fetch(url, { cache: "no-cache" }); diff --git a/src/lib/pe.ts b/src/lib/pe.ts index 4f33f0a6..7c64bf74 100644 --- a/src/lib/pe.ts +++ b/src/lib/pe.ts @@ -2,6 +2,15 @@ import type { Activity } from "./activity"; import type { RawPEClass } from "./rawPEClass"; import type { Event } from "./activity"; import { fallbackColor, type ColorScheme } from "./colors"; +import { TermCode } from "./rawClass"; + +export const QUARTERS: Record = { + 1: TermCode.FA, + 2: TermCode.FA, + 3: TermCode.SP, + 4: TermCode.SP, + 5: TermCode.JA, +}; /** * PE&W activity placeholder diff --git a/src/lib/state.ts b/src/lib/state.ts index e74b4681..17b1b0b3 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -69,7 +69,7 @@ export class State { constructor( rawClasses: Map, - rawPEClasses: Map, + rawPEClasses: Record>, /** The current term object. */ public readonly term: Term, /** String representing last update time. */ @@ -83,8 +83,10 @@ export class State { rawClasses.forEach((cls, number) => { this.classes.set(number, new Class(cls, this.colorScheme)); }); - rawPEClasses.forEach((cls, number) => { - this.peClasses.set(number, new PEClass(cls, this.colorScheme)); + Object.values(rawPEClasses).forEach((map) => { + map.forEach((cls, number) => { + this.peClasses.set(number, new PEClass(cls, this.colorScheme)); + }); }); this.initState(); } @@ -136,9 +138,11 @@ export class State { if (this.isSelectedActivity(toAdd)) return; if (toAdd instanceof Class) { this.selectedClasses.push(toAdd); - } else if (toAdd instanceof PEClass) { + } + if (toAdd instanceof PEClass) { this.selectedPEClasses.push(toAdd); - } else { + } + if (toAdd instanceof CustomActivity) { this.selectedCustomActivities.push(toAdd); } this.updateActivities(); diff --git a/src/routes/_index.tsx b/src/routes/_index.tsx index c57fa62e..88d84534 100644 --- a/src/routes/_index.tsx +++ b/src/routes/_index.tsx @@ -16,7 +16,7 @@ import { ClassTypesSwitcher } from "../components/ClassTypes"; import { State } from "../lib/state"; import { Term } from "../lib/dates"; -import type { SemesterData } from "../lib/hydrant"; +import { type SemesterData, getStateMaps } from "../lib/hydrant"; import { useHydrant, HydrantContext, fetchNoCache } from "../lib/hydrant"; import { getClosestUrlName, type LatestTermInfo } from "../lib/dates"; @@ -53,9 +53,7 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) { const { classes, lastUpdated, termInfo, pe } = await fetchNoCache(`/${termToFetch}.json`); - - const classesMap = new Map(Object.entries(classes)); - const peClassesMap = new Map(Object.entries(pe?.[0] ?? {})); + const { classesMap, peClassesMap } = getStateMaps(classes, pe); return { globalState: new State( diff --git a/src/routes/export.ts b/src/routes/export.ts index c1094c95..5753f5df 100644 --- a/src/routes/export.ts +++ b/src/routes/export.ts @@ -1,6 +1,6 @@ import { redirect } from "react-router"; -import { fetchNoCache, type SemesterData } from "../lib/hydrant"; +import { fetchNoCache, type SemesterData, getStateMaps } from "../lib/hydrant"; import { getClosestUrlName, Term, type LatestTermInfo } from "../lib/dates"; import { State } from "../lib/state"; import { Class } from "../lib/class"; @@ -35,8 +35,7 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) { const { classes, lastUpdated, termInfo, pe } = await fetchNoCache(`/${term}.json`); - const classesMap = new Map(Object.entries(classes)); - const peClassesMap = new Map(Object.entries(pe?.[0] ?? {})); + const { classesMap, peClassesMap } = getStateMaps(classes, pe); const hydrantObj = new State( classesMap, From f19a7319776b9dc462e29f51ae8eff4062ebc173 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Fri, 23 Jan 2026 00:00:52 -0500 Subject: [PATCH 30/89] fix tsc issue --- src/components/ActivityDescription.tsx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/ActivityDescription.tsx b/src/components/ActivityDescription.tsx index 6a4447f1..359afec4 100644 --- a/src/components/ActivityDescription.tsx +++ b/src/components/ActivityDescription.tsx @@ -13,7 +13,7 @@ import { import { useColorModeValue } from "./ui/color-mode"; import { Tooltip } from "./ui/tooltip"; -import type { CustomActivity } from "../lib/activity"; +import { CustomActivity } from "../lib/activity"; import type { Flags } from "../lib/class"; import { Class, DARK_IMAGES, getFlagImg } from "../lib/class"; import { linkClasses } from "../lib/utils"; @@ -288,11 +288,15 @@ export function ActivityDescription() { return null; } - return activity instanceof Class ? ( - - ) : activity instanceof PEClass ? ( - - ) : ( - - ); -} + if (activity instanceof Class) { + return ; + } + if (activity instanceof PEClass) { + return ; + } + if (activity instanceof CustomActivity) { + return ; + } + + return null; +} \ No newline at end of file From 4722a45b20435bab9aebbc8d3d5c07b2b96e1cf9 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Fri, 23 Jan 2026 00:21:02 -0500 Subject: [PATCH 31/89] VERY TEMPORARY --- src/components/ActivityDescription.tsx | 4 +- src/lib/calendarSlots.ts | 35 ++++++-- src/lib/class.ts | 2 +- src/lib/pe.ts | 117 ++++++++++++++++++++++++- src/lib/state.ts | 11 ++- 5 files changed, 151 insertions(+), 18 deletions(-) diff --git a/src/components/ActivityDescription.tsx b/src/components/ActivityDescription.tsx index 359afec4..f91f2e32 100644 --- a/src/components/ActivityDescription.tsx +++ b/src/components/ActivityDescription.tsx @@ -297,6 +297,6 @@ export function ActivityDescription() { if (activity instanceof CustomActivity) { return ; } - + return null; -} \ No newline at end of file +} diff --git a/src/lib/calendarSlots.ts b/src/lib/calendarSlots.ts index 6708764b..e6bdcf8b 100644 --- a/src/lib/calendarSlots.ts +++ b/src/lib/calendarSlots.ts @@ -1,5 +1,6 @@ import type { CustomActivity, Timeslot } from "./activity"; import type { Section, Sections, Class } from "./class"; +import type { PEClass, PESection, PESections } from "./pe"; /** * Helper function for selectSlots. Implements backtracking: we try to place @@ -14,20 +15,20 @@ import type { Section, Sections, Class } from "./class"; * @returns Object with best options found so far and number of conflicts */ function selectHelper( - freeSections: Sections[], + freeSections: (Sections | PESections)[], filledSlots: Timeslot[], - foundOptions: Section[], + foundOptions: (Section | PESection)[], curConflicts: number, foundMinConflicts: number, ): { - options: Section[][]; + options: (Section | PESection)[][]; minConflicts: number; } { if (freeSections.length === 0) { return { options: [foundOptions], minConflicts: curConflicts }; } - let options: Section[][] = []; + let options: (Section | PESection)[][] = []; let minConflicts: number = foundMinConflicts; const [secs, ...remainingSections] = freeSections; @@ -70,15 +71,16 @@ function selectHelper( */ export function scheduleSlots( selectedClasses: Class[], + selectedPEClasses: PEClass[], selectedCustomActivities: CustomActivity[], ): { - options: Section[][]; + options: (Section | PESection)[][]; conflicts: number; } { - const lockedSections: Sections[] = []; - const lockedOptions: Section[] = []; + const lockedSections: (Sections | PESections)[] = []; + const lockedOptions: (Section | PESection)[] = []; const initialSlots: Timeslot[] = []; - const freeSections: Sections[] = []; + const freeSections: (Sections | PESections)[] = []; for (const cls of selectedClasses) { for (const secs of cls.sections) { @@ -97,6 +99,23 @@ export function scheduleSlots( } } + for (const cls of selectedPEClasses) { + for (const secs of cls.sections) { + if (secs.locked) { + const sec = secs.selected; + if (sec) { + lockedSections.push(secs); + lockedOptions.push(sec); + initialSlots.push(...sec.timeslots); + } else { + // locked to having no section, do nothing + } + } else if (secs.sections.length > 0) { + freeSections.push(secs); + } + } + } + for (const activity of selectedCustomActivities) { initialSlots.push(...activity.timeslots); } diff --git a/src/lib/class.ts b/src/lib/class.ts index 21029e45..053836e9 100644 --- a/src/lib/class.ts +++ b/src/lib/class.ts @@ -172,7 +172,7 @@ export const LockOption = { } as const; /** The type of {@link LockOption}. */ -type TLockOption = (typeof LockOption)[keyof typeof LockOption]; +export type TLockOption = (typeof LockOption)[keyof typeof LockOption]; /** All section options for a manual section time. */ export type SectionLockOption = Section | TLockOption; diff --git a/src/lib/pe.ts b/src/lib/pe.ts index 7c64bf74..2f6e0b92 100644 --- a/src/lib/pe.ts +++ b/src/lib/pe.ts @@ -1,8 +1,9 @@ -import type { Activity } from "./activity"; +import { Timeslot, type Activity } from "./activity"; import type { RawPEClass } from "./rawPEClass"; -import type { Event } from "./activity"; +import { Event } from "./activity"; import { fallbackColor, type ColorScheme } from "./colors"; -import { TermCode } from "./rawClass"; +import { TermCode, type RawSection } from "./rawClass"; +import { LockOption, type TLockOption } from "./class"; export const QUARTERS: Record = { 1: TermCode.FA, @@ -12,6 +13,112 @@ export const QUARTERS: Record = { 5: TermCode.JA, }; +export type PESectionLockOption = PESection | TLockOption; + +export class PESection { + /** Group of sections this section belongs to */ + secs: PESections; + /** Timeslots this section meets */ + timeslots: Timeslot[]; + /** String representing raw timeslots, e.g. MW9-11 or T2,F1. */ + rawTime: string; + /** Room this section meets in */ + room: string; + + /** @param section - raw section info (timeslot and room) */ + constructor(secs: PESections, rawTime: string, section: RawSection) { + this.secs = secs; + this.rawTime = rawTime; + const [rawSlots, room] = section; + this.timeslots = rawSlots.map((slot) => new Timeslot(...slot)); + this.room = room; + } + + /** Get the parsed time for this section in a format similar to the Registrar. */ + get parsedTime(): string { + const [room, days, eveningBool, times] = this.rawTime.split("/"); + + const isEvening = eveningBool === "1"; + + if (isEvening) { + return `${days} EVE (${times}) (${room})`; + } + + return `${days}${times} (${room})`; + } + + /** + * @param currentSlots - array of timeslots currently occupied + * @returns number of conflicts this section has with currentSlots + */ + countConflicts(currentSlots: Timeslot[]): number { + let conflicts = 0; + for (const slot of this.timeslots) { + for (const otherSlot of currentSlots) { + conflicts += slot.conflicts(otherSlot) ? 1 : 0; + } + } + return conflicts; + } +} + +export class PESections { + cls: PEClass; + sections: PESection[]; + /** Are these sections locked? None counts as locked. */ + locked: boolean; + /** Currently selected section out of these. None is null. */ + selected: PESection | null; + /** Overridden location for this particular section. */ + roomOverride = ""; + + constructor( + cls: PEClass, + rawTimes: string[], + secs: RawSection[], + locked?: boolean, + selected?: PESection | null, + ) { + this.cls = cls; + this.sections = secs.map((sec, i) => new PESection(this, rawTimes[i], sec)); + this.locked = locked ?? false; + this.selected = selected ?? null; + } + + /** Short name for the kind of sections these are. */ + readonly shortName = "pe"; + + readonly priority = -1; + + /** Name for the kind of sections these are. */ + readonly name = "PE and Wellness"; + + /** The event (possibly none) for this group of sections. */ + get event(): Event | null { + return this.selected + ? new Event( + this.cls, + `${this.cls.id} ${this.shortName}`, + this.selected.timeslots, + this.roomOverride || this.selected.room, + ) + : null; + } + + /** Lock a specific section of this class. Does not validate. */ + lockSection(sec: PESectionLockOption): void { + if (sec === LockOption.Auto) { + this.locked = false; + } else if (sec === LockOption.None) { + this.locked = true; + this.selected = null; + } else { + this.locked = true; + this.selected = sec; + } + } +} + /** * PE&W activity placeholder */ @@ -19,10 +126,14 @@ export class PEClass implements Activity { backgroundColor: string; manualColor = false; readonly rawClass: RawPEClass; + readonly sections: PESections[]; constructor(rawClass: RawPEClass, colorScheme: ColorScheme) { this.rawClass = rawClass; this.backgroundColor = fallbackColor(colorScheme); + this.sections = [ + new PESections(this, rawClass.rawSections, rawClass.sections), + ]; } get id(): string { diff --git a/src/lib/state.ts b/src/lib/state.ts index 17b1b0b3..95b496af 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -13,7 +13,7 @@ import { Store } from "./store"; import { sum, urldecode, urlencode } from "./utils"; import type { HydrantState, Preferences, Save } from "./schema"; import { BANNER_LAST_CHANGED, DEFAULT_PREFERENCES, ClassType } from "./schema"; -import { PEClass } from "./pe"; +import { PEClass, type PESection } from "./pe"; import type { RawPEClass } from "./rawPEClass"; /** @@ -26,7 +26,7 @@ export class State { /** Map from class number to PEClass object. */ peClasses: Map; /** Possible section choices. */ - options: Section[][] = [[]]; + options: (Section | PESection)[][] = [[]]; /** Current number of schedule conflicts. */ conflicts = 0; /** Browser-specific saved state. */ @@ -282,6 +282,7 @@ export class State { chooseColors(this.selectedActivities, this.colorScheme); const result = scheduleSlots( this.selectedClasses, + this.selectedPEClasses, this.selectedCustomActivities, ); this.options = result.options; @@ -296,14 +297,16 @@ export class State { * Used for the "fits schedule" filter in ClassTable. Might be slow; careful * with using this too frequently. */ - fitsSchedule(cls: Class): boolean { + fitsSchedule(cls: Class | PEClass): boolean { return ( !this.isSelectedActivity(cls) && (cls.sections.length === 0 || (this.selectedClasses.length === 0 && + this.selectedPEClasses.length === 0 && this.selectedCustomActivities.length === 0) || scheduleSlots( - this.selectedClasses.concat([cls]), + this.selectedClasses.concat(cls instanceof Class ? [cls] : []), + this.selectedPEClasses.concat(cls instanceof PEClass ? [cls] : []), this.selectedCustomActivities, ).conflicts === this.conflicts) ); From 957d28d5afae644a56caba1ff28be94d80a90c17 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Fri, 23 Jan 2026 00:23:49 -0500 Subject: [PATCH 32/89] fix and format --- scrapers/package.py | 2 +- scrapers/pe.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scrapers/package.py b/scrapers/package.py index fd4497b8..41a8cd21 100644 --- a/scrapers/package.py +++ b/scrapers/package.py @@ -150,7 +150,7 @@ def run() -> None: term_info = get_term_info(sem) url_name = term_info["urlName"] - + pe_data = {} for quarter in get_pe_quarters(url_name): pe_file = f"pe-q{quarter}.json" diff --git a/scrapers/pe.py b/scrapers/pe.py index c15c8b3e..54b49061 100644 --- a/scrapers/pe.py +++ b/scrapers/pe.py @@ -171,10 +171,10 @@ def term_to_semester_year(term_str: str) -> tuple[int, Term, Literal[1, 2] | Non >>> term_to_semester_year("2026Q2") (2025, , 2) - >>> term_to_semester_year("2026Q3") + >>> term_to_semester_year("2026Q5") (2026, , None) - >>> term_to_semester_year("2026Q4") + >>> term_to_semester_year("2026Q3") (2026, , 1) """ From 62ac22359499e98a5cf09a0f51215f30a5c41591 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Fri, 23 Jan 2026 00:38:33 -0500 Subject: [PATCH 33/89] make activity sections --- src/lib/activity.ts | 34 ++++++++++++++++++++++++++ src/lib/calendarSlots.ts | 22 ++++++++--------- src/lib/class.ts | 52 ++++++++++++++++++++-------------------- src/lib/pe.ts | 14 +++++++---- src/lib/state.ts | 13 ++++++---- 5 files changed, 90 insertions(+), 45 deletions(-) diff --git a/src/lib/activity.ts b/src/lib/activity.ts index 2a57582a..08b0a80f 100644 --- a/src/lib/activity.ts +++ b/src/lib/activity.ts @@ -201,3 +201,37 @@ export class CustomActivity implements Activity { } } } + +/** The non-section options for a manual section time. */ +export const LockOption = { + Auto: "Auto", + None: "None", +} as const; + +/** The type of {@link LockOption}. */ +export type TLockOption = (typeof LockOption)[keyof typeof LockOption]; + +/** All section options for a manual section time. */ +export type SectionLockOption = Section | TLockOption; + +export interface Sections { + cls: Activity; + sections: Section[]; + locked: boolean; + selected: Section | null; + roomOverride: string; + shortName: string; + priority: number; + name: string; + event: Event | null; + lockSection(sec: SectionLockOption): void; +} + +export interface Section { + secs: Sections; + timeslots: Timeslot[]; + rawTime: string; + room: string; + parsedTime: string; + countConflicts(currentSlots: Timeslot[]): number; +} diff --git a/src/lib/calendarSlots.ts b/src/lib/calendarSlots.ts index e6bdcf8b..a5de166a 100644 --- a/src/lib/calendarSlots.ts +++ b/src/lib/calendarSlots.ts @@ -1,6 +1,6 @@ -import type { CustomActivity, Timeslot } from "./activity"; -import type { Section, Sections, Class } from "./class"; -import type { PEClass, PESection, PESections } from "./pe"; +import type { CustomActivity, Timeslot, Section, Sections } from "./activity"; +import type { Class } from "./class"; +import type { PEClass } from "./pe"; /** * Helper function for selectSlots. Implements backtracking: we try to place @@ -15,20 +15,20 @@ import type { PEClass, PESection, PESections } from "./pe"; * @returns Object with best options found so far and number of conflicts */ function selectHelper( - freeSections: (Sections | PESections)[], + freeSections: Sections[], filledSlots: Timeslot[], - foundOptions: (Section | PESection)[], + foundOptions: Section[], curConflicts: number, foundMinConflicts: number, ): { - options: (Section | PESection)[][]; + options: Section[][]; minConflicts: number; } { if (freeSections.length === 0) { return { options: [foundOptions], minConflicts: curConflicts }; } - let options: (Section | PESection)[][] = []; + let options: Section[][] = []; let minConflicts: number = foundMinConflicts; const [secs, ...remainingSections] = freeSections; @@ -74,13 +74,13 @@ export function scheduleSlots( selectedPEClasses: PEClass[], selectedCustomActivities: CustomActivity[], ): { - options: (Section | PESection)[][]; + options: Section[][]; conflicts: number; } { - const lockedSections: (Sections | PESections)[] = []; - const lockedOptions: (Section | PESection)[] = []; + const lockedSections: Sections[] = []; + const lockedOptions: Section[] = []; const initialSlots: Timeslot[] = []; - const freeSections: (Sections | PESections)[] = []; + const freeSections: Sections[] = []; for (const cls of selectedClasses) { for (const secs of cls.sections) { diff --git a/src/lib/class.ts b/src/lib/class.ts index 053836e9..62f7250b 100644 --- a/src/lib/class.ts +++ b/src/lib/class.ts @@ -1,4 +1,12 @@ -import { Timeslot, Event, type Activity } from "./activity"; +import { + Timeslot, + Event, + type Activity, + type Sections, + LockOption, + type Section, + type TLockOption, +} from "./activity"; import type { ColorScheme } from "./colors"; import { fallbackColor } from "./colors"; import { @@ -113,14 +121,16 @@ export const getFlagImg = (flag: keyof Flags): string => { return flagImages[flag] ?? ""; }; +export type ClassSectionLockOption = ClassSection | TLockOption; + /** * A section is an array of timeslots that meet in the same room for the same * purpose. Sections can be lectures, recitations, or labs, for a given class. * All instances of Section belong to a Sections. */ -export class Section { +export class ClassSection implements Section { /** Group of sections this section belongs to */ - secs: Sections; + secs: ClassSections; /** Timeslots this section meets */ timeslots: Timeslot[]; /** String representing raw timeslots, e.g. MW9-11 or T2,F1. */ @@ -129,7 +139,7 @@ export class Section { room: string; /** @param section - raw section info (timeslot and room) */ - constructor(secs: Sections, rawTime: string, section: RawSection) { + constructor(secs: ClassSections, rawTime: string, section: RawSection) { this.secs = secs; this.rawTime = rawTime; const [rawSlots, room] = section; @@ -165,31 +175,19 @@ export class Section { } } -/** The non-section options for a manual section time. */ -export const LockOption = { - Auto: "Auto", - None: "None", -} as const; - -/** The type of {@link LockOption}. */ -export type TLockOption = (typeof LockOption)[keyof typeof LockOption]; - -/** All section options for a manual section time. */ -export type SectionLockOption = Section | TLockOption; - /** * A group of {@link Section}s, all the same kind (like lec, rec, or lab). At * most one of these can be selected at a time, and that selection is possibly * locked. */ -export class Sections { +export class ClassSections implements Sections { cls: Class; kind: SectionKind; - sections: Section[]; + sections: ClassSection[]; /** Are these sections locked? None counts as locked. */ locked: boolean; /** Currently selected section out of these. None is null. */ - selected: Section | null; + selected: ClassSection | null; /** Overridden location for this particular section. */ roomOverride = ""; @@ -199,11 +197,13 @@ export class Sections { rawTimes: string[], secs: RawSection[], locked?: boolean, - selected?: Section | null, + selected?: ClassSection | null, ) { this.cls = cls; this.kind = kind; - this.sections = secs.map((sec, i) => new Section(this, rawTimes[i], sec)); + this.sections = secs.map( + (sec, i) => new ClassSection(this, rawTimes[i], sec), + ); this.locked = locked ?? false; this.selected = selected ?? null; } @@ -263,7 +263,7 @@ export class Sections { } /** Lock a specific section of this class. Does not validate. */ - lockSection(sec: SectionLockOption): void { + lockSection(sec: ClassSectionLockOption): void { if (sec === LockOption.Auto) { this.locked = false; } else if (sec === LockOption.None) { @@ -298,28 +298,28 @@ export class Class implements Activity { .map((kind) => { switch (kind) { case SectionKind.LECTURE: - return new Sections( + return new ClassSections( this, SectionKind.LECTURE, rawClass.lectureRawSections, rawClass.lectureSections, ); case SectionKind.RECITATION: - return new Sections( + return new ClassSections( this, SectionKind.RECITATION, rawClass.recitationRawSections, rawClass.recitationSections, ); case SectionKind.LAB: - return new Sections( + return new ClassSections( this, SectionKind.LAB, rawClass.labRawSections, rawClass.labSections, ); case SectionKind.DESIGN: - return new Sections( + return new ClassSections( this, SectionKind.DESIGN, rawClass.designRawSections, diff --git a/src/lib/pe.ts b/src/lib/pe.ts index 2f6e0b92..443e7add 100644 --- a/src/lib/pe.ts +++ b/src/lib/pe.ts @@ -1,9 +1,15 @@ -import { Timeslot, type Activity } from "./activity"; +import { + Timeslot, + type Activity, + type Section, + type Sections, + LockOption, + type TLockOption, +} from "./activity"; import type { RawPEClass } from "./rawPEClass"; import { Event } from "./activity"; import { fallbackColor, type ColorScheme } from "./colors"; import { TermCode, type RawSection } from "./rawClass"; -import { LockOption, type TLockOption } from "./class"; export const QUARTERS: Record = { 1: TermCode.FA, @@ -15,7 +21,7 @@ export const QUARTERS: Record = { export type PESectionLockOption = PESection | TLockOption; -export class PESection { +export class PESection implements Section { /** Group of sections this section belongs to */ secs: PESections; /** Timeslots this section meets */ @@ -62,7 +68,7 @@ export class PESection { } } -export class PESections { +export class PESections implements Sections { cls: PEClass; sections: PESection[]; /** Are these sections locked? None counts as locked. */ diff --git a/src/lib/state.ts b/src/lib/state.ts index 95b496af..ff2546c4 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -1,9 +1,14 @@ import { nanoid } from "nanoid"; -import type { Timeslot, Activity } from "./activity"; +import type { + Timeslot, + Activity, + Section, + SectionLockOption, + Sections, +} from "./activity"; import { CustomActivity } from "./activity"; import { scheduleSlots } from "./calendarSlots"; -import type { Section, SectionLockOption, Sections } from "./class"; import { Class } from "./class"; import type { Term } from "./dates"; import type { ColorScheme } from "./colors"; @@ -13,7 +18,7 @@ import { Store } from "./store"; import { sum, urldecode, urlencode } from "./utils"; import type { HydrantState, Preferences, Save } from "./schema"; import { BANNER_LAST_CHANGED, DEFAULT_PREFERENCES, ClassType } from "./schema"; -import { PEClass, type PESection } from "./pe"; +import { PEClass } from "./pe"; import type { RawPEClass } from "./rawPEClass"; /** @@ -26,7 +31,7 @@ export class State { /** Map from class number to PEClass object. */ peClasses: Map; /** Possible section choices. */ - options: (Section | PESection)[][] = [[]]; + options: Section[][] = [[]]; /** Current number of schedule conflicts. */ conflicts = 0; /** Browser-specific saved state. */ From fa578b5007ba3e1259ec49b96d9239cd8b677fb0 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Fri, 23 Jan 2026 00:48:58 -0500 Subject: [PATCH 34/89] fix tests --- src/lib/class.ts | 2 +- tests/class.test.ts | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/lib/class.ts b/src/lib/class.ts index 62f7250b..da282494 100644 --- a/src/lib/class.ts +++ b/src/lib/class.ts @@ -284,7 +284,7 @@ export class Class implements Activity { */ readonly rawClass: RawClass; /** The sections associated with this class. */ - readonly sections: Sections[]; + readonly sections: ClassSections[]; /** The background color for the class, used for buttons and calendar. */ backgroundColor: string; /** Is the color set by the user (as opposed to chosen automatically?) */ diff --git a/tests/class.test.ts b/tests/class.test.ts index 95839f5e..d1e10b59 100644 --- a/tests/class.test.ts +++ b/tests/class.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { type Flags, getFlagImg, Class, Sections } from "../src/lib/class"; +import { type Flags, getFlagImg, Class, ClassSections } from "../src/lib/class"; import { CI, GIR, @@ -584,8 +584,8 @@ describe("Class", () => { test("has unlocked sections", () => { const myClass: Class = new Class(myRawClass, COLOR_SCHEME_LIGHT); - const mySections: Sections | undefined = myClass.sections.at(0); - assert(mySections instanceof Sections); + const mySections: ClassSections | undefined = myClass.sections.at(0); + assert(mySections instanceof ClassSections); mySections.locked = true; const expectedDeflated: Deflated = ["21H.143", [""], -1]; expect(myClass.deflate()).toStrictEqual(expectedDeflated); @@ -595,8 +595,9 @@ describe("Class", () => { COLOR_SCHEME_LIGHT, ); myOtherClass.inflate(expectedDeflated); - const myOtherSections: Sections | undefined = myOtherClass.sections.at(0); - assert(myOtherSections instanceof Sections); + const myOtherSections: ClassSections | undefined = + myOtherClass.sections.at(0); + assert(myOtherSections instanceof ClassSections); // If you don't change this, it is `undefined` (TODO: fix!) myOtherSections.selected = null; expect(myClass).toStrictEqual(myOtherClass); @@ -618,8 +619,8 @@ describe("Class", () => { test("has section room override", () => { const myClass: Class = new Class(myRawClass, COLOR_SCHEME_LIGHT); - const mySections: Sections | undefined = myClass.sections.at(0); - assert(mySections instanceof Sections); + const mySections: ClassSections | undefined = myClass.sections.at(0); + assert(mySections instanceof ClassSections); mySections.roomOverride = "lorem"; const expectedDeflated: Deflated = ["21H.143", ["lorem"]]; expect(myClass.deflate()).toStrictEqual(expectedDeflated); From 77e56d9ae59bff8e83e8b54b4762ae3c571c1d53 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Fri, 23 Jan 2026 00:49:04 -0500 Subject: [PATCH 35/89] fix activity buttons here --- src/components/ActivityButtons.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/ActivityButtons.tsx b/src/components/ActivityButtons.tsx index 4bedecd3..d1f6c8de 100644 --- a/src/components/ActivityButtons.tsx +++ b/src/components/ActivityButtons.tsx @@ -24,10 +24,15 @@ import { Checkbox } from "./ui/checkbox"; import { Field } from "./ui/field"; import { Radio, RadioGroup } from "./ui/radio"; -import type { Activity, CustomActivity } from "../lib/activity"; -import { Timeslot } from "../lib/activity"; -import type { Class, SectionLockOption, Sections } from "../lib/class"; -import { LockOption } from "../lib/class"; +import { + Timeslot, + LockOption, + type Activity, + type CustomActivity, + type Sections, + type SectionLockOption, +} from "../lib/activity"; +import type { Class } from "../lib/class"; import { Slot, TIMESLOT_STRINGS, WEEKDAY_STRINGS } from "../lib/dates"; import { HydrantContext } from "../lib/hydrant"; From 1d7d8a453cbe8e89a4bdb5325b453f0ccab67864 Mon Sep 17 00:00:00 2001 From: Pratyush Venkatakrishnan Date: Fri, 23 Jan 2026 00:41:52 -0500 Subject: [PATCH 36/89] Get PEClassTable working with PE classes --- src/components/PEClassTable.tsx | 118 +++++++++++++++----------------- src/lib/state.ts | 35 ++++++++-- 2 files changed, 86 insertions(+), 67 deletions(-) diff --git a/src/components/PEClassTable.tsx b/src/components/PEClassTable.tsx index 00d0fb84..68dd5c20 100644 --- a/src/components/PEClassTable.tsx +++ b/src/components/PEClassTable.tsx @@ -38,10 +38,10 @@ import { LuPlus, LuMinus, LuSearch, LuStar } from "react-icons/lu"; import { LabelledButton } from "./ui/button"; import { useColorModeValue } from "./ui/color-mode"; -import type { Class, Flags } from "../lib/class"; +import type { Flags } from "../lib/class"; +import type { PEClass } from "../lib/pe"; import { DARK_IMAGES } from "../lib/class"; import { classNumberMatch, classSort, simplifyString } from "../lib/utils"; -import type { TSemester } from "../lib/dates"; import "./ClassTable.scss"; import { HydrantContext } from "../lib/hydrant"; import type { State } from "../lib/state"; @@ -68,25 +68,26 @@ const GRID_MODULES: Module[] = [ ModuleRegistry.registerModules(GRID_MODULES); -enum ColorEnum { - Muted = "ag-cell-muted-text", - Success = "ag-cell-success-text", - Warning = "ag-cell-warning-text", - Error = "ag-cell-error-text", - Normal = "ag-cell-normal-text", -} +// TODO colors +// enum ColorEnum { +// Muted = "ag-cell-muted-text", +// Success = "ag-cell-success-text", +// Warning = "ag-cell-warning-text", +// Error = "ag-cell-error-text", +// Normal = "ag-cell-normal-text", +// } /** A single row in the class table. */ interface ClassTableRow { number: string; - size: string; + classSize: number; fee: string; name: string; - class: Class; + class: PEClass; inCharge: string; } -type ClassFilter = (cls?: Class) => boolean; +type ClassFilter = (cls?: PEClass) => boolean; /** Type of filter on class list; null if no filter. */ type SetClassFilter = Dispatch>; @@ -110,22 +111,18 @@ function ClassInput(props: { // Search results for classes. const searchResults = useRef< { - numbers: string[]; + number: string; name: string; - class: Class; + class: PEClass; }[] >(undefined); const processedRows = useMemo( () => rowData.map((data) => { - const numbers = [data.number]; - const [, otherNumber, realName] = - /^\[(.*)\] (.*)$/.exec(data.name) ?? []; - if (otherNumber) numbers.push(otherNumber); return { - numbers, - name: simplifyString(realName || data.name), + number: data.number, + name: simplifyString(data.name), class: data.class, inCharge: simplifyString(data.inCharge), }; @@ -138,12 +135,12 @@ function ClassInput(props: { const simplifyInput = simplifyString(input); searchResults.current = processedRows.filter( (row) => - row.numbers.some((number) => classNumberMatch(input, number)) || + classNumberMatch(input, row.number) || row.name.includes(simplifyInput) || row.inCharge.includes(simplifyInput), ); - const index = new Set(searchResults.current.map((cls) => cls.numbers[0])); - setInputFilter(() => (cls?: Class) => index.has(cls?.number ?? "")); + const index = new Set(searchResults.current.map((row) => row.number)); + setInputFilter(() => (cls?: PEClass) => index.has(cls?.rawClass.number ?? "")); } else { setInputFilter(null); } @@ -151,17 +148,17 @@ function ClassInput(props: { }; const onEnter = () => { - const { numbers, class: cls } = searchResults.current?.[0] ?? {}; + const { number, class: cls } = searchResults.current?.[0] ?? {}; if ( searchResults.current?.length === 1 || - numbers?.some((number) => classNumberMatch(number, classInput, true)) + (number && classNumberMatch(number, classInput, true)) ) { // first check if the first result matches state.toggleActivity(cls); onClassInputChange(""); - } else if (state.classes.has(classInput)) { + } else if (state.peClasses.has(classInput)) { // else check if this number exists exactly - const cls = state.classes.get(classInput); + const cls = state.peClasses.get(classInput); state.toggleActivity(cls); } }; @@ -208,26 +205,29 @@ function ClassInput(props: { ); } -const filtersNonFlags = { +const filters = { fits: (state, cls) => state.fitsSchedule(cls), - starred: (state, cls) => state.isClassStarred(cls), - new: (_, cls) => cls.new, -} satisfies Record boolean>; - -type Filter = keyof Flags | keyof typeof filtersNonFlags; + starred: (state, cls) => state.isPEClassStarred(cls), + // TODO move these to flags in src/lib/pe.ts + nofee: (_state, cls) => cls.rawClass.fee == "$0.00", + nopreq: (_state, cls) => cls.rawClass.prereqs == "None", + wizard: (_state, _cls) => true, // FIXME + pirate: (_state, _cls) => true, // FIXME +} satisfies Record boolean>; + +type Filter = keyof typeof filters; type FilterGroup = [Filter, string, ReactNode?][]; /** List of top filter IDs and their displayed names. */ const CLASS_FLAGS_1: FilterGroup = [ ["starred", "Starred", ], ["nofee", "No fee"], + ["nopreq", "No prereq"], ["fits", "Fits schedule"], - ["new", "✨ New!"], ]; /** List of hidden filter IDs, their displayed names, and image path, if any. */ const CLASS_FLAGS_2: FilterGroup = [ - ["nopreq", "No prereq"], ["wizard", "🔮 Wellness Wizard"], ["pirate", "🏴‍☠️ Pirate Certificate"], ]; @@ -273,20 +273,14 @@ function ClassFlags(props: { // careful! we have to wrap it with a () => because otherwise React will // think it's an updater function instead of the actual function. - setFlagsFilter(() => (cls?: Class) => { + setFlagsFilter(() => (cls?: PEClass) => { if (!cls) return false; let result = true; newFlags.forEach((value, flag) => { if ( value && - flag in filtersNonFlags && - !filtersNonFlags[flag as keyof typeof filtersNonFlags](state, cls) - ) { - result = false; - } else if ( - value && - !(flag in filtersNonFlags) && - !cls.flags[flag as keyof typeof cls.flags] + flag in filters && + !filters[flag](state, cls) ) { result = false; } @@ -309,7 +303,7 @@ function ClassFlags(props: { // hide starred button if no classes starred if ( flag === "starred" && - state.getStarredClasses().length === 0 && + state.getStarredPEClasses().length === 0 && !checked ) { return null; @@ -385,17 +379,17 @@ const StarButton = ({ cls, onStarToggle, }: { - cls: Class; + cls: PEClass; onStarToggle?: () => void; }) => { const { state } = useContext(HydrantContext); - const isStarred = state.isClassStarred(cls); + const isStarred = state.isPEClassStarred(cls); return ( - ) + // image is a react element, like an icon + ) : (