@@ -443,7 +458,7 @@ const MobileMenu = ({
onClick={() => setIsOpen(false)}
aria-current={isActive ? 'page' : undefined}
className={cn(
- 'rounded-lg px-4 py-3 text-sm font-medium transition-all duration-200',
+ 'rounded-lg py-3 text-sm font-medium transition-all duration-200',
isActive
? 'border'
: 'text-white/70 hover:border hover:border-white/10 hover:bg-white/5 hover:text-white/90'
diff --git a/components/landing-page/project/ProjectCard.tsx b/components/landing-page/project/ProjectCard.tsx
index a7bab2c8d..810450a97 100644
--- a/components/landing-page/project/ProjectCard.tsx
+++ b/components/landing-page/project/ProjectCard.tsx
@@ -4,6 +4,7 @@ import { formatNumber } from '@/lib/utils';
import { useRouter } from 'nextjs-toploader/app';
type ProjectCardProps = {
+ newTab?: boolean;
projectId?: string;
creatorName: string;
creatorLogo: string;
@@ -30,6 +31,7 @@ type ProjectCardProps = {
};
function ProjectCard({
projectId,
+ newTab = false,
creatorName,
creatorLogo,
projectImage,
@@ -94,7 +96,7 @@ function ProjectCard({
return (
{}}
className={`font-inter hover:border-primary/45 flex w-full ${isFullWidth ? 'max-w-full' : 'max-w-[397px]'} cursor-pointer flex-col gap-4 rounded-[8px] border border-gray-900 bg-[#030303] p-4 transition-all duration-300 sm:p-5`}
>
diff --git a/components/organization/DeleteHackathonDialog.tsx b/components/organization/DeleteHackathonDialog.tsx
new file mode 100644
index 000000000..73a6207ca
--- /dev/null
+++ b/components/organization/DeleteHackathonDialog.tsx
@@ -0,0 +1,103 @@
+'use client';
+
+import { useState } from 'react';
+import { AlertTriangle } from 'lucide-react';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@/components/ui/alert-dialog';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Label } from '@/components/ui/label';
+
+interface DeleteHackathonDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ hackathonTitle: string;
+ onConfirm: () => void;
+ isDeleting?: boolean;
+}
+
+export default function DeleteHackathonDialog({
+ open,
+ onOpenChange,
+ hackathonTitle,
+ onConfirm,
+ isDeleting = false,
+}: DeleteHackathonDialogProps) {
+ const [isConfirmed, setIsConfirmed] = useState(false);
+
+ const handleOpenChange = (newOpen: boolean) => {
+ if (!newOpen) {
+ // Reset confirmation when dialog closes
+ setIsConfirmed(false);
+ }
+ onOpenChange(newOpen);
+ };
+
+ const handleConfirm = () => {
+ if (isConfirmed) {
+ onConfirm();
+ setIsConfirmed(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ Delete Hackathon Permanently
+
+
+ This is a permanent action. All data associated with{' '}
+ "{hackathonTitle}" {' '}
+ including participants, teams, submissions, and settings will be
+ deleted immediately and cannot be recovered.
+
+
+
+
+ setIsConfirmed(checked === true)}
+ className='mt-0.5 border-zinc-700 data-[state=checked]:border-red-500 data-[state=checked]:bg-red-500 data-[state=checked]:text-white'
+ />
+
+ I understand this action is permanent and irreversible. All
+ hackathon data will be permanently deleted.
+
+
+
+
+ handleOpenChange(false)}
+ disabled={isDeleting}
+ className='border-zinc-700 bg-zinc-900 text-zinc-300 hover:bg-zinc-800 hover:text-white'
+ >
+ Cancel
+
+
+ {isDeleting ? 'Deleting...' : 'Delete Hackathon'}
+
+
+
+
+ );
+}
diff --git a/components/profile/ActivityFeed.tsx b/components/profile/ActivityFeed.tsx
index 8e5ec3b76..a8f955d15 100644
--- a/components/profile/ActivityFeed.tsx
+++ b/components/profile/ActivityFeed.tsx
@@ -151,7 +151,7 @@ export default function ActivityFeed({ filter, user }: ActivityFeedProps) {
No Activity Yet
{filter === 'All' || filter === 'All Time'
- ? 'Your activity will appear here once you start engaging'
+ ? 'Activity will appear here once the user start engaging'
: `No activity found for ${filter.toLowerCase()}`}
diff --git a/components/profile/OrganizationsList.tsx b/components/profile/OrganizationsList.tsx
index 45eb77e4b..2c1a632cf 100644
--- a/components/profile/OrganizationsList.tsx
+++ b/components/profile/OrganizationsList.tsx
@@ -3,6 +3,7 @@ import OrganizationCard from './OrganizationCard';
interface OrganizationsListProps {
organizations: Organization[];
+ isOwnProfile?: boolean;
}
export default function OrganizationsList({
diff --git a/components/profile/ProfileDataClient.tsx b/components/profile/ProfileDataClient.tsx
index 71659f79d..50f3e195d 100644
--- a/components/profile/ProfileDataClient.tsx
+++ b/components/profile/ProfileDataClient.tsx
@@ -1,4 +1,3 @@
-// ProfileDataClient.tsx - Main Component
'use client';
import { useState } from 'react';
@@ -19,6 +18,7 @@ import {
import { Button } from '../ui/button';
import ActivityHeatmap from './ActivityHeatMap';
import { FutureFeature } from '../FeatureFuture';
+import { useAuthStore } from '@/lib/stores/auth-store';
interface ProfileDataClientProps {
user: GetMeResponse;
@@ -41,6 +41,13 @@ export default function ProfileDataClient({
}: ProfileDataClientProps) {
const [selectedFilter, setSelectedFilter] = useState('All');
+ // Get auth state from store
+ const { user: authUser, isAuthenticated } = useAuthStore();
+
+ // Determine if it's the user's own profile
+ const isOwnProfile =
+ isAuthenticated && authUser?.profile?.username === username;
+
const organizationsData =
user.organizations?.map(org => ({
name: org.name,
@@ -49,7 +56,12 @@ export default function ProfileDataClient({
return (
-
+
diff --git a/components/profile/ProfileHeader.tsx b/components/profile/ProfileHeader.tsx
index 0ac4e77b3..00c37b689 100644
--- a/components/profile/ProfileHeader.tsx
+++ b/components/profile/ProfileHeader.tsx
@@ -7,29 +7,30 @@ import { UserProfile, UserStats as UserStatsType } from '@/types/profile';
import { GetMeResponse } from '@/lib/api/types';
import { TeamMember } from '@/components/ui/TeamList';
import { BoundlessButton } from '@/components/buttons';
-import { BellPlus, Settings, View } from 'lucide-react';
+import { BellPlus, Settings } from 'lucide-react';
import { ProfileSocialLinks } from '@/lib/config';
import UserStats from './UserStats';
import FollowersModal from './FollowersModal';
-import { usePathname } from 'next/navigation';
-import { FutureFeature } from '../FeatureFuture';
+import { toast } from 'sonner';
interface ProfileHeaderProps {
profile: UserProfile;
stats: UserStatsType;
user: GetMeResponse;
+ isAuthenticated?: boolean;
+ isOwnProfile?: boolean;
}
export default function ProfileHeader({
profile,
stats,
user,
+ isAuthenticated,
+ isOwnProfile,
}: ProfileHeaderProps) {
const [followersModalOpen, setFollowersModalOpen] = useState(false);
const [followingModalOpen, setFollowingModalOpen] = useState(false);
- const pathname = usePathname();
-
- // const isOwnProfile = pathname === '/me';
+ const profileUrl = `${process.env.NEXT_PUBLIC_APP_URL}/profile/${profile.username}`;
// Convert API user data to TeamMember format
const convertToTeamMembers = (
@@ -58,6 +59,28 @@ export default function ProfileHeader({
setFollowingModalOpen(true);
};
+ const handleShare = async () => {
+ if (navigator.share) {
+ try {
+ await navigator.share({
+ title: `Share ${profile.username}'s profile`,
+ url: profileUrl,
+ });
+ } catch (err) {
+ console.error('Failed to share profile:', err);
+ }
+ } else {
+ try {
+ await navigator.clipboard.writeText(profileUrl);
+ toast.success('Profile URL copied to clipboard!');
+ } catch (err) {
+ const errorMessage =
+ err instanceof Error ? err.message : 'Failed to copy URL';
+ toast.error(errorMessage);
+ }
+ }
+ };
+
return (
@@ -90,7 +113,7 @@ export default function ProfileHeader({
alt={`${name} icon`}
width={24}
height={24}
- className='h-6 w-6 fill-white'
+ className='h-4 w-4 fill-white'
/>
{index < Object.keys(ProfileSocialLinks).length - 1 && (
@@ -103,33 +126,43 @@ export default function ProfileHeader({
))}
-
-
}
- >
- Edit Profile
-
-
- {pathname ? (
-
- } iconPosition='right'>
- Preview
+ {/* Show Edit Profile only for own profile AND authenticated */}
+ {isOwnProfile && isAuthenticated && (
+
+ }
+ >
+ Edit Profile
-
- ) : (
-
- } iconPosition='right'>
- Follow
+
+ )}
+
+ {/* Show Follow button for others' profiles AND authenticated */}
+ {!isOwnProfile && isAuthenticated && (
+ } iconPosition='right' disabled>
+ Follow
+
+ )}
+
+ {/* Show sign in prompt for non-authenticated users */}
+ {!isAuthenticated && (
+
+
+ Sign in to Follow
-
+
)}
-
+
+ {/* Share button - available to everyone */}
+
Share
diff --git a/components/profile/ProfileOverview.tsx b/components/profile/ProfileOverview.tsx
index 29504f01d..89728db4e 100644
--- a/components/profile/ProfileOverview.tsx
+++ b/components/profile/ProfileOverview.tsx
@@ -8,13 +8,20 @@ import {
UserStats as UserStatsType,
Organization,
} from '@/types/profile';
+import { usePathname } from 'next/navigation';
interface ProfileOverviewProps {
username: string;
user: GetMeResponse;
+ isAuthenticated?: boolean;
+ isOwnProfile?: boolean;
}
-export default function ProfileOverview({ user }: ProfileOverviewProps) {
+export default function ProfileOverview({
+ user,
+ isAuthenticated,
+ isOwnProfile,
+}: ProfileOverviewProps) {
const profileData: UserProfile = {
username: user.profile.username,
displayName: `${user.profile.firstName} ${user.profile.lastName}`,
@@ -24,7 +31,8 @@ export default function ProfileOverview({ user }: ProfileOverviewProps) {
(user as unknown as { socialLinks?: Record })
.socialLinks || {},
};
-
+ const pathname = usePathname();
+ const isProfileRoute = pathname.startsWith('/profile');
const statsData: UserStatsType = {
organizations: user.organizations?.length || 0,
projects: user.projects?.length || 0,
@@ -33,18 +41,35 @@ export default function ProfileOverview({ user }: ProfileOverviewProps) {
};
const organizationsData: Organization[] =
- user.organizations?.map(org => ({
- name: org.name,
- avatarUrl: (org as unknown as { logo?: string }).logo || '/blog1.jpg',
- })) || [];
+ user.organizations?.map(org => {
+ const avatarUrl = isProfileRoute
+ ? (org as { avatar?: string }).avatar
+ : (org as { logo?: string }).logo;
+
+ return {
+ name: org.name,
+ avatarUrl: avatarUrl || '/blog1.jpg',
+ };
+ }) || [];
return (
-
- {/* Organizations hidden on mobile - moved to tab */}
-
-
-
+
+
+ {isAuthenticated && isOwnProfile && (
+
+
+
+ )}
);
}
diff --git a/components/profile/ProjectsTab.tsx b/components/profile/ProjectsTab.tsx
index a3e6818d2..849be3096 100644
--- a/components/profile/ProjectsTab.tsx
+++ b/components/profile/ProjectsTab.tsx
@@ -6,6 +6,7 @@ import { Project } from '@/types/project';
import { GetMeResponse } from '@/lib/api/types';
import { useWindowSize } from '@/hooks/use-window-size';
import { ScrollArea } from '@/components/ui/scroll-area';
+import Link from 'next/link';
interface ProjectsTabProps {
user: GetMeResponse;
@@ -127,24 +128,30 @@ export default function ProjectsTab({ user }: ProjectsTabProps) {
>
{projects.map(project => (
-
+ href={`/projects/${project.id}`}
+ target='_blank'
+ >
+
+
))}
{loading && (
diff --git a/components/profile/UserStats.tsx b/components/profile/UserStats.tsx
index 3316eb7be..8f9f1ff2c 100644
--- a/components/profile/UserStats.tsx
+++ b/components/profile/UserStats.tsx
@@ -2,24 +2,30 @@ import type { UserStats as UserStatsType } from '@/types/profile';
interface UserStatsProps {
stats: UserStatsType;
+ isAuthenticated?: boolean;
+ isOwnProfile?: boolean;
onFollowersClick?: () => void;
onFollowingClick?: () => void;
}
export default function UserStats({
stats,
+ isAuthenticated,
+ isOwnProfile,
onFollowersClick,
onFollowingClick,
}: UserStatsProps) {
return (
-
-
- {stats.organizations}
-
- Organizations
- Orgs
-
+ {isAuthenticated && isOwnProfile && (
+
+
+ {stats.organizations}
+
+ Organizations
+ Orgs
+
+ )}
{stats.projects}
diff --git a/hooks/hackathon/use-delete-hackathon.ts b/hooks/hackathon/use-delete-hackathon.ts
new file mode 100644
index 000000000..8002e6a15
--- /dev/null
+++ b/hooks/hackathon/use-delete-hackathon.ts
@@ -0,0 +1,66 @@
+'use client';
+
+import { useState, useCallback } from 'react';
+import { deleteHackathon } from '@/lib/api/hackathons';
+import { useAuthStatus } from '@/hooks/use-auth';
+import { toast } from 'sonner';
+
+interface UseDeleteHackathonOptions {
+ organizationId: string;
+ hackathonId: string;
+ onSuccess?: () => void;
+ onError?: (error: string) => void;
+}
+
+export function useDeleteHackathon({
+ organizationId,
+ hackathonId,
+ onSuccess,
+ onError,
+}: UseDeleteHackathonOptions) {
+ const { isAuthenticated } = useAuthStatus();
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const deleteHackathonAction = useCallback(async () => {
+ if (!isAuthenticated) {
+ toast.error('Please sign in to delete hackathons');
+ throw new Error('Authentication required');
+ }
+
+ if (!organizationId || !hackathonId) {
+ toast.error('Organization ID and Hackathon ID are required');
+ throw new Error('Organization ID and Hackathon ID are required');
+ }
+
+ setIsDeleting(true);
+ setError(null);
+
+ try {
+ const response = await deleteHackathon(organizationId, hackathonId);
+
+ if (response.success) {
+ toast.success('Hackathon deleted successfully');
+ onSuccess?.();
+ return response.data;
+ } else {
+ throw new Error(response.message || 'Failed to delete hackathon');
+ }
+ } catch (err) {
+ const errorMessage =
+ err instanceof Error ? err.message : 'Failed to delete hackathon';
+ setError(errorMessage);
+ toast.error(errorMessage);
+ onError?.(errorMessage);
+ throw err;
+ } finally {
+ setIsDeleting(false);
+ }
+ }, [organizationId, hackathonId, isAuthenticated, onSuccess, onError]);
+
+ return {
+ isDeleting,
+ error,
+ deleteHackathon: deleteHackathonAction,
+ };
+}
diff --git a/lib/api/auth-server.ts b/lib/api/auth-server.ts
index 26c0f8cc6..056084839 100644
--- a/lib/api/auth-server.ts
+++ b/lib/api/auth-server.ts
@@ -41,7 +41,7 @@ export const getUserProfileByUsernameServer = async (
message?: string;
timestamp: string;
path?: string;
- }>(`/auth/profile/${username}`, {
+ }>(`/users/profile/${username}`, {
headers: authHeaders,
});
return res.data.data;
diff --git a/lib/api/auth.ts b/lib/api/auth.ts
index b8345e310..644e26fef 100644
--- a/lib/api/auth.ts
+++ b/lib/api/auth.ts
@@ -35,7 +35,7 @@ export const getUserProfileByUsername = async (
message?: string;
timestamp: string;
path?: string;
- }>(`/auth/profile/${username}`);
+ }>(`/users/profile/${username}`);
return res.data.data;
};
diff --git a/lib/api/hackathon.ts b/lib/api/hackathon.ts
index 08aae0ac1..4c9ee4ad5 100644
--- a/lib/api/hackathon.ts
+++ b/lib/api/hackathon.ts
@@ -1,9 +1,9 @@
import { api } from './api';
import {
Hackathon,
- Participant,
SubmissionCardProps,
Discussion,
+ ParticipantsResponse,
} from '@/types/hackathon';
export interface HackathonListResponse {
@@ -24,18 +24,6 @@ export interface HackathonResponse {
message: string;
}
-export interface ParticipantsResponse {
- success: boolean;
- data: {
- participants: Participant[];
- hasMore: boolean;
- total: number;
- currentPage: number;
- totalPages: number;
- };
- message: string;
-}
-
export interface SubmissionsResponse {
success: boolean;
data: {
diff --git a/lib/api/hackathons.ts b/lib/api/hackathons.ts
index 5ee29ff20..7838dda7b 100644
--- a/lib/api/hackathons.ts
+++ b/lib/api/hackathons.ts
@@ -961,6 +961,23 @@ const transformHackathonResponse = (
};
};
+export interface AcceptTeamInvitationRequest {
+ token: string;
+}
+
+export interface AcceptTeamInvitationResponse
+ extends ApiResponse<{
+ message: string;
+ teamName: string;
+ }> {
+ success: true;
+ data: {
+ message: string;
+ teamName: string;
+ };
+ message: string;
+}
+
/**
* Transform flat API response to nested HackathonDraft structure
*/
@@ -1102,6 +1119,26 @@ export const publishHackathon = async (
};
};
+// Accpet invitition function
+export const acceptTeamInvitation = async (
+ hackathonSlugOrId: string,
+ data: AcceptTeamInvitationRequest,
+ organizationId?: string
+): Promise => {
+ let url: string;
+
+ // If organizationId is provided, use authenticated endpoint
+ if (organizationId) {
+ url = `/organizations/${organizationId}/hackathons/${hackathonSlugOrId}/team/accept`;
+ } else {
+ // Otherwise, use public slug-based endpoint
+ url = `hackathons/${hackathonSlugOrId}/team/accept`;
+ }
+
+ const res = await api.post(url, data);
+ return res.data;
+};
+
/**
* Update an existing published hackathon
*/
diff --git a/lib/providers/hackathonProvider.tsx b/lib/providers/hackathonProvider.tsx
index dc5e7c681..6f550fc33 100644
--- a/lib/providers/hackathonProvider.tsx
+++ b/lib/providers/hackathonProvider.tsx
@@ -204,10 +204,30 @@ export function HackathonDataProvider({
try {
const response = await getHackathonParticipants(slug, { limit: 50 });
if (response.success && response.data) {
- setParticipants(response.data.participants);
+ const data = response.data;
+
+ let flattenedParticipants: Participant[] = [];
+
+ // Handle both grouped and flat responses
+ if (data.grouping === 'team' && data.groups) {
+ // Flatten the groups
+ flattenedParticipants = data.groups.flatMap(group =>
+ group.members.map(member => ({
+ ...member,
+ teamId: group.teamId,
+ teamName: group.teamName,
+ isIndividual: group.isIndividual,
+ }))
+ );
+ } else if (data.participants) {
+ // Flat response
+ flattenedParticipants = data.participants;
+ }
+
+ setParticipants(flattenedParticipants);
}
} catch {
- /* ignore */
+ setParticipants([]);
}
}, []);
diff --git a/middleware.ts b/middleware.ts
index baaf21d7d..3f38f98f6 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -40,7 +40,7 @@ export async function middleware(req: NextRequest) {
// );
// }
- const isOtherUserProfile = pathname.startsWith('/profile/');
+ // const isOtherUserProfile = pathname.startsWith('/profile/');
// if (process.env.NODE_ENV === 'development') {
// // eslint-disable-next-line no-console
@@ -57,11 +57,11 @@ export async function middleware(req: NextRequest) {
return NextResponse.redirect(signinUrl);
}
- if (isOtherUserProfile && !isAuthenticated) {
- const signinUrl = new URL('/auth', req.url);
- signinUrl.searchParams.set('callbackUrl', pathname);
- return NextResponse.redirect(signinUrl);
- }
+ // if (isOtherUserProfile && !isAuthenticated) {
+ // const signinUrl = new URL('/auth', req.url);
+ // signinUrl.searchParams.set('callbackUrl', pathname);
+ // return NextResponse.redirect(signinUrl);
+ // }
return NextResponse.next();
}
diff --git a/types/hackathon.ts b/types/hackathon.ts
index d44cc8a72..2798775ca 100644
--- a/types/hackathon.ts
+++ b/types/hackathon.ts
@@ -33,6 +33,38 @@ export interface Participant {
followers?: number;
following?: number;
hasSubmitted?: boolean;
+ teamId?: string | null;
+ teamName?: string | null;
+ isIndividual?: boolean;
+}
+
+export interface ParticipantGroup {
+ teamId: string | null;
+ teamName: string | null;
+ isIndividual: boolean;
+ members: Participant[];
+ memberCount: number;
+ hasSubmission: boolean;
+ teamCreatedAt: string;
+}
+
+export interface ParticipantsData {
+ groups?: ParticipantGroup[];
+ participants?: Participant[];
+ grouping: 'team' | 'flat';
+ participantType: string;
+ hasMore: boolean;
+ total: number;
+ currentPage: number;
+ totalPages: number;
+}
+
+export interface ParticipantsResponse {
+ success: boolean;
+ data: ParticipantsData;
+ message: string;
+ timestamp?: string;
+ path?: string;
}
export interface CommentUser {