Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b9b39dd
implemnted reservations shell
dkupper4 Oct 1, 2025
0224471
add photos page shell and navbar link
khai-ta Oct 6, 2025
1ed75a0
fix type error
joeboppell Oct 7, 2025
7d4fbd1
simplify gallery + add uploader hooked to API
khai-ta Oct 8, 2025
9e9aaa1
Res system partially complete
dkupper4 Oct 9, 2025
c3b5869
fixed photo provider and aesthetic changes
joeboppell Oct 14, 2025
899fa2f
build fix
joeboppell Oct 14, 2025
f7d7ee5
fix file type
joeboppell Oct 15, 2025
2d1b764
Reservations work, cancel does not, UI is right
dkupper4 Oct 15, 2025
561cdfe
Merge branch 'khai/photo-gallery' of https://github.com/Hack-PSU/fron…
khai-ta Oct 19, 2025
c811141
only show team photos on photo gallery
khai-ta Oct 19, 2025
4f4db53
change flag enabling (local variable instead of flag)
nirjarai Oct 19, 2025
c907b12
updated prizes and challanges text
nirjarai Oct 19, 2025
4e75dc5
new track
kensac Oct 19, 2025
de96fe1
Modify the photo gallery
kensac Oct 20, 2025
aadb325
Merge pull request #385 from Hack-PSU/teamphotogallery
kensac Oct 20, 2025
c0c30de
Small mods to room reservation system
kensac Oct 20, 2025
bd4d6a8
Text font update
nirjarai Oct 20, 2025
0116d3e
Merge pull request #382 from Hack-PSU/nyikadylan/roomreserve
kensac Oct 20, 2025
0b40236
Update AwardBox styling and improve layout for better readability
kensac Oct 20, 2025
b900cc4
Merge pull request #386 from Hack-PSU/prize-and-challanges-update
kensac Oct 20, 2025
3725d17
fix flags
kensac Oct 20, 2025
6af8a59
Update dependency tailwindcss to v3.4.18
renovate[bot] Oct 20, 2025
4edef86
Update dependency @types/node to v24.8.1
renovate[bot] Oct 20, 2025
e334cdf
Update dependency eslint to v9.38.0
renovate[bot] Oct 20, 2025
9817bdf
Update dependency posthog-js to v1.276.0
renovate[bot] Oct 20, 2025
d986857
Update react monorepo
renovate[bot] Oct 20, 2025
a8f5ae7
Merge pull request #377 from Hack-PSU/renovate/eslint-monorepo
kensac Oct 20, 2025
9f16702
Merge pull request #376 from Hack-PSU/renovate/posthog-js-1.x-lockfile
kensac Oct 20, 2025
61afce7
Merge pull request #375 from Hack-PSU/renovate/node-24.x-lockfile
kensac Oct 20, 2025
0b55d3c
Merge pull request #374 from Hack-PSU/renovate/react-monorepo
kensac Oct 20, 2025
7f9b334
Merge pull request #373 from Hack-PSU/renovate/tailwindcss-monorepo
kensac Oct 20, 2025
4175443
Update dependency @posthog/nextjs-config to v1.3.4
renovate[bot] Oct 20, 2025
f639cc6
Update dependency @tanstack/react-query to v5.90.5
renovate[bot] Oct 20, 2025
c10209c
Update dependency framer-motion to v12.23.24
renovate[bot] Oct 20, 2025
27be042
Update dependency lint-staged to v16.2.4
renovate[bot] Oct 20, 2025
4c90cfd
Update nextjs monorepo to v15.5.6
renovate[bot] Oct 20, 2025
18b5b52
Merge pull request #387 from Hack-PSU/renovate/posthog-nextjs-config-…
kensac Oct 20, 2025
de266a5
Merge pull request #388 from Hack-PSU/renovate/tanstack-query-monorepo
kensac Oct 20, 2025
3fd3291
Merge pull request #389 from Hack-PSU/renovate/framer-motion-12.x-loc…
kensac Oct 20, 2025
b57b2fd
Merge pull request #390 from Hack-PSU/renovate/lint-staged-16.x-lockfile
kensac Oct 20, 2025
55e891b
Merge pull request #391 from Hack-PSU/renovate/nextjs-monorepo
kensac Oct 20, 2025
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
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@
"@tanstack/react-query": "^5.66.0",
"@types/luxon": "^3.3.2",
"@types/node": "^24.1.0",
"@types/react": "19.1.16",
"@types/react-dom": "19.1.9",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"eslint": "9.36.0",
"eslint-config-next": "15.5.4",
"eslint": "9.38.0",
"eslint-config-next": "15.5.6",
"firebase": "^12.0.0",
"form-data": "^4.0.0",
"framer-motion": "^12.0.0",
Expand All @@ -54,7 +54,7 @@
"lint-staged": "^16.0.0",
"lucide-react": "^0.544.0",
"luxon": "^3.4.2",
"next": "15.5.4",
"next": "15.5.6",
"posthog-js": "^1.257.0",
"posthog-node": "^5.5.1",
"prettier": "^3.3.3",
Expand All @@ -69,7 +69,7 @@
"sonner": "^2.0.6",
"swiper": "^12.0.0",
"tailwind-merge": "^3.3.1",
"tailwindcss": "3.4.17",
"tailwindcss": "3.4.18",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.9.2",
"zod": "^3.24.2"
Expand Down
243 changes: 243 additions & 0 deletions src/app/(protected)/photos/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
"use client";

