- {/* Image Counter */}
-
- {selectedImageIndex !== null && `${selectedImageIndex + 1} / ${currentPhotos.length}`}
+ {/* Image Counter & Privacy Badge */}
+
+
+ {selectedImageIndex !== null && `${selectedImageIndex + 1} / ${currentPhotos.length}`}
+
+ {isPhotoPrivate(selectedPhoto) && (
+
+
+ Private
+
+ )}
)}
diff --git a/src/app/(protected)/project/page.tsx b/src/app/(protected)/project/page.tsx
index 97fdfde5..f08ba0b3 100644
--- a/src/app/(protected)/project/page.tsx
+++ b/src/app/(protected)/project/page.tsx
@@ -42,6 +42,7 @@ export default function Project() {
const { data: teams, error: teamsError } = useAllTeams();
const [projectName, setProjectName] = useState("");
+ const [githubLink, setGithubLink] = useState ("");
const [devpostLink, setDevpostLink] = useState("");
const [selectedCategories, setSelectedCategories] = useState<
ProjectCategory[]
@@ -107,6 +108,7 @@ export default function Project() {
useEffect(() => {
if (existingProject) {
setProjectName(existingProject.name);
+ setGithubLink(existingProject.githubLink || "");
setDevpostLink(existingProject.devpostLink || "");
if (existingProject.categories) {
const categories = existingProject.categories
@@ -141,6 +143,19 @@ export default function Project() {
return;
}
+ // Validate Github Link - required
+ if (!githubLink.trim()) {
+ toast.error("Github link is required");
+ return;
+ }
+
+ if (!isValidGithubUrl(githubLink)) {
+ toast.error(
+ "Please enter a valid Github URL (must be from github.com)"
+ );
+ return;
+ }
+
// Validate Devpost link - now required
if (!devpostLink.trim()) {
toast.error("Devpost link is required");
@@ -168,6 +183,7 @@ export default function Project() {
? selectedCategories.join(", ")
: undefined,
teamId: userTeam.id,
+ githubLink: githubLink.trim(),
devpostLink: devpostLink.trim(),
};
@@ -187,6 +203,18 @@ export default function Project() {
}
};
+ const isValidGithubUrl = (url: string) => {
+ try {
+ const parsedUrl = new URL(url);
+ return (
+ parsedUrl.hostname === "github.com" ||
+ parsedUrl.hostname === "www.github.com"
+ );
+ } catch (_) {
+ return false;
+ }
+ };
+
const isValidDevpostUrl = (url: string) => {
try {
const parsedUrl = new URL(url);
@@ -325,6 +353,19 @@ export default function Project() {
/>
Devpost Link *
diff --git a/src/components/PhotoUpload.tsx b/src/components/PhotoUpload.tsx
index 1fc19b5e..1a0570fb 100644
--- a/src/components/PhotoUpload.tsx
+++ b/src/components/PhotoUpload.tsx
@@ -2,19 +2,13 @@
import React, { useRef, useState } from "react";
import { useUploadPhoto } from "@/lib/api/photo";
-import { PHOTO_MILESTONES } from "@/lib/api/photo";
import { Button } from "@/components/ui/button";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
import { Card, CardContent } from "@/components/ui/card";
-import { Upload, X, Camera, Loader2 } from "lucide-react";
+import { Upload, X, Camera, Loader2, Lock, Globe } from "lucide-react";
import { toast } from "sonner";
import { Progress } from "@/components/ui/progress";
+import { Label } from "@/components/ui/label";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
interface FileWithPreview {
file: File;
@@ -28,7 +22,7 @@ export default function PhotoUpload({
}) {
const inputRef = useRef
(null);
const [files, setFiles] = useState([]);
- const [fileType, setFileType] = useState("public");
+ const [isPublic, setIsPublic] = useState(true);
const [isDragging, setIsDragging] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState<{current: number, total: number}>({current: 0, total: 0});
@@ -114,8 +108,8 @@ export default function PhotoUpload({
};
const startUpload = async () => {
- if (files.length === 0 || !fileType) {
- toast.error("Please select at least one file and category");
+ if (files.length === 0) {
+ toast.error("Please select at least one file");
return;
}
@@ -124,6 +118,7 @@ export default function PhotoUpload({
let successCount = 0;
let failCount = 0;
+ const fileType = isPublic ? "public" : "private";
for (let i = 0; i < files.length; i++) {
try {
@@ -139,7 +134,7 @@ export default function PhotoUpload({
// Clean up
files.forEach(f => URL.revokeObjectURL(f.preview));
setFiles([]);
- setFileType("public");
+ setIsPublic(true);
setIsUploading(false);
setUploadProgress({current: 0, total: 0});
onUploaded?.();
@@ -147,7 +142,7 @@ export default function PhotoUpload({
// Show summary toast
if (successCount > 0) {
toast.success(`${successCount} ${successCount === 1 ? "photo" : "photos"} uploaded successfully!`, {
- description: "All photos are reviewed by our team before appearing in the gallery. This typically takes a few minutes.",
+ description: "Your photos are being reviewed by our moderation team and will appear in your gallery once approved.",
duration: 6000,
});
}
@@ -157,22 +152,20 @@ export default function PhotoUpload({
}
};
- const selectedMilestone = PHOTO_MILESTONES.find((m) => m.id === fileType);
-
return (
-
+
-
+
{/* Upload Area */}
e.preventDefault()}
onDragEnter={() => setIsDragging(true)}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
- className={`relative border-2 border-dashed rounded-lg p-8 transition-colors ${
+ className={`relative border-2 border-dashed rounded-xl p-8 transition-all ${
isDragging
- ? "border-primary bg-primary/5"
- : "border-muted-foreground/25 hover:border-muted-foreground/50"
+ ? "border-primary bg-primary/10 scale-[1.02]"
+ : "border-muted-foreground/25 hover:border-muted-foreground/50 hover:bg-muted/20"
}`}
>
0 ? (
-
+
{files.map((fileWithPreview, idx) => (
-
+
{fileWithPreview.file.type.startsWith("video/") ? (
) : (
)}
removeFile(idx)}
- className="absolute -top-2 -right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors z-10"
+ className="absolute -top-2 -right-2 p-1.5 bg-red-500 text-white rounded-full hover:bg-red-600 transition-all shadow-lg opacity-0 group-hover:opacity-100 z-10"
+ aria-label="Remove file"
>
-
+
))}
-
-
+
+
{files.length} {files.length === 1 ? "file" : "files"} selected
) : (
-
-
+
+
+
+
Upload Photos or Videos
-
- Drag and drop your files here, or click to browse
+
+ Drag and drop your files here, or click the button below to browse
inputRef.current?.click()}
+ className="mb-3"
>
Choose Files
-
+
Supports: JPG, PNG, GIF, WebP, MP4, MOV (max 100MB each)
)}
- {/* Milestone Selection */}
-
-
- What's this photo about?{" "}
- *
-
-
-
-
-
-
-
- Public Gallery
-
- {PHOTO_MILESTONES.filter((m) => m.id !== "public").map(
- (milestone) => (
-
- {milestone.label}
-
- )
- )}
-
-
- {selectedMilestone && (
-
- {selectedMilestone.description}
+ {/* Privacy Toggle */}
+
+
Privacy Settings
+
+
+
+
+
+ All photos are reviewed by our moderation team before appearing in galleries. Public photos will be visible to everyone once approved. Private photos will only be visible to you.
- )}
+
+
setIsPublic(value === "public")}
+ disabled={isUploading}
+ className="grid grid-cols-1 sm:grid-cols-2 gap-3"
+ >
+
+
+
+
+
+
+
+
+
Public
+
+ Visible to everyone in the gallery
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Private
+
+ Only visible to you
+
+
+
+
+
+
{/* Upload Progress */}
{isUploading && (
-
+
-
+
Uploading {uploadProgress.current} of {uploadProgress.total}
-
+
{Math.round((uploadProgress.current / uploadProgress.total) * 100)}%
-
+
)}
- {/* Upload Button */}
-
+ {/* Action Buttons */}
+
{isUploading ? (
<>
@@ -308,7 +333,10 @@ export default function PhotoUpload({
Uploading...
>
) : (
- `Upload ${files.length > 0 ? files.length : ""} ${files.length === 1 ? "Photo" : "Photos"}`
+ <>
+
+ Upload {files.length > 0 ? `${files.length} ${files.length === 1 ? "Photo" : "Photos"}` : ""}
+ >
)}
{files.length > 0 && !isUploading && (
@@ -318,8 +346,10 @@ export default function PhotoUpload({
files.forEach(f => URL.revokeObjectURL(f.preview));
setFiles([]);
}}
+ className="h-11"
>
- Clear All
+
+ Clear
)}
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
new file mode 100644
index 00000000..0f4caebb
--- /dev/null
+++ b/src/components/ui/tabs.tsx
@@ -0,0 +1,55 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/src/lib/api/judging/entity.ts b/src/lib/api/judging/entity.ts
index 76aa446d..3f28be6f 100644
--- a/src/lib/api/judging/entity.ts
+++ b/src/lib/api/judging/entity.ts
@@ -21,6 +21,7 @@ export interface ProjectEntity {
hackathonId: string;
categories?: string;
teamId?: string;
+ githubLink?: string;
devpostLink?: string;
}
diff --git a/src/styles/globals.css b/src/styles/globals.css
index b512dca6..754e5bf6 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -72,6 +72,11 @@
/* Account for fixed size of the Navbar upon jumping to links. */
+ html {
+ overflow-x: hidden;
+ width: 100%;
+ }
+
body {
font-family: "Lato", "Georgia", sans-serif;
font-feature-settings:
@@ -81,6 +86,9 @@
min-height: 100vh;
margin: 0;
overscroll-behavior: none;
+ overflow-x: hidden;
+ width: 100%;
+ position: relative;
}
.dark {
--background: 0 0% 3.9%;
diff --git a/yarn.lock b/yarn.lock
index 04160049..e0e71f25 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2441,7 +2441,7 @@
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-primitive" "2.1.3"
-"@radix-ui/react-radio-group@^1.3.7":
+"@radix-ui/react-radio-group@^1.3.8":
version "1.3.8"
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz#93f102b5b948d602c2f2adb1bc5c347cbaf64bd9"
integrity sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==
@@ -2526,6 +2526,20 @@
"@radix-ui/react-use-previous" "1.1.1"
"@radix-ui/react-use-size" "1.1.1"
+"@radix-ui/react-tabs@^1.1.13":
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz#3537ce379d7e7ff4eeb6b67a0973e139c2ac1f15"
+ integrity sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==
+ dependencies:
+ "@radix-ui/primitive" "1.1.3"
+ "@radix-ui/react-context" "1.1.2"
+ "@radix-ui/react-direction" "1.1.1"
+ "@radix-ui/react-id" "1.1.1"
+ "@radix-ui/react-presence" "1.1.5"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-roving-focus" "1.1.11"
+ "@radix-ui/react-use-controllable-state" "1.2.2"
+
"@radix-ui/react-use-callback-ref@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40"