Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cb8d48b
feat(profile): updates and enhance notification settings management
SebasianDev Feb 4, 2026
a67bf1e
fix(SocialLogin): update social media provider from 'X' to 'twitter' …
SebasianDev Feb 5, 2026
b9cfc54
feat(set-winner): enhance project winner setting logic with error han…
SebasianDev Feb 5, 2026
b605cd3
Merge branch 'master' of https://github.com/Voyager-Ship/avalanche-do…
SebasianDev Feb 10, 2026
92c76e1
Merge branch 'master' of https://github.com/Voyager-Ship/avalanche-do…
SebasianDev Feb 17, 2026
e05b9be
fix: enhance WalletConnectButton to use a stable account request method
SebasianDev Feb 17, 2026
f354c91
feat: implement profile completion percentage calculation and integra…
SebasianDev Feb 17, 2026
853866d
refactor: replace authentication redirection with login modal trigger…
SebasianDev Feb 17, 2026
7f22132
fix: update wallet label in profile component to specify EVM Wallet
SebasianDev Feb 17, 2026
fca801a
refactor: streamline image handling in reward components and enhance …
SebasianDev Feb 18, 2026
1655353
feat: add website and socials fields to project submission form
SebasianDev Feb 18, 2026
7e4e560
feat: integrate UserAvatarContext for enhanced avatar management acro…
SebasianDev Feb 18, 2026
eda77bc
feat: enhance registration form with profile integration and field pe…
SebasianDev Feb 18, 2026
4dd4d08
fix: update profile form label from "City of Residence" to "Country"
SebasianDev Feb 18, 2026
813466c
refactor: improve avatar section in ProfileHeader for better accessib…
SebasianDev Feb 18, 2026
e54dfe5
refactor: enforce name validation in profile and registration forms
SebasianDev Feb 18, 2026
feb3b06
refactor: update form validation mode to 'onChange' in profile setup …
SebasianDev Feb 18, 2026
241de66
refactor: update styling for various components to improve layout and…
SebasianDev Feb 18, 2026
b0ec93f
refactor: update project submission handling to use Prisma.JsonNull f…
SebasianDev Feb 18, 2026
fb0036d
refactor: update import paths and comment out unused components in pr…
SebasianDev Feb 18, 2026
ed2922c
refactor: update API route handlers to use Session type and improve t…
SebasianDev Feb 19, 2026
d5d1bba
refactor: enhance error handling in project winner API and improve ty…
SebasianDev Feb 19, 2026
ee17669
Merge branch 'master' of https://github.com/Voyager-Ship/avalanche-do…
SebasianDev Feb 25, 2026
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
61 changes: 9 additions & 52 deletions app/(home)/showcase/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import React from "react";
import { redirect } from "next/navigation";
import ProjectOverview from "../../../../components/showcase/ProjectOverview";
import { getProject } from "@/server/services/projects";
import { Project } from "@/types/showcase";
import { getUserBadgesByProjectId } from "@/server/services/project-badge";
import { getAuthSession } from "@/lib/auth/authSession";
import { ShowcaseProjectAuthWrapper } from "@/components/showcase/ShowcaseProjectAuthWrapper";

