diff --git a/app/(landing)/bounties/page.tsx b/app/(landing)/bounties/page.tsx new file mode 100644 index 000000000..c303997a7 --- /dev/null +++ b/app/(landing)/bounties/page.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('grants'); + +const GrantPage = () => { + return ( +
+ Bounties Page +
+ ); +}; + +export default GrantPage; diff --git a/components/FeatureFuture.tsx b/components/FeatureFuture.tsx new file mode 100644 index 000000000..9211d75a1 --- /dev/null +++ b/components/FeatureFuture.tsx @@ -0,0 +1,37 @@ +'use client'; + +import React from 'react'; +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; + +interface FutureFeatureProps { + children: React.ReactNode; + label?: string; + className?: string; + disabledClass?: string; + badgeClassName?: string; +} + +export function FutureFeature({ + children, + label = 'Coming Soon', + className, + disabledClass = 'pointer-events-none opacity-50', + badgeClassName, +}: FutureFeatureProps) { + return ( +
+
{children}
+ + + {label} + +
+ ); +} diff --git a/components/Project-Page-Hero.tsx b/components/Project-Page-Hero.tsx index 320dca43c..b7221ad2b 100644 --- a/components/Project-Page-Hero.tsx +++ b/components/Project-Page-Hero.tsx @@ -1,80 +1,57 @@ 'use client'; import React from 'react'; -import Image from 'next/image'; import { ArrowDown } from 'lucide-react'; import { BoundlessButton } from './buttons/BoundlessButton'; export default function ProjectPageHero() { const scrollToProjects = () => { - const projectsSection = document.getElementById('explore-project'); - if (projectsSection) { - const targetPosition = projectsSection.offsetTop - 100; - const startPosition = window.pageYOffset; - const distance = targetPosition - startPosition; - const duration = 1000; - let start: number | null = null; + const section = document.getElementById('explore-project'); + if (!section) return; - const animation = (currentTime: number) => { - if (start === null) start = currentTime; - const timeElapsed = currentTime - start; - const run = easeInOutQuad( - timeElapsed, - startPosition, - distance, - duration - ); - window.scrollTo(0, run); - if (timeElapsed < duration) requestAnimationFrame(animation); - }; + const targetPosition = section.offsetTop - 100; + const startPosition = window.pageYOffset; + const distance = targetPosition - startPosition; + const duration = 1000; + let start: number | null = null; - const easeInOutQuad = (t: number, b: number, c: number, d: number) => { - t /= d / 2; - if (t < 1) return (c / 2) * t * t + b; - t--; - return (-c / 2) * (t * (t - 2) - 1) + b; - }; + const easeInOutQuad = (t: number, b: number, c: number, d: number) => { + t /= d / 2; + if (t < 1) return (c / 2) * t * t + b; + t--; + return (-c / 2) * (t * (t - 2) - 1) + b; + }; - requestAnimationFrame(animation); - } + const animation = (currentTime: number) => { + if (start === null) start = currentTime; + const timeElapsed = currentTime - start; + const run = easeInOutQuad(timeElapsed, startPosition, distance, duration); + window.scrollTo(0, run); + if (timeElapsed < duration) requestAnimationFrame(animation); + }; + + requestAnimationFrame(animation); }; return ( -
-
-
-
-
-
- +
-
+
+ {/* Left Text */}
-

+

Discover projects - -
- that are shaping -
- the future on Stellar + {' '} + that are shaping the future on Stellar

Validated by the community. Backed milestone by milestone.

+
+ {/* Buttons */} +
- -
-
-
-
- Project cards showcase -
-
-
-
-
-
- -
-
-
- Project cards showcase -
-
-
-
diff --git a/components/organization/OrganizationContent.tsx b/components/organization/OrganizationContent.tsx index e68ed4faa..593561827 100644 --- a/components/organization/OrganizationContent.tsx +++ b/components/organization/OrganizationContent.tsx @@ -2,18 +2,125 @@ import { Search, ArrowUpDown, Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import OrganizationCard from './cards/OrganzationCards'; import Link from 'next/link'; import { BoundlessButton } from '../buttons'; import { useOrganization } from '@/lib/providers/OrganizationProvider'; import LoadingSpinner from '../LoadingSpinner'; +import { useState, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; + +type SortOption = 'newest' | 'oldest' | 'name-asc' | 'name-desc'; export default function OrganizationContent() { - const { organizations, isLoading, isLoadingOrganizations } = - useOrganization(); + const router = useRouter(); + const { + organizations, + isLoading, + isLoadingOrganizations, + deleteOrganization, + } = useOrganization(); + const [searchQuery, setSearchQuery] = useState(''); + const [sortBy, setSortBy] = useState('newest'); + const [deletingId, setDeletingId] = useState(null); + const loading = isLoading || isLoadingOrganizations; + + // Filter and sort organizations + const filteredAndSortedOrganizations = useMemo(() => { + let filtered = organizations; + + // Apply search filter + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + filtered = organizations.filter(org => + org.name.toLowerCase().includes(query) + ); + } + + // Apply sorting + const sorted = [...filtered].sort((a, b) => { + switch (sortBy) { + case 'newest': + return ( + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + case 'oldest': + return ( + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + case 'name-asc': + return a.name.localeCompare(b.name); + case 'name-desc': + return b.name.localeCompare(a.name); + default: + return 0; + } + }); + + return sorted; + }, [organizations, searchQuery, sortBy]); + const hasOrganizations = organizations.length > 0; + const handleDelete = async (orgId: string) => { + if (!confirm('Are you sure you want to delete this organization?')) { + return; + } + + try { + setDeletingId(orgId); + await deleteOrganization(orgId); + } catch (error) { + console.error('Failed to delete organization:', error); + alert('Failed to delete organization. Please try again.'); + } finally { + setDeletingId(null); + } + }; + + const handleEdit = (orgId: string) => { + router.push(`/organizations/${orgId}/edit`); + }; + + const handleArchive = async (orgId: string) => { + if (!confirm('Are you sure you want to archive this organization?')) { + return; + } + + try { + setDeletingId(orgId); + console.log('Archive organization:', orgId, deletingId); + alert('Archive functionality will be implemented soon.'); + } catch (error) { + console.error('Failed to archive organization:', error); + alert('Failed to archive organization. Please try again.'); + } finally { + setDeletingId(null); + } + }; + + const getSortLabel = () => { + switch (sortBy) { + case 'newest': + return 'Newest First'; + case 'oldest': + return 'Oldest First'; + case 'name-asc': + return 'Name (A-Z)'; + case 'name-desc': + return 'Name (Z-A)'; + default: + return 'Sort'; + } + }; + if (loading) { return (
@@ -35,16 +142,53 @@ export default function OrganizationContent() { setSearchQuery(e.target.value)} className='focus-visible:border-primary focus-visible:ring-primary w-full rounded-lg border-zinc-800 bg-zinc-900 py-6 pr-4 pl-12 text-white placeholder:text-zinc-500 focus-visible:ring-[1px]' />
- + + + + + + + setSortBy('newest')} + className={`cursor-pointer ${sortBy === 'newest' ? 'text-primary bg-zinc-800' : 'text-zinc-300'}`} + > + Newest First + + setSortBy('oldest')} + className={`cursor-pointer ${sortBy === 'oldest' ? 'text-primary bg-zinc-800' : 'text-zinc-300'}`} + > + Oldest First + + setSortBy('name-asc')} + className={`cursor-pointer ${sortBy === 'name-asc' ? 'text-primary bg-zinc-800' : 'text-zinc-300'}`} + > + Name (A-Z) + + setSortBy('name-desc')} + className={`cursor-pointer ${sortBy === 'name-desc' ? 'text-primary bg-zinc-800' : 'text-zinc-300'}`} + > + Name (Z-A) + + + + {hasOrganizations && (
@@ -65,26 +209,47 @@ export default function OrganizationContent() {
{hasOrganizations && ( -
- {organizations.map(org => ( - - - - ))} -
+ <> + {filteredAndSortedOrganizations.length > 0 ? ( +
+ {filteredAndSortedOrganizations.map(org => ( + + ))} +
+ ) : ( +
+
+

+ No organizations found matching "{searchQuery}" +

+ +
+
+ )} + )} {!hasOrganizations && ( diff --git a/components/organization/OrganizationHeader.tsx b/components/organization/OrganizationHeader.tsx index 4e0701de3..42114dcc8 100644 --- a/components/organization/OrganizationHeader.tsx +++ b/components/organization/OrganizationHeader.tsx @@ -34,12 +34,14 @@ export default function OrganizationHeader() {
- Boundless Logo + + Boundless Logo +
diff --git a/components/organization/OrganizationSidebar.tsx b/components/organization/OrganizationSidebar.tsx index 2e5515495..abb1685cb 100644 --- a/components/organization/OrganizationSidebar.tsx +++ b/components/organization/OrganizationSidebar.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { cn } from '@/lib/utils'; import { useWindowSize } from '@/hooks/use-window-size'; +import { FutureFeature } from '../FeatureFuture'; interface OrganizationSidebarProps { organizationId?: string; @@ -42,6 +43,7 @@ export default function OrganizationSidebar({ icon: HandCoins, label: 'Grants', href: derivedOrgId ? `/organizations/${derivedOrgId}/grants` : '#', + disabled: true, }, { icon: Settings, @@ -71,8 +73,18 @@ export default function OrganizationSidebar({ isValidHref && (normalizedPath === item.href || normalizedPath?.startsWith(item.href + '/')); - - return ( + return item.disabled ? ( + +
+ + {item.label} +
+
+ ) : ( Host Hackathon - +
Create Grants
- +
diff --git a/components/organization/cards/OrganzationCards.tsx b/components/organization/cards/OrganzationCards.tsx index 79dfc9b6b..f8691017b 100644 --- a/components/organization/cards/OrganzationCards.tsx +++ b/components/organization/cards/OrganzationCards.tsx @@ -1,6 +1,13 @@ 'use client'; -import { ArrowRight, HandCoins, Trophy } from 'lucide-react'; +import { + MoreVertical, + HandCoins, + Trophy, + Edit, + Archive, + Trash2, +} from 'lucide-react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { @@ -9,6 +16,13 @@ import { TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import Link from 'next/link'; interface OrganizationCardProps { id: string; @@ -23,6 +37,9 @@ interface OrganizationCardProps { count: number; applications: number; }; + onEdit?: (id: string) => void; + onArchive?: (id: string) => void; + onDelete?: (id: string) => void; } export default function OrganizationCard({ @@ -32,75 +49,147 @@ export default function OrganizationCard({ createdAt, hackathons, grants, + onEdit, + onArchive, + onDelete, }: OrganizationCardProps) { const router = useRouter(); + + const handleEdit = (e: React.MouseEvent) => { + e.stopPropagation(); + if (onEdit) { + onEdit(id); + } else { + router.push(`/organizations/${id}/edit`); + } + }; + + const handleArchive = (e: React.MouseEvent) => { + e.stopPropagation(); + if (onArchive) { + onArchive(id); + } else { + if (confirm('Are you sure you want to archive this organization?')) { + console.log('Archive organization:', id); + // TODO: Implement archive functionality + } + } + }; + + const handleDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + if (onDelete) { + onDelete(id); + } else { + if (confirm('Are you sure you want to delete this organization?')) { + console.log('Delete organization:', id); + // TODO: Implement delete functionality + } + } + }; + return ( -
router.push(`/organizations/${id}/settings`)} - className='hover:shadow-primary/10 cursor-pointer rounded-lg border border-zinc-800 bg-black transition-shadow duration-300 hover:shadow-lg' - > -
-
- {`Org -
-

- {name} -

-

- {new Date(createdAt).toLocaleDateString()} -

+ +
+
+
+ {`Org +
+

+ {name} +

+

+ {new Date(createdAt).toLocaleDateString()} +

+
-
-
- - -
-
- +
+ + +
+
+ +
+ + {hackathons.count} +
- - {hackathons.count} - -
- - -

- {hackathons.count} hackathons ({hackathons.submissions}{' '} - submissions) -

-
- + + +

+ {hackathons.count} hackathons ({hackathons.submissions}{' '} + submissions) +

+
+ - - -
-
- + + +
+
+ +
+ + {grants.count} +
- - {grants.count} - -
- - -

- {grants.count} grants ({grants.applications} applications) -

-
- + + +

+ {grants.count} grants ({grants.applications} applications) +

+
+ - + + + + + + + + Edit + + + + Archive + + + + Delete + + + +
-
-
+
+
); } diff --git a/hooks/use-hackathon-publish.ts b/hooks/use-hackathon-publish.ts index a70215618..b5dc58f5e 100644 --- a/hooks/use-hackathon-publish.ts +++ b/hooks/use-hackathon-publish.ts @@ -102,9 +102,8 @@ export const useHackathonPublish = ({ try { toast.info('Creating escrow contract...'); const escrowPayload = createHackathonEscrow({ - signer: 'GA2XHHR6RMQXPYQGUGXMOAGC5N5RFPM4HPUPOZJCHOO6LINM7DJIQFFJ', - organizationAddress: - 'GA2XHHR6RMQXPYQGUGXMOAGC5N5RFPM4HPUPOZJCHOO6LINM7DJIQFFJ', + signer: walletAddress, + organizationAddress: walletAddress, hackathonTitle: stepData.information.name || 'Hackathon', hackathonDescription: stepData.information.description || '', rewards: stepData.rewards, @@ -156,7 +155,7 @@ export const useHackathonPublish = ({ toast.info('Funding escrow with prize pool...'); const fundPayload: FundEscrowPayload = { contractId: contractId, - signer: 'GA2XHHR6RMQXPYQGUGXMOAGC5N5RFPM4HPUPOZJCHOO6LINM7DJIQFFJ', + signer: walletAddress, amount: totalPrizeAmount, }; @@ -179,7 +178,7 @@ export const useHackathonPublish = ({ toast.info('Please sign the funding transaction...'); const signedFundXdr = await signTransaction({ unsignedTransaction: fundResponse.unsignedTransaction, - address: 'GA2XHHR6RMQXPYQGUGXMOAGC5N5RFPM4HPUPOZJCHOO6LINM7DJIQFFJ', + address: walletAddress, }); const fundSendResponse = await sendTransaction(signedFundXdr);