Skip to content

Commit

Permalink
Merge pull request #157 from Berkeley-CS61B/LocationPicker
Browse files Browse the repository at this point in the history
 feat: add LocationPicker for students to select physical location
  • Loading branch information
22anirudhk authored Jan 18, 2025
2 parents 3e33ea0 + 6df3b86 commit 32ed929
Show file tree
Hide file tree
Showing 14 changed files with 475 additions and 830 deletions.
812 changes: 46 additions & 766 deletions package-lock.json

Large diffs are not rendered by default.

Binary file added public/location-picker-images/271-staff-v1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/location-picker-images/271-student-v1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/location-picker-images/273-staff-v1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/location-picker-images/273-student-v1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/location-picker-images/275-staff-v1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/location-picker-images/275-student-v1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 134 additions & 0 deletions src/components/location-picker/LocationPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Box, Image, useColorModeValue } from "@chakra-ui/react";
import { UserRole } from "@prisma/client";
import { useSession } from "next-auth/react";
import { useEffect, useState } from "react";
import { getLocationImagePath } from "./locationConfig";
import { Location } from "../queue/CreateTicketForm";

interface LocationPickerProps {
onChange: (coordinates: { x: number; y: number }) => void;
initialCoordinates?: { x: number; y: number };
disabled?: boolean;
location: Location;
}

const LocationPicker = ({
onChange,
initialCoordinates,
disabled = false,
location,
}: LocationPickerProps) => {
const { data: session } = useSession();
const [coordinates, setCoordinates] = useState<{
x: number;
y: number;
} | null>(initialCoordinates || null);
const isStaff = session?.user?.role === UserRole.STAFF;

// Get the appropriate image path based on user role (staff/student)
const locationId = location?.id;
const imagePath = getLocationImagePath(locationId, isStaff);
const imageSrc = imagePath;

// Update coordinates if initialCoordinates prop changes
useEffect(() => {
if (initialCoordinates) {
setCoordinates(initialCoordinates);
} else {
// Reset to null when location changes and no initialCoordinates
setCoordinates(null);
}
}, [initialCoordinates, location?.id]);

// Track image loading state to prevent visual glitches
const [isImageLoaded, setIsImageLoaded] = useState(false);
// Track if image is wider than tall for responsive sizing
const [isHorizontal, setIsHorizontal] = useState(true);

// Handle clicks on the image to update coordinates
const handleImageClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (disabled) return;

const rect = event.currentTarget.getBoundingClientRect();
let x = ((event.clientX - rect.left) / rect.width) * 100;
let y = ((event.clientY - rect.top) / rect.height) * 100;

// Ensure coordinates stay within bounds
x = Math.min(Math.max(x, 0), 100);
y = Math.min(Math.max(y, 0), 100);

// Invert coordinates for staff view
if (isStaff) {
x = 100 - x;
y = 100 - y;
}

const newCoordinates = { x, y };
setCoordinates(newCoordinates);
onChange(newCoordinates);
};

// Determine image orientation on load for proper sizing
const handleImageLoad = (
event: React.SyntheticEvent<HTMLImageElement, Event>,
) => {
const img = event.currentTarget;
setIsImageLoaded(true);
setIsHorizontal(img.naturalWidth >= img.naturalHeight);
};

// Adjust container width based on image orientation
const maxWidth = isHorizontal ? "500px" : "300px";

return (
<Box
position="relative"
cursor={disabled ? "default" : coordinates ? "pointer" : "crosshair"}
maxWidth={maxWidth}
margin="0 auto"
role="button"
aria-label="Pick location on room layout"
borderRadius="lg"
overflow="hidden"
boxShadow="lg"
>
<Image
src={imageSrc}
alt="Room layout"
width="100%"
height="auto"
userSelect="none"
objectFit="cover"
onLoad={handleImageLoad}
style={{
opacity: isImageLoaded ? 1 : 0,
transition: "opacity 0.3s ease-in-out",
}}
/>
{isImageLoaded && coordinates && (
<>
<Box
position="absolute"
left={`${isStaff ? 100 - coordinates.x : coordinates.x}%`}
top={`${isStaff ? 100 - coordinates.y : coordinates.y}%`}
transform="translate(-50%, -50%)"
fontSize="32px"
aria-label="Selected location marker"
>
👋
</Box>
</>
)}
<Box
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
onClick={handleImageClick}
/>
</Box>
);
};