import React, { useState } from "react";
import PhotoUpload from "@/components/PhotoUpload";
import { usePhotos } from "@/lib/api/photo";
import { PHOTO_MILESTONES } from "@/lib/api/photo";
import { Toaster } from "sonner";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Camera, Image as ImageIcon, Clock, X, ChevronLeft, ChevronRight } from "lucide-react";
import { useFirebase } from "@/lib/providers/FirebaseProvider";
import { Button } from "@/components/ui/button";

export default function PhotosPage() {
const { data: photos, isLoading, refetch } = usePhotos();
const { user } = useFirebase();
const [selectedImageIndex, setSelectedImageIndex] = useState<number | null>(null);
const [viewingMyPhotos, setViewingMyPhotos] = useState(false);

// Separate my photos (all of them) from community photos (only public)
const myPhotos = photos?.filter((photo) => {
const userId = photo.name.split("_")[0];
return userId === user?.uid;
}) || [];

const communityPhotos = photos?.filter((photo) => {
const userId = photo.name.split("_")[0];
const fileType = photo.name.split("_")[1];
return userId !== user?.uid && fileType === "public";
}) || [];

const currentPhotos = viewingMyPhotos ? myPhotos : communityPhotos;
const selectedPhoto = selectedImageIndex !== null ? currentPhotos[selectedImageIndex] : null;

const openLightbox = (index: number, isMyPhotos: boolean) => {
setSelectedImageIndex(index);
setViewingMyPhotos(isMyPhotos);
};

const closeLightbox = () => {
setSelectedImageIndex(null);
};

const goToPrevious = () => {
if (selectedImageIndex !== null && selectedImageIndex > 0) {
setSelectedImageIndex(selectedImageIndex - 1);
}
};

const goToNext = () => {
if (selectedImageIndex !== null && selectedImageIndex < currentPhotos.length - 1) {
setSelectedImageIndex(selectedImageIndex + 1);
}
};

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "ArrowLeft") goToPrevious();
if (e.key === "ArrowRight") goToNext();
if (e.key === "Escape") closeLightbox();
};

