diff --git a/app/(landing)/hackathons/[slug]/page.tsx b/app/(landing)/hackathons/[slug]/page.tsx index 35fbb8dba..aec9ae495 100644 --- a/app/(landing)/hackathons/[slug]/page.tsx +++ b/app/(landing)/hackathons/[slug]/page.tsx @@ -20,7 +20,7 @@ export default function HackathonPage() { const { currentHackathon, - content, + // content, timelineEvents, participants, submissions, @@ -70,7 +70,6 @@ export default function HackathonPage() { if (loading) { return ; } - if (!currentHackathon) { return (
@@ -91,7 +90,7 @@ export default function HackathonPage() { {/* Banner */} {activeTab === 'overview' && ( diff --git a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx index 187f47e52..4432cf4ba 100644 --- a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx +++ b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx @@ -106,7 +106,7 @@ export default function DraftPreviewPage({ params }: PreviewPageProps) { const hackathon: Hackathon = { id: transformed._id, title: transformed.information.title, - subtitle: transformed.participation?.about || '', + tagline: transformed.participation?.about || '', description: transformed.information.description, slug: transformed.information.slug || '', imageUrl: transformed.information.banner, @@ -378,7 +378,7 @@ export default function DraftPreviewPage({ params }: PreviewPageProps) { {/* Banner */} -
{children}
- +
+ {' '} + {/* Changed from inline-flex to w-full */} +
{children}
{' '} + {/* Added w-full */} { setLoadingState(true); }, onSuccess: async () => { - setIsLoading(false); - setLoadingState(false); - - // Wait a bit for cookies to be set by Better Auth - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise(resolve => setTimeout(resolve, 200)); const session = await authClient.getSession(); @@ -204,8 +200,8 @@ const LoginWrapper = ({ setLoadingState }: LoginWrapperProps) => { } } - // Use full page reload to ensure cookies are available in middleware - // Use callbackUrl if provided, otherwise redirect to home + // Keep loading state active during redirect + // The page will unmount when redirecting, so no need to set false window.location.href = callbackUrl; }, onError: ctx => { diff --git a/components/hackathons/hackathonBanner.tsx b/components/hackathons/hackathonBanner.tsx index 32d05071b..64856c7ad 100644 --- a/components/hackathons/hackathonBanner.tsx +++ b/components/hackathons/hackathonBanner.tsx @@ -4,12 +4,12 @@ import { Card } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { formatDate } from '@/lib/utils'; import { useEffect, useState } from 'react'; -import { sanitizeHtml } from '@/lib/utils/renderHtml'; +// import { sanitizeHtml } from '@/lib/utils/renderHtml'; interface HackathonBannerProps { title: string; - subtitle?: string | React.ReactNode; imageUrl?: string; + tagline?: string; deadline?: string; startDate?: string; endDate?: string; @@ -61,7 +61,7 @@ function formatCountdown(time: TimeRemaining): string { export function HackathonBanner({ title, - subtitle, + tagline, imageUrl, deadline, startDate, @@ -245,18 +245,17 @@ export function HackathonBanner({

{title}

- {subtitle && typeof subtitle === 'string' ? ( -
+ {tagline && typeof tagline === 'string' ? ( + + {tagline} + ) : (
- {subtitle} + {tagline}
)} - {subtitle && ( + {tagline && (
)}
diff --git a/components/hackathons/overview/hackathonOverview.tsx b/components/hackathons/overview/hackathonOverview.tsx index c43088c6e..3e19422bb 100644 --- a/components/hackathons/overview/hackathonOverview.tsx +++ b/components/hackathons/overview/hackathonOverview.tsx @@ -1,5 +1,6 @@ 'use client'; import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import { HackathonTimeline } from './hackathonTimeline'; import { HackathonPrizes } from './hackathonPrizes'; @@ -33,19 +34,8 @@ export function HackathonOverview({
( -

- ), - h2: ({ ...props }) => ( -

- ), - h3: ({ ...props }) => ( -

- ), - p: ({ ...props }) => ( -

- ), ul: ({ ...props }) => (

    ), - li: ({ ...props }) =>
  • , - blockquote: ({ ...props }) => ( -
    ( + ), - code: ({ ...props }) => ( - ( + ), - pre: ({ ...props }) => ( -
    +            tbody: ({ ...props }) => (
    +              
    ), - table: ({ ...props }) => ( -
    + tr: ({ ...props }) => ( + ), th: ({ ...props }) => (
    ), td: ({ ...props }) => ( ), diff --git a/components/hackathons/overview/hackathonPrizes.tsx b/components/hackathons/overview/hackathonPrizes.tsx index b56670829..cf6d50439 100644 --- a/components/hackathons/overview/hackathonPrizes.tsx +++ b/components/hackathons/overview/hackathonPrizes.tsx @@ -1,13 +1,5 @@ 'use client'; -// interface Prize { -// title: string -// rank: string -// prize: string -// details: string[] -// icon?: string -// } - interface HackathonPrizesProps { title?: string; totalPrizes?: string; diff --git a/components/landing-page/Explore.tsx b/components/landing-page/Explore.tsx index d6c85d0fc..8a6d00e3d 100644 --- a/components/landing-page/Explore.tsx +++ b/components/landing-page/Explore.tsx @@ -117,6 +117,7 @@ const tabs = [ { name: 'Featured Projects', value: 'featured-projects' }, { name: 'Ongoing Hackathons', value: 'ongoing-hackathons' }, { name: 'Open Grants', value: 'open-grants' }, + { name: 'Bounties', value: 'live-grants' }, ]; export default function Explore() { diff --git a/components/landing-page/Hero2.tsx b/components/landing-page/Hero2.tsx index c3d9cc530..60c2c0c36 100644 --- a/components/landing-page/Hero2.tsx +++ b/components/landing-page/Hero2.tsx @@ -118,6 +118,7 @@ export default function Hero2() {
    {hackathonTitle} -

    - {hackathonDescription} -

    +

    {tagline}

    diff --git a/components/organization/OrganizationAnalytics.tsx b/components/organization/OrganizationAnalytics.tsx index 8fd537e22..4c7444466 100644 --- a/components/organization/OrganizationAnalytics.tsx +++ b/components/organization/OrganizationAnalytics.tsx @@ -123,7 +123,7 @@ const OrganizationAnalytics = () => { return (
    -
    +
    {/* Header */}

    Analytics

    diff --git a/components/organization/hackathons/new/NewHackathonTab.tsx b/components/organization/hackathons/new/NewHackathonTab.tsx index da473f693..0e9d21b2d 100644 --- a/components/organization/hackathons/new/NewHackathonTab.tsx +++ b/components/organization/hackathons/new/NewHackathonTab.tsx @@ -183,7 +183,7 @@ export default function NewHackathonTab({ return (
    - + {/* Tagline */} + ( + + + Tagline * + + + + + + + )} + /> {/* Description */} = ({ + activities = [], +}) => { + const { activityData, stats } = useMemo(() => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const oneYearAgo = new Date(today); + oneYearAgo.setFullYear(today.getFullYear() - 1); + + const activityMap = new Map(); + + activities.forEach(activity => { + if (!activity.timestamp) return; + + const date = new Date(activity.timestamp); + date.setHours(0, 0, 0, 0); + const dateKey = date.toISOString().split('T')[0]; + activityMap.set(dateKey, (activityMap.get(dateKey) || 0) + 1); + }); + + const data: Array<{ date: Date; count: number }> = []; + const currentDate = new Date(oneYearAgo); + + while (currentDate <= today) { + const dateKey = currentDate.toISOString().split('T')[0]; + data.push({ + date: new Date(currentDate), + count: activityMap.get(dateKey) || 0, + }); + currentDate.setDate(currentDate.getDate() + 1); + } + + const totalCount = data.reduce((sum, day) => sum + day.count, 0); + + const sevenDaysAgo = new Date(today); + sevenDaysAgo.setDate(today.getDate() - 7); + const currentWeekCount = data + .filter(day => day.date >= sevenDaysAgo) + .reduce((sum, day) => sum + day.count, 0); + + const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); + const currentMonthCount = data + .filter(day => day.date >= firstDayOfMonth) + .reduce((sum, day) => sum + day.count, 0); + + const todayCount = data + .filter(day => day.date.toDateString() === today.toDateString()) + .reduce((sum, day) => sum + day.count, 0); + + const avgPerDay = + data.length > 0 ? (totalCount / data.length).toFixed(1) : '0.0'; + + const mostActiveDay = data.reduce( + (max, day) => (day.count > max.count ? day : max), + { date: new Date(), count: 0 } + ); + + let currentStreak = 0; + let longestStreak = 0; + let tempStreak = 0; + + for (let i = data.length - 1; i >= 0; i--) { + if (data[i].count > 0) { + currentStreak++; + } else if (data[i].date < today) { + break; + } + } + + data.forEach(day => { + if (day.count > 0) { + tempStreak++; + longestStreak = Math.max(longestStreak, tempStreak); + } else { + tempStreak = 0; + } + }); + + return { + activityData: data, + stats: { + total: totalCount, + currentWeek: currentWeekCount, + currentMonth: currentMonthCount, + today: todayCount, + avgPerDay, + mostActiveDay, + currentStreak, + longestStreak, + }, + }; + }, [activities]); + + const getIntensityClass = (count: number) => { + if (count === 0) return 'bg-zinc-900'; + if (count <= 2) return 'bg-emerald-900/40'; + if (count <= 4) return 'bg-emerald-700/60'; + if (count <= 6) return 'bg-emerald-600/80'; + return 'bg-emerald-500'; + }; + + const weeks = useMemo(() => { + const weeksArray: ((typeof activityData)[0] | null)[][] = []; + let currentWeek: ((typeof activityData)[0] | null)[] = []; + + const firstDate = activityData[0].date; + const firstDay = firstDate.getDay(); + + for (let i = 0; i < firstDay; i++) { + currentWeek.push(null); + } + + activityData.forEach(day => { + currentWeek.push(day); + + if (currentWeek.length === 7) { + weeksArray.push([...currentWeek]); + currentWeek = []; + } + }); + + if (currentWeek.length > 0) { + while (currentWeek.length < 7) { + currentWeek.push(null); + } + weeksArray.push(currentWeek); + } + + return weeksArray; + }, [activityData]); + + const monthLabels = useMemo(() => { + const months: Array<{ label: string; weekIndex: number }> = []; + let currentMonth: number | null = null; + + weeks.forEach((week, weekIndex) => { + const firstDay = week.find(day => day !== null); + if (firstDay) { + const month = firstDay.date.getMonth(); + if (month !== currentMonth) { + months.push({ + label: firstDay.date.toLocaleDateString('en-US', { + month: 'short', + }), + weekIndex, + }); + currentMonth = month; + } + } + }); + + return months; + }, [weeks]); + + return ( +
    +
    +

    + {stats.total} contributions in the last year +

    +

    + Track your hackathon journey - participations, submissions, and + community engagement +

    + +
    +
    +
    + {monthLabels.map((month, index) => ( +
    + {month.label} +
    + ))} +
    + +
    +
    +
    +
    Mon
    +
    +
    Wed
    +
    +
    Fri
    +
    +
    + +
    + {weeks.map((week, weekIndex) => ( +
    + {week.map((day, dayIndex) => ( +
    + ))} +
    + ))} +
    +
    + +
    +
    Inspired by GitHub
    +
    + Less +
    +
    +
    +
    +
    +
    +
    + More +
    +
    +
    +
    +
    +
    + ); +}; + +export default ActivityHeatmap; diff --git a/components/profile/ProfileDataClient.tsx b/components/profile/ProfileDataClient.tsx index b5e0a7fb4..71659f79d 100644 --- a/components/profile/ProfileDataClient.tsx +++ b/components/profile/ProfileDataClient.tsx @@ -17,6 +17,8 @@ import { DropdownMenuTrigger, } from '../ui/dropdown-menu'; import { Button } from '../ui/button'; +import ActivityHeatmap from './ActivityHeatMap'; +import { FutureFeature } from '../FeatureFuture'; interface ProfileDataClientProps { user: GetMeResponse; @@ -55,13 +57,13 @@ export default function ProfileDataClient({ Activity Projects @@ -109,6 +111,9 @@ export default function ProfileDataClient({ + + + diff --git a/components/profile/ProfileHeader.tsx b/components/profile/ProfileHeader.tsx index 4cabe498d..0ac4e77b3 100644 --- a/components/profile/ProfileHeader.tsx +++ b/components/profile/ProfileHeader.tsx @@ -7,10 +7,12 @@ 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 } from 'lucide-react'; +import { BellPlus, Settings, View } 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'; interface ProfileHeaderProps { profile: UserProfile; @@ -25,6 +27,9 @@ export default function ProfileHeader({ }: ProfileHeaderProps) { const [followersModalOpen, setFollowersModalOpen] = useState(false); const [followingModalOpen, setFollowingModalOpen] = useState(false); + const pathname = usePathname(); + + // const isOwnProfile = pathname === '/me'; // Convert API user data to TeamMember format const convertToTeamMembers = ( @@ -111,9 +116,19 @@ export default function ProfileHeader({ Edit Profile - } iconPosition='right'> - Follow - + {pathname ? ( + + } iconPosition='right'> + Preview + + + ) : ( + + } iconPosition='right'> + Follow + + + )} Share Share icon diff --git a/components/user/UserMenu.tsx b/components/user/UserMenu.tsx index 202d15790..d02127aa4 100644 --- a/components/user/UserMenu.tsx +++ b/components/user/UserMenu.tsx @@ -60,6 +60,7 @@ export const UserMenu: React.FC = ({ {(user?.name || user?.profile?.firstName || 'U') @@ -82,6 +83,7 @@ export const UserMenu: React.FC = ({
    @@ -119,7 +121,7 @@ export const UserMenu: React.FC = ({ className='flex items-center gap-3 hover:!text-white' > - Profile + Your Profile diff --git a/hooks/hackathon/use-hackathon-transform.ts b/hooks/hackathon/use-hackathon-transform.ts index 2ddbe4382..c3821da70 100644 --- a/hooks/hackathon/use-hackathon-transform.ts +++ b/hooks/hackathon/use-hackathon-transform.ts @@ -19,6 +19,7 @@ interface TransformedHackathon { organizerLogo: string; hackathonImage: string; hackathonTitle: string; + tagline: string; hackathonDescription: string; status: 'Published' | 'Ongoing' | 'Completed' | 'Cancelled'; deadlineInDays: number; @@ -153,6 +154,7 @@ export function useHackathonTransform() { organizationName: orgName, hackathonSlug: hackathon.information.slug, organizerName: orgName, + tagline: hackathon.information.tagline, organizerLogo: '/avatar.png', // This should come from organization data hackathonImage: hackathon.information?.banner || diff --git a/hooks/project/use-project-transform.ts b/hooks/project/use-project-transform.ts index 5d92f0190..aa9ba9fc6 100644 --- a/hooks/project/use-project-transform.ts +++ b/hooks/project/use-project-transform.ts @@ -63,7 +63,8 @@ export function useProjectTransform() { return { projectId: project._id, - creatorName: `${project.creator.profile.firstName} ${project.creator.profile.lastName}`, + creatorName: + `${project.creator?.profile?.firstName ?? ''} ${project.creator?.profile?.lastName ?? ''}`.trim(), creatorLogo: '/avatar.png', projectImage: project.media?.logo || diff --git a/lib/api/hackathons.ts b/lib/api/hackathons.ts index 011fa82be..f6772bde5 100644 --- a/lib/api/hackathons.ts +++ b/lib/api/hackathons.ts @@ -41,6 +41,7 @@ export interface HackathonVenue { export interface HackathonInformation { title: string; banner: string; + tagline: string; description: string; categories: HackathonCategory[]; // New format (array of categories) slug: string; @@ -650,6 +651,7 @@ export interface PublicHackathon { slug: string; title: string; subtitle: string; + tagline: string; description: string; imageUrl: string; status: 'upcoming' | 'ongoing' | 'ended'; @@ -715,6 +717,7 @@ interface FlatHackathonData { escrowDetails?: object; // Flat fields that map to nested structure banner?: string; + tagline?: string; description?: string; categories?: HackathonCategory[]; // New format venue?: HackathonVenue; @@ -788,6 +791,7 @@ const transformHackathonResponse = ( title: flat.title || '', banner: flat.banner || '', description: flat.description || '', + tagline: flat.tagline || '', slug: flat.slug || '', // Support both new format (categories) and legacy format (category) categories: Array.isArray(flat.categories) @@ -1440,6 +1444,7 @@ export const transformPublicHackathonToHackathon = ( description: publicHackathon.description, categories: categories, slug: publicHackathon.slug, + tagline: publicHackathon.tagline, venue, }, timeline: { diff --git a/lib/providers/hackathonProvider.tsx b/lib/providers/hackathonProvider.tsx index 901ad0b48..4158842ce 100644 --- a/lib/providers/hackathonProvider.tsx +++ b/lib/providers/hackathonProvider.tsx @@ -60,7 +60,7 @@ interface HackathonDataContextType { discussions: Discussion[]; participants: Participant[]; submissions: SubmissionCardProps[]; - content: string; + // content: string; timelineEvents: TimelineEvent[]; prizes: Prize[]; resources: ResourceItem[]; @@ -344,14 +344,14 @@ export function HackathonDataProvider({ }, ]; - const mockContent = `# ${ - currentHackathon?.title || 'Hackathon' - }\n\n## 🌐 Hackathon Theme\n${ - currentHackathon?.subtitle || 'Build innovative solutions' - }\n\n## Challenge Description\n${ - currentHackathon?.description || - 'Create an innovative project that solves real-world problems.' - }`; + // const mockContent = `# ${ + // currentHackathon?.title || 'Hackathon' + // }\n\n## 🌐 Hackathon Theme\n${ + // currentHackathon?.tagline || 'Build innovative solutions' + // }\n\n## Challenge Description\n${ + // currentHackathon?.description || + // 'Create an innovative project that solves real-world problems.' + // }`; const mockTimelineEvents: TimelineEvent[] = currentHackathon ? [ @@ -458,7 +458,7 @@ export function HackathonDataProvider({ discussions: mockDiscussions, participants, submissions, - content: mockContent, + // content: mockContent, timelineEvents: mockTimelineEvents, prizes: mockPrizes, resources: mockResources, diff --git a/lib/utils/hackathon-form-transforms.ts b/lib/utils/hackathon-form-transforms.ts index 5a50a8a2d..9575b002f 100644 --- a/lib/utils/hackathon-form-transforms.ts +++ b/lib/utils/hackathon-form-transforms.ts @@ -40,6 +40,7 @@ export const transformToApiFormat = (stepData: { title: info?.name || '', banner: info?.banner || '', description: info?.description || '', + tagline: info?.tagline || '', slug: info?.slug || '', // Send categories array (new format, recommended) categories: @@ -147,6 +148,7 @@ export const transformFromApiFormat = (draft: HackathonDraft) => { name: info?.title || '', banner: info?.banner || '', description: info?.description || '', + tagline: info.tagline || '', categories: categoriesArray, venueType: info?.venue?.type || 'physical', country: info?.venue?.country || '', diff --git a/package-lock.json b/package-lock.json index 8d2b8bdf3..a2467859a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,6 +104,7 @@ "react-notification-core": "^1.0.5", "react-resizable-panels": "^3.0.3", "recharts": "^2.15.4", + "remark-gfm": "^4.0.1", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "three": "^0.180.0", diff --git a/package.json b/package.json index 2c87ea8ac..232c5c580 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "react-notification-core": "^1.0.5", "react-resizable-panels": "^3.0.3", "recharts": "^2.15.4", + "remark-gfm": "^4.0.1", "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "three": "^0.180.0", diff --git a/types/hackathon.ts b/types/hackathon.ts index f739ed43f..022fb749f 100644 --- a/types/hackathon.ts +++ b/types/hackathon.ts @@ -84,7 +84,7 @@ export interface SubmissionCardProps { export interface Hackathon { id: string; title: string; - subtitle: string; + tagline: string; description: string; slug?: string; imageUrl: string;