export default async function ProjectPage({
params,
Expand All @@ -17,55 +15,12 @@ export default async function ProjectPage({
console.log('πŸ“ Project ID from params:', id);
console.log('πŸ“ Project ID type:', typeof id);

// Require authentication
console.log('πŸ“ Getting server session...');
const session = await getAuthSession();

console.log('πŸ“ Session exists?', !!session);
console.log('πŸ“ Session object:', JSON.stringify(session, null, 2));
console.log('πŸ“ Session.user exists?', !!session?.user);
console.log('πŸ“ Session.user.id:', session?.user?.id);

if (!session?.user?.id) {
console.log('❌ No session.user.id, redirecting to login');
redirect(`/login?callbackUrl=/showcase/${id}`);
}

// Showcase individual project pages are admin-only
console.log('πŸ“ Checking user roles...');
console.log('πŸ“ session.user.custom_attributes raw:', session.user.custom_attributes);
console.log('πŸ“ session.user.custom_attributes type:', typeof session.user.custom_attributes);
console.log('πŸ“ session.user.custom_attributes is array?', Array.isArray(session.user.custom_attributes));

const userRoles = session.user.custom_attributes || [];
console.log('πŸ“ userRoles after default:', userRoles);
console.log('πŸ“ userRoles length:', userRoles.length);
console.log('πŸ“ userRoles JSON:', JSON.stringify(userRoles));

// Check each role individually
const hasShowcase = userRoles.includes('showcase');
const hasDevrel = userRoles.includes('devrel');
const hasAdmin = userRoles.includes('admin');

console.log('πŸ“ Has "showcase" role?', hasShowcase);
console.log('πŸ“ Has "devrel" role?', hasDevrel);
console.log('πŸ“ Has "admin" role?', hasAdmin);

const hasShowcaseRole = hasShowcase || hasDevrel || hasAdmin;
console.log('πŸ“ Has ANY showcase role?', hasShowcaseRole);

if (!hasShowcaseRole) {
console.log('❌ Access denied - no showcase role, redirecting to /showcase');
redirect("/showcase?error=unauthorized");
}

console.log('βœ… Access granted! Loading project...');

console.log('πŸ”„ Fetching project with id:', id);

let project;
let badges;

try {
console.log('πŸ”„ Fetching project with id:', id);
project = await getProject(id);
console.log('βœ… Project loaded successfully:', project?.project_name);

Expand All @@ -77,11 +32,13 @@ export default async function ProjectPage({
throw error; // Re-throw to see Next.js error handling
}

console.log('🎨 Rendering ProjectOverview component');
console.log('🎨 Rendering ShowcaseProjectAuthWrapper component');

return (
<main className="container relative max-w-[1400px] pb-16">
<ProjectOverview project={project as unknown as Project} badges={badges} />
</main>
<ShowcaseProjectAuthWrapper
project={project as unknown as Project}
badges={badges}
projectId={id}
/>
);
}
11 changes: 5 additions & 6 deletions app/(home)/student-launchpad/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,26 @@ import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useState, useEffect } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useLoginModalTrigger } from '@/hooks/useLoginModal'

export default function StudentLaunchpadPage() {
const { resolvedTheme } = useTheme()
const arrowColor = resolvedTheme === "dark" ? "white" : "black"
const [iframeLoaded, setIframeLoaded] = useState(false)
const { data: session, status } = useSession()
const router = useRouter()
const { openLoginModal } = useLoginModalTrigger()

const handleIframeLoad = () => {
setIframeLoaded(true)
}

// Redirect to login if not authenticated
// Show login modal if not authenticated
useEffect(() => {
if (status === 'unauthenticated') {
const currentUrl = window.location.href
const loginUrl = `/login?callbackUrl=${encodeURIComponent(currentUrl)}`
router.push(loginUrl)
openLoginModal(currentUrl)
}
}, [status, router])
}, [status, openLoginModal])

// Show loading state while checking authentication
if (status === 'loading') {
Expand Down
54 changes: 54 additions & 0 deletions app/api/profile/extended/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,57 @@ export const PUT = withAuth(async (
}
});

/**
* PATCH /api/profile/extended/[id]
* Partial update of extended profile (useful for settings updates)
*/
export const PATCH = withAuth(async (
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
session: any
) => {
try {
const id = (await params).id;

if (!id) {
return NextResponse.json(
{ error: 'User ID is required.' },
{ status: 400 }
);
}

// verify that the user can only update their own profile
if (session.user.id !== id) {
return NextResponse.json(
{ error: 'Forbidden: You can only update your own profile.' },
{ status: 403 }
);
}

const newProfileData = (await req.json()) as UpdateExtendedProfileData;

// The service now handles all business validations
const updatedProfile = await updateExtendedProfile(id, newProfileData);

return NextResponse.json(updatedProfile);
} catch (error) {
console.error('Error in PATCH /api/profile/extended/[id]:', error);

// Handle validation errors with the appropriate status code
if (error instanceof ProfileValidationError) {
return NextResponse.json(
{ error: error.message },
{ status: error.statusCode }
);
}

// Handle other errors
return NextResponse.json(
{
error: 'Internal Server Error',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
});
16 changes: 9 additions & 7 deletions app/api/project/set-winner/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,25 @@ export const PUT = withAuthRole("badge_admin", async (req: NextRequest) => {
try {
if (!body.project_id) {
return NextResponse.json(
{ error: "project_id parameter is required" },
{ success: false, error: "project_id parameter is required" },
{ status: 400 }
);
}
if (!body.isWinner) {
if (body.isWinner === undefined) {
return NextResponse.json(
{ error: "IsWinner parameter is required" },
{ success: false, error: "isWinner parameter is required" },
{ status: 400 }
);
}
const badge = await SetWinner(body.project_id, body.isWinner, name);

const result = await SetWinner(body.project_id, body.isWinner, name);

return NextResponse.json(badge, { status: 200 });
return NextResponse.json(result, { status: 200 });
} catch (error) {
console.error("Error checking user by email:", error);
console.error("Error setting project winner:", error);
const errorMessage = error instanceof Error ? error.message : "Internal server error";
return NextResponse.json(
{ error: "Internal server error" },
{ success: false, error: errorMessage, message: errorMessage },
{ status: 500 }
);
}
Expand Down
24 changes: 10 additions & 14 deletions app/api/user/create-after-terms/route.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { AuthOptions } from '@/lib/auth/authOptions';
import { prisma } from '@/prisma/prisma';
import { syncUserDataToHubSpot } from '@/server/services/hubspotUserData';
import { getDefaultNotificationMeans } from '@/lib/notificationDefaults';
import { withAuth } from '@/lib/protectedRoute';

/**
* API endpoint to create a new user after they accept terms.
* This is called when a user verifies their email via OTP but hasn't been
* created in the database yet (to avoid creating accounts for users who
* don't accept terms).
*/
export async function POST(req: NextRequest) {
export const POST = withAuth(async (
req: NextRequest,
context: any,
session: any
) => {
try {
const session = await getServerSession(AuthOptions);

if (!session?.user?.email) {
return NextResponse.json(
{ error: 'Unauthorized: No valid session' },
{ status: 401 }
);
}

const email = session.user.email;

// Check if user already exists (shouldn't happen, but safety check)
Expand Down Expand Up @@ -51,7 +46,8 @@ export async function POST(req: NextRequest) {
authentication_mode: 'credentials',
last_login: new Date(),
notifications: notifications,
},
notification_means: getDefaultNotificationMeans(),
} as any,
});

// Sync user data to HubSpot (after terms acceptance)
Expand Down Expand Up @@ -81,4 +77,4 @@ export async function POST(req: NextRequest) {
{ status: 500 }
);
}
}
});
7 changes: 6 additions & 1 deletion app/console/history/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useSession } from 'next-auth/react';
import { useLoginModalTrigger } from '@/hooks/useLoginModal';

import { useState, useMemo } from 'react';
import useConsoleNotifications from '@/hooks/useConsoleNotifications';
Expand All @@ -24,6 +25,7 @@ import { cn } from '@/lib/cn';

export default function ConsoleHistoryPage() {
const { data: session, status } = useSession();
const { openLoginModal } = useLoginModalTrigger();
const { logs: fullHistory, getExplorerUrl, loading } = useConsoleNotifications();
const [searchTerm, setSearchTerm] = useState('');
const [copiedId, setCopiedId] = useState<string | null>(null);
Expand Down Expand Up @@ -435,7 +437,10 @@ export default function ConsoleHistoryPage() {
<Button
variant="outline"
size="sm"
onClick={() => window.location.href = '/login'}
onClick={() => {
const currentUrl = window.location.href;
openLoginModal(currentUrl);
}}
className="gap-2"
>
<LogIn className="h-4 w-4" />
Expand Down
15 changes: 9 additions & 6 deletions app/hackathons/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Plus, Trash, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
import { t } from './translations';
import { useSession, SessionProvider } from "next-auth/react";
import { useLoginModalTrigger } from "@/hooks/useLoginModal";
import axios from 'axios';
import { initialData, IDataMain, IDataContent, IDataLatest, ITrack, ISchedule, ISpeaker, IResource, IPartner } from './initials';
import { LanguageButton } from './language-button';
Expand Down Expand Up @@ -141,8 +142,8 @@ const UpdateModal = ({ open, onClose, onConfirm, fieldsToUpdate, t, language }:
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white dark:bg-zinc-900 rounded-lg shadow-lg p-6 max-w-4xl w-full max-h-[90vh] flex flex-col">
<h2 className="text-lg font-bold mb-4 flex-shrink-0">{t[language].confirmUpdateTitle || 'Confirm Update'}</h2>
<p className="mb-2 flex-shrink-0">{t[language].confirmUpdateText || 'You are about to update the following fields:'}</p>
<h2 className="text-lg font-bold mb-4 shrink-0">{t[language].confirmUpdateTitle || 'Confirm Update'}</h2>
<p className="mb-2 shrink-0">{t[language].confirmUpdateText || 'You are about to update the following fields:'}</p>
<ul className="list-disc pl-6 flex-1 min-h-0 overflow-y-auto overflow-x-auto mb-4">
{fieldsToUpdate.map(({ key, oldValue, newValue }) => (
<li key={key} className="mb-1">
Expand All @@ -156,7 +157,7 @@ const UpdateModal = ({ open, onClose, onConfirm, fieldsToUpdate, t, language }:
</li>
))}
</ul>
<div className="flex justify-end gap-2 mt-4 flex-shrink-0">
<div className="flex justify-end gap-2 mt-4 shrink-0">
<button onClick={onClose} className="px-4 py-2 rounded bg-zinc-200 dark:bg-zinc-700 hover:bg-zinc-300 dark:hover:bg-zinc-600">{t[language].cancel}</button>
<button onClick={onConfirm} className="px-4 py-2 rounded bg-green-600 text-white hover:bg-green-700">{t[language].update}</button>
</div>
Expand Down Expand Up @@ -753,6 +754,7 @@ const ResourceItem = memo(function ResourceItem({ resource, index, collapsed, on

const HackathonsEdit = () => {
const { data: session, status } = useSession();
const { openLoginModal } = useLoginModalTrigger();
const [myHackathons, setMyHackathons] = useState<any[]>([]);
const [loadingHackathons, setLoadingHackathons] = useState<boolean>(true);
const [isSelectedHackathon, setIsSelectedHackathon] = useState(false);
Expand Down Expand Up @@ -1633,20 +1635,21 @@ const HackathonsEdit = () => {
session.user.custom_attributes.includes("devrel");
};

// Redirect unauthorized users
// Show login modal for unauthorized users
React.useEffect(() => {
if (status === "loading") return; // Still loading

if (status === "unauthenticated") {
window.location.href = "/login";
const currentUrl = window.location.href;
openLoginModal(currentUrl);
return;
}

if (status === "authenticated" && !hasRequiredPermissions()) {
window.location.href = "/";
return;
}
}, [session, status]);
}, [session, status, openLoginModal]);

// Show loading while checking authentication
if (status === "loading") {
Expand Down
5 changes: 4 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CustomCountdownBanner } from "@/components/ui/custom-countdown-banner";
import { HideOnChatPage } from "@/components/layout/chat-page-hider";
import { EmbedModeDetector } from "@/components/layout/embed-mode-detector";
import { ThemeProvider } from "@/components/content-design/theme-observer";
import { UserAvatarProvider } from "@/components/context/UserAvatarContext";

export const metadata = createMetadata({
title: {
Expand Down Expand Up @@ -50,7 +51,9 @@ export default function Layout({ children }: { children: ReactNode }) {
</HideOnChatPage>
<Body>
<ThemeProvider>
<SearchRootProvider>{children}</SearchRootProvider>
<UserAvatarProvider>
<SearchRootProvider>{children}</SearchRootProvider>
</UserAvatarProvider>
<HideOnChatPage>
<div id="privacy-banner-root" className="relative">
<PrivacyPolicyBox />
Expand Down
Loading