Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add LocationPicker for students to select physical location #157

Merged
merged 2 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
50 changes: 50 additions & 0 deletions src/components/location-picker/locationConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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
}

// 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> = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry if other classes use this, this might mess them up

perhaps what we can do is check if the name of the location is "Soda 271" or "Soda 273" also

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added checking of location name to next commit

9: {
// Soda 271
hasLocationPicker: true,
staffImagePath: "/location-picker-images/271-staff-v1.png",
studentImagePath: "/location-picker-images/271-student-v1.png",
},
10: {
// Soda 273
hasLocationPicker: true,
staffImagePath: "/location-picker-images/273-staff-v1.png",
studentImagePath: "/location-picker-images/273-student-v1.png",
},
11: {
// Soda 275
hasLocationPicker: true,
staffImagePath: "/location-picker-images/275-staff-v1.png",
studentImagePath: "/location-picker-images/275-student-v1.png",
},
};

// Check if a location supports the visual picker
export const hasLocationPicker = (locationId: number): boolean => {
return !!locationConfigs[locationId]?.hasLocationPicker;
};

// 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;
15 changes: 13 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,14 @@ 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) &&
!parseCoordinates(existingTicket.locationDescription ?? undefined)
}
>
Confirm
</Button>
</ModalFooter>
Expand Down
Loading