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;