return (
<>
<Toaster richColors />
<div className="min-h-screen bg-transparent py-8 md:py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-12">
<div className="max-w-3xl">
<h1 className="text-4xl md:text-5xl font-bold mb-4">
Photo Gallery
</h1>
<p className="text-lg text-muted-foreground">
Share your HackPSU experience with the community. Upload photos, browse what others have captured, and relive the highlights of the event.
</p>
</div>
</div>

{/* Upload Section */}
<div className="mb-16">
<div className="mb-6">
<h2 className="text-2xl font-semibold mb-2">
Share a Photo
</h2>
<p className="text-muted-foreground">
Upload a photo to the public gallery and let everyone see your HackPSU moment.
</p>
</div>
<PhotoUpload onUploaded={() => refetch()} />
</div>

{/* My Photos Section */}
{myPhotos.length > 0 && (
<div className="mb-16">
<div className="mb-4">
<h2 className="text-xl font-semibold">Your Photos</h2>
<p className="text-sm text-muted-foreground">
{myPhotos.length} {myPhotos.length === 1 ? "photo" : "photos"} uploaded
</p>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{myPhotos.map((photo, idx) => (
<button
key={idx}
onClick={() => openLightbox(idx, true)}
className="group relative aspect-square overflow-hidden rounded-lg bg-muted cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
<img
src={photo.url}
alt={`Your photo ${idx + 1}`}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300" />
</button>
))}
</div>
</div>
)}

{/* Community Gallery Section */}
<div className="mb-8">
<div className="mb-6">
<h2 className="text-2xl font-semibold mb-2">
Community Highlights
</h2>
<p className="text-muted-foreground">
{isLoading
? "Loading gallery..."
: communityPhotos.length > 0
? `${communityPhotos.length} ${communityPhotos.length === 1 ? "photo" : "photos"} from the HackPSU community`
: "Waiting for the first community photo"}
</p>
</div>

{isLoading ? (
<div className="flex items-center justify-center py-20">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4" />
<p className="text-muted-foreground">Loading gallery...</p>
</div>
</div>
) : communityPhotos.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{communityPhotos.map((photo, idx) => (
<button
key={idx}
onClick={() => openLightbox(idx, false)}
className="group relative aspect-square overflow-hidden rounded-lg bg-muted cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
<img
src={photo.url}
alt={`Community photo ${idx + 1}`}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300" />
</button>
))}
</div>
) : (
<Card className="bg-muted/10 border-dashed">
<CardContent className="py-16 text-center">
<div className="max-w-md mx-auto">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-muted flex items-center justify-center">
<Camera className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-xl font-semibold mb-2">
{myPhotos.length > 0 ? "You're the First!" : "Gallery Opening Soon"}
</h3>
<p className="text-muted-foreground">
{myPhotos.length > 0
? "Your photo is the first in the gallery. Others will join soon!"
: "Upload a photo above to start the gallery and inspire others to share their moments."}
</p>
</div>
</CardContent>
</Card>
)}
</div>
</div>
</div>

{/* Lightbox Modal */}
<Dialog open={selectedImageIndex !== null} onOpenChange={closeLightbox}>
<DialogContent
className="max-w-[100vw] w-[95vw] sm:w-[90vw] h-[85vh] sm:h-[90vh] p-0 bg-black/95 border-none overflow-hidden"
onKeyDown={handleKeyDown}
>
<div className="relative w-full h-full flex items-center justify-center">
{/* Close Button */}
<button
onClick={closeLightbox}
className="absolute top-2 right-2 sm:top-4 sm:right-4 z-50 p-2 rounded-full bg-black/50 hover:bg-black/70 text-white transition-colors"
aria-label="Close"
>
<X className="h-5 w-5 sm:h-6 sm:w-6" />
</button>

{/* Previous Button */}
{selectedImageIndex !== null && selectedImageIndex > 0 && (
<button
onClick={goToPrevious}
className="absolute left-2 sm:left-4 z-50 p-2 sm:p-3 rounded-full bg-black/50 hover:bg-black/70 text-white transition-colors"
aria-label="Previous photo"
>
<ChevronLeft className="h-6 w-6 sm:h-8 sm:w-8" />
</button>
)}

{/* Next Button */}
{selectedImageIndex !== null && selectedImageIndex < currentPhotos.length - 1 && (
<button
onClick={goToNext}
className="absolute right-2 sm:right-4 z-50 p-2 sm:p-3 rounded-full bg-black/50 hover:bg-black/70 text-white transition-colors"
aria-label="Next photo"
>
<ChevronRight className="h-6 w-6 sm:h-8 sm:w-8" />
</button>
)}

