Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ const ActionButtons = () => {
)
: false;

// Server-derived role for the current viewer. Organizers and assigned
// judges cannot join their own hackathon; surfacing the role lets us
// show the right copy instead of a broken join button.
const viewerRole = (hackathon as any)?.viewerRole as
| 'organizer'
| 'judge'
| 'participant'
| 'guest'
| undefined;
const hasConflictingRole =
viewerRole === 'organizer' || viewerRole === 'judge';

const handleJoin = withAuth(async () => {
try {
await joinMutation.mutateAsync();
Expand Down Expand Up @@ -86,7 +98,13 @@ const ActionButtons = () => {

return (
<div className='flex w-full flex-col gap-3 md:w-auto md:flex-row md:items-center'>
{!isParticipant ? (
{hasConflictingRole ? (
<div className='flex h-12 w-full items-center justify-center rounded-xl border border-white/10 bg-white/5 px-8 text-sm font-medium text-gray-300 md:w-auto'>
{viewerRole === 'organizer'
? 'You are managing this hackathon'
: 'You are judging this hackathon'}
</div>
) : !isParticipant ? (
<BoundlessButton
className='s d bg-primary hover:bg-primary/90 h-12 w-full rounded-xl px-8 font-bold text-black disabled:bg-white/5 disabled:text-white/20 md:w-auto'
icon={!isRegistrationClosed && <IconUserPlus size={20} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const SubmissionCard = ({ submission }: SubmissionCardProps) => {
<img
src={logo}
alt={`${projectName} logo`}
className='absolute bottom-3 left-3 h-12 w-12 rounded-lg border-2 border-[#0D0E10] bg-[#0D0E10] object-cover shadow-lg'
className='absolute bottom-3 left-3 h-12 w-12 rounded-lg border-2 border-[#0D0E10] bg-[#0D0E10] object-contain shadow-lg'
/>
)}
</div>
Expand All @@ -130,12 +130,9 @@ const SubmissionCard = ({ submission }: SubmissionCardProps) => {

{/* Tags/Categories */}
<div className='flex flex-wrap gap-2 pt-2'>
<span className='text-primary rounded-md bg-[#232B20]/50 px-2.5 py-1 text-[10px] font-bold tracking-wider uppercase'>
{category}
</span>
{submission.category && (
<span className='rounded-md bg-white/5 px-2.5 py-1 text-[10px] font-bold tracking-wider text-gray-400 uppercase'>
{submission.category}
{category && (
<span className='text-primary rounded-md bg-[#232B20]/50 px-2.5 py-1 text-[10px] font-bold tracking-wider uppercase'>
{category}
</span>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions app/(landing)/hackathons/[slug]/submit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export default function SubmitProjectPage({
category: mySubmission.category,
description: mySubmission.description,
logo: mySubmission.logo,
banner: mySubmission.banner,
videoUrl: mySubmission.videoUrl,
introduction: mySubmission.introduction,
links: mySubmission.links,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const ParticipantsPage: React.FC = () => {
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false);
const [isJudgeModalOpen, setIsJudgeModalOpen] = useState(false);
const [criteria, setCriteria] = useState<
Array<{ title: string; weight: number; description?: string }>
Array<{ id: string; title: string; weight: number; description?: string }>
>([]);
const [isLoadingCriteria, setIsLoadingCriteria] = useState(false);

Expand Down Expand Up @@ -194,6 +194,9 @@ const ParticipantsPage: React.FC = () => {
if (response.success) {
setCriteria(
response.data?.judgingCriteria?.map(criterion => ({
// Persisted criteria always have an id (server enforces it).
// Fall back to name only for resilience against historic data.
id: criterion.id ?? criterion.name ?? '',
title: criterion.name || '',
weight: criterion.weight || 0,
description: criterion.description || '',
Expand Down
18 changes: 18 additions & 0 deletions app/auth/check-email/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,29 @@
import { MailIcon } from 'lucide-react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { BoundlessButton } from '@/components/buttons';

const POST_VERIFY_KEY = 'boundless:postVerifyCallbackUrl';

const CheckEmail = () => {
const searchParams = useSearchParams();
const email = searchParams.get('email');
const callbackUrl = searchParams.get('callbackUrl');

// The verification email link Better Auth sends does not preserve query
// params, so stash the desired post-verify destination here. The
// /auth/verify-email page reads this back after the token is consumed.
useEffect(() => {
if (typeof window === 'undefined') return;
if (callbackUrl && callbackUrl.startsWith('/')) {
try {
window.localStorage.setItem(POST_VERIFY_KEY, callbackUrl);
} catch {
// ignore quota / private mode errors
}
}
}, [callbackUrl]);

return (
<div className='flex min-h-screen items-center justify-center p-4'>
Expand Down
18 changes: 17 additions & 1 deletion app/auth/verify-email/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'sonner';

const POST_VERIFY_KEY = 'boundless:postVerifyCallbackUrl';

const VerifyEmail = () => {
const router = useRouter();
const searchParams = useSearchParams();
Expand All @@ -16,7 +18,21 @@ const VerifyEmail = () => {
fetchOptions: {
onSuccess: () => {
toast.success('Email verified successfully');
router.push('/');
// Honor a post-verify destination stashed by the signup flow
// (e.g. a judge invitation page). Same-origin paths only.
let target = '/';
if (typeof window !== 'undefined') {
try {
const stashed = window.localStorage.getItem(POST_VERIFY_KEY);
if (stashed && stashed.startsWith('/')) {
target = stashed;
}
window.localStorage.removeItem(POST_VERIFY_KEY);
} catch {
// ignore storage errors
}
}
router.push(target);
},
onError: () => {
toast.error('Failed to verify email');
Expand Down
49 changes: 49 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -618,3 +618,52 @@ input:-webkit-autofill:active {
-webkit-text-fill-color: white !important; /* Optional: Customize text color */
transition: background-color 5000s ease-in-out 0s; /* Optional: Smooth transition */
}

/* Judge portal: score slider */
.judge-slider {
appearance: none;
-webkit-appearance: none;
width: 100%;
height: 6px;
background: linear-gradient(
to right,
#2eedaa 0%,
#2eedaa var(--judge-slider-pct, 0%),
rgba(255, 255, 255, 0.08) var(--judge-slider-pct, 0%),
rgba(255, 255, 255, 0.08) 100%
);
border-radius: 9999px;
outline: none;
cursor: pointer;
}
.judge-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 9999px;
background: #2eedaa;
border: 2px solid #030303;
box-shadow:
0 0 0 1px rgba(46, 237, 170, 0.4),
0 2px 6px rgba(0, 0, 0, 0.6);
transition: transform 120ms ease;
}
.judge-slider:active::-webkit-slider-thumb,
.judge-slider:focus-visible::-webkit-slider-thumb {
transform: scale(1.1);
}
.judge-slider::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 9999px;
background: #2eedaa;
border: 2px solid #030303;
box-shadow:
0 0 0 1px rgba(46, 237, 170, 0.4),
0 2px 6px rgba(0, 0, 0, 0.6);
}
.judge-slider::-moz-range-track {
background: transparent;
height: 6px;
}
Loading
Loading