diff --git a/src/config/routes/routes.tsx b/src/config/routes/routes.tsx index 84f54ce62..623274ac5 100644 --- a/src/config/routes/routes.tsx +++ b/src/config/routes/routes.tsx @@ -8,6 +8,7 @@ import { NotAuthorized, NotFoundPage } from '../../pages/fallback' import { PrivacyPolicy, TermsAndConditions } from '../../pages/legal' import { ErrorBoundary } from './ErrorBoundary' import { renderPrivateRoute } from './PrivateRoute' +import LatestNostrProjects from '../../pages/landing/projects/views/LatestNostrProjects' const Grants = () => import('../../pages/grants') const ProjectLaunch = () => import('../../pages/projectCreate') @@ -400,6 +401,10 @@ export const platformRoutes: RouteObject[] = [ return { Component: LandingFeed } }, }, + { + path:'latest', + Component: LatestNostrProjects, + }, ], }, { diff --git a/src/pages/landing/filters/status/StatusFilterBody.tsx b/src/pages/landing/filters/status/StatusFilterBody.tsx index c0cba39cd..9466fabb2 100644 --- a/src/pages/landing/filters/status/StatusFilterBody.tsx +++ b/src/pages/landing/filters/status/StatusFilterBody.tsx @@ -1,5 +1,7 @@ -import { Button, StackProps, VStack } from '@chakra-ui/react' +import { Button, StackProps, VStack, Box } from '@chakra-ui/react' import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import { AiOutlineFieldTime } from 'react-icons/ai' import { Body1 } from '../../../../components/typography' import { useFilterContext } from '../../../../context' @@ -38,7 +40,13 @@ export const StatusFilterBody = ({ { type: ProjectType.Reward }, { status: ProjectStatus.Active }, { status: ProjectStatus.Inactive }, - ] + { + linkTo: '/latest', + text: 'Latest Projects', + icon: AiOutlineFieldTime, + size: '1.5em', + }, + ]; return ( {options.map((option, index) => { + if (option.linkTo) { + return ( + + + + + + ); + } const isActive = - filters.type === option.type && filters.status === option.status + filters.type === option.type && filters.status === option.status; - const { icon: Icon, text, color } = getStatusTypeButtonContent(option) + const { icon: Icon, text } = getStatusTypeButtonContent(option); return ( - ) + ); })} - ) -} + ); +}; diff --git a/src/pages/landing/projects/components/NostrProjectCard.tsx b/src/pages/landing/projects/components/NostrProjectCard.tsx new file mode 100644 index 000000000..eff0b4760 --- /dev/null +++ b/src/pages/landing/projects/components/NostrProjectCard.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { Text, Box, Flex, Link as ChakraLink, useBreakpointValue } from '@chakra-ui/react'; +import { ImageWithReload } from '../../../../components/ui/ImageWithReload'; + +interface RawContent { + display_name: string; + image: string; + about: string; + website: string; +} + +interface NostrichType { + pubkey: string; +} + +interface NostrProjectCardProps { + content: string; + nostrich: NostrichType; +} + +const NostrProjectCard: React.FC = ({ content, nostrich }) => { + let parsedContent: RawContent; + try { + parsedContent = JSON.parse(content); + } catch (error) { + console.error('Error parsing JSON:', error); + parsedContent = { display_name: '', image: '', about: '', website: '' }; + } + const { display_name, image, about, website } = parsedContent; + + const imageSize = useBreakpointValue({ base: '120px', md: '120px' }); + + return ( + + + + + + + + + {display_name} + + + {about} + + + + + ); +}; + +export default NostrProjectCard; diff --git a/src/pages/landing/projects/components/NostrProjectList.tsx b/src/pages/landing/projects/components/NostrProjectList.tsx new file mode 100644 index 000000000..cd37b0d78 --- /dev/null +++ b/src/pages/landing/projects/components/NostrProjectList.tsx @@ -0,0 +1,48 @@ +import { useState } from 'react'; +import { Event } from 'nostr-tools'; +import { Button, Flex } from '@chakra-ui/react'; +import NostrProjectCard from './NostrProjectCard'; + +interface Props { + projects: Event[]; +} + +export default function NostrProjectList({ projects }: Props) { + const initialDisplayCount = 12; + const itemsPerPage = 10; + const [displayCount, setDisplayCount] = useState(initialDisplayCount); + + const loadMore = () => { + const nextDisplayCount = displayCount + itemsPerPage; + setDisplayCount(nextDisplayCount); + }; + + const initialProjects = projects.slice(0, initialDisplayCount); + + const renderProjects = + displayCount > initialDisplayCount ? projects.slice(0, displayCount) : initialProjects; + + const buttonStyle = { + border: '1px solid', + padding: '8px 16px', + borderColor: 'neutral.200', + borderRadius: '8px', + margin: '8px 0', + cursor: 'pointer', + }; + + return ( + + + {renderProjects.map((nostr, index) => ( + + ))} + {projects.length > displayCount && ( + + )} + + + ); +} diff --git a/src/pages/landing/projects/views/LatestNostrProjects.tsx b/src/pages/landing/projects/views/LatestNostrProjects.tsx new file mode 100644 index 000000000..55a9d9437 --- /dev/null +++ b/src/pages/landing/projects/views/LatestNostrProjects.tsx @@ -0,0 +1,63 @@ +import { useEffect, useState } from 'react'; +import { Container, VStack, HStack, useBreakpointValue, useDisclosure } from '@chakra-ui/react'; +import { SimplePool, Event } from 'nostr-tools'; +import NostrProjectList from '../components/NostrProjectList'; +import { FilterDrawer } from '../../filters/mobile/FilterDrawer'; +import { MobileTopBar } from '../../filters/mobile/MobileTopBar'; + +const GEYSER_RELAY_URL = [ + 'wss://relay.geyser.fund', +]; + +export const LatestNostrProjects = () => { + const [pool, setPool] = useState(new SimplePool()); + const initialEvents: Event[] = []; + const [events, setEvents] = useState(initialEvents); + + useEffect(() => { + const _pool = new SimplePool(); + setPool(_pool); + + return () => { + _pool.close(GEYSER_RELAY_URL); + }; + }, []); + + useEffect(() => { + if (!pool) return; + + const sub = pool.sub(GEYSER_RELAY_URL, [ + { + kinds: [0], + limit: 100, + }, + ]); + + sub.on('event', (event: Event) => { + setEvents((events) => [...events, event]); + }); + + return () => { + sub.unsub(); + }; + }, [pool]); + + const isMobile = useBreakpointValue({ base: true, sm: true, md: true, lg: false }); + + const { isOpen, onClose } = useDisclosure(); + + return ( + + + {isMobile && } + {isMobile && } + + + + + + + ); +}; + +export default LatestNostrProjects;