{/* Image Container */}
{selectedPhoto && (
<div className="relative w-full h-full flex items-center justify-center px-12 sm:px-16 py-12 sm:py-16">
<img
src={selectedPhoto.url}
alt={selectedPhoto.name}
className="max-w-full max-h-full w-auto h-auto object-contain"
style={{
maxWidth: 'calc(100vw - 4rem)',
maxHeight: 'calc(85vh - 6rem)',
}}
/>
{/* Image Counter */}
<div className="absolute bottom-2 sm:bottom-4 left-1/2 -translate-x-1/2 px-3 py-1.5 sm:px-4 sm:py-2 rounded-full bg-black/50 text-white text-xs sm:text-sm">
{selectedImageIndex !== null && `${selectedImageIndex + 1} / ${currentPhotos.length}`}
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
</>
);
}
14 changes: 14 additions & 0 deletions src/app/(protected)/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
GraduationCap,
HelpCircle,
} from "lucide-react";
import { Roofing, Room } from "@mui/icons-material";

export default function Profile() {
const { isAuthenticated, user, logout, isLoading } = useFirebase();
Expand Down Expand Up @@ -143,6 +144,10 @@ export default function Profile() {
router.push("/team");
};

const handleReserve = () => {
router.push("/reservation")
}

const handleProject = () => {
router.push("/project");
};
Expand Down Expand Up @@ -384,6 +389,15 @@ export default function Profile() {
<Users className="mr-2 h-4 w-4" />
Manage Team
</Button>
<Button
onClick={handleReserve}
className="w-full"
variant="default"
size="lg"
>
<Room className="mr-2 h-4 w-4" />
Reserve Room
</Button>
</>
) : (
<>
Expand Down
8 changes: 8 additions & 0 deletions src/app/(protected)/reservation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ReservationSystem from "@/components/ReservationSystem";
import React from "react";

const ReservationPage = () => {
return <ReservationSystem />;
};

export default ReservationPage;
10 changes: 9 additions & 1 deletion src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,15 @@ const Navbar: React.FC = () => {
? { href: "/profile", text: "Profile" }
: { href: "/profile", text: "Register" };

return [...baseItems, authItem];
// Only show photos link for authenticated users (keeps homepage clean for guests)
const photosItem =
!isLoading && isAuthenticated
? { href: "/photos", text: "Photos" }
: null;

return photosItem
? [...baseItems, photosItem, authItem]
: [...baseItems, authItem];
};

const navItems = getNavItems();
Expand Down
38 changes: 25 additions & 13 deletions src/components/PhotoGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,31 @@ import Image from "next/image";
interface CarouselProps {
/** Array of image URLs (e.g. ['/images/carousel/1.jpg', '/images/carousel/2.jpg', ...]) */
images: string[];
/** Variant for different styling - 'default' for home page, 'photos' for photos page */
variant?: 'default' | 'photos';
}

const PhotoGallery: React.FC<CarouselProps> = ({ images }) => (
<div className="w-full overflow-hidden bg-[#215172] py-8 md:py-16">
{/* Header */}
<div className="text-center mb-8">
<h1
className="text-4xl md:text-5xl font-bold text-[#FFEBB8] mb-3"
style={{ fontFamily: "Monomaniac One, monospace" }}
>
Gallery
</h1>
<div className="w-16 h-1 bg-[#FFEBB8] rounded-full mx-auto"></div>
</div>
const PhotoGallery: React.FC<CarouselProps> = ({ images, variant = 'default' }) => {
const isPhotosPage = variant === 'photos';

return (
<div className={`w-full overflow-hidden py-8 md:py-16 ${
isPhotosPage ? 'bg-[#ffebb8]' : 'bg-[#215172]'
}`}>
{/* Header */}
<div className="text-center mb-8">
<h1
className={`text-4xl md:text-5xl font-bold mb-3 ${
isPhotosPage ? 'text-[#215172]' : 'text-[#FFEBB8]'
}`}
style={{ fontFamily: "Monomaniac One, monospace" }}
>
Gallery
</h1>
<div className={`w-16 h-1 rounded-full mx-auto ${
isPhotosPage ? 'bg-[#215172]' : 'bg-[#FFEBB8]'
}`}></div>
</div>
<Swiper
modules={[Pagination, Autoplay]}
slidesPerView={1}
Expand Down Expand Up @@ -104,6 +115,7 @@ const PhotoGallery: React.FC<CarouselProps> = ({ images }) => (
}
`}</style>
</div>
);
);
};

export default PhotoGallery;
Loading
Loading