export default LocationPicker;
65 changes: 65 additions & 0 deletions src/components/location-picker/locationConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
interface LocationConfig {
hasLocationPicker: boolean; // Whether this location supports the visual picker
staffImagePath?: string; // Path to staff view floor plan
studentImagePath?: string; // Path to student view floor plan
name: string; // The name of the location that should match
}

// Map of location IDs to their configuration
// Each location can have different floor plans for staff/students
// Important: make sure the location ID value is for the correct room based on the database
const locationConfigs: Record<number, LocationConfig> = {
9: {
// Soda 271
hasLocationPicker: true,
staffImagePath: "/location-picker-images/271-staff-v1.png",
studentImagePath: "/location-picker-images/271-student-v1.png",
name: "Soda 271",
},
10: {
// Soda 273
hasLocationPicker: true,
staffImagePath: "/location-picker-images/273-staff-v1.png",
studentImagePath: "/location-picker-images/273-student-v1.png",
name: "Soda 273",
},
11: {
// Soda 275
hasLocationPicker: true,
staffImagePath: "/location-picker-images/275-staff-v1.png",
studentImagePath: "/location-picker-images/275-student-v1.png",
name: "Soda 275",
},
};

// Check if a location supports the visual picker and matches the expected name
export const hasLocationPicker = (
locationId: number,
locationName?: string,
): boolean => {
const config = locationConfigs[locationId];
if (!config?.hasLocationPicker) {
return false;
}
// If locationName is provided, verify it matches the expected name
if (locationName && config.name !== locationName) {
return false;
}
return true;
};

// Get the appropriate floor plan image based on location and user role
export const getLocationImagePath = (
locationId: number,
isStaff: boolean,
): string | undefined => {
const config = locationConfigs[locationId];
if (!config) {
return undefined;
}

// Return staff or student view based on user role
return isStaff ? config.staffImagePath : config.studentImagePath;
};

export default locationConfigs;
18 changes: 16 additions & 2 deletions src/components/modals/EditTicketModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { useState } from "react";
import { TicketWithNames } from "../../server/trpc/router/ticket";
import { DARK_GRAY_COLOR } from "../../utils/constants";
import CreateTicketForm from "../queue/CreateTicketForm";
import { hasLocationPicker } from "../location-picker/locationConfig";
import { parseCoordinates } from "../../utils/utils";

interface EditTicketModalProps {
isModalOpen: boolean;
Expand All @@ -31,7 +33,9 @@ const EditTicketModal = (props: EditTicketModalProps) => {
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
>
<ModalContent backgroundColor={useColorModeValue("", DARK_GRAY_COLOR)}>
<ModalContent
backgroundColor={useColorModeValue("white", DARK_GRAY_COLOR)}
>
<ModalHeader>Edit Ticket</ModalHeader>
<ModalCloseButton />
<ModalBody>
Expand All @@ -46,7 +50,17 @@ const EditTicketModal = (props: EditTicketModalProps) => {
<Button variant="ghost" mr={3} onClick={() => setIsModalOpen(false)}>
Cancel
</Button>
<Button colorScheme="blue" onClick={() => onSubmit(existingTicket)}>
<Button
colorScheme="blue"
onClick={() => onSubmit(existingTicket)}
isDisabled={
hasLocationPicker(
existingTicket.locationId,
existingTicket.locationName,
) &&
!parseCoordinates(existingTicket.locationDescription ?? undefined)
}
>
Confirm
</Button>
</ModalFooter>
Expand Down
Loading

0 comments on commit 32ed929

Please sign in to comment.