Skip to content
6 changes: 6 additions & 0 deletions .changeset/clean-spies-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"create-hypergraph": patch
---

improve projects listing in both templates

14 changes: 14 additions & 0 deletions apps/create-hypergraph/template-nextjs/Components/GraphImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
function getImageUrl(src: string | undefined | Blob) {
if (!src || typeof src !== 'string') return src;
const image = src.split('ipfs://');
if (image.length === 2) {
return `https://gateway.lighthouse.storage/ipfs/${image[1]}`;
}
return src;
}

export function GraphImage(
props: React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
) {
return <img {...props} src={getImageUrl(props.src)} />;
}
Original file line number Diff line number Diff line change
@@ -1,62 +1,127 @@
'use client';

import { useQuery } from '@graphprotocol/hypergraph-react';
import { useState } from 'react';
import { Project } from '../../app/schema';
import { GraphImage } from '../GraphImage';

export function PublicKnowledgeExplorer() {
const [searchTerm, setSearchTerm] = useState('');

const { data: projects, isPending } = useQuery(Project, {
mode: 'public',
space: 'b2565802-3118-47be-91f2-e59170735bac',
first: 40,
include: { avatar: {} },
filter: {
name: {
// contains is case sensitive
contains: searchTerm,
},
},
});

// empty state
if (isPending === false && projects.length === 0) {
return (
<div className="text-center py-16">
<div className="w-24 h-24 bg-gradient-to-br from-blue-100 to-purple-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg className="w-12 h-12 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
return (
<>
{/* Search UI */}
<div className="max-w-md mx-auto mb-8">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search projects..."
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200"
/>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">No Projects Found</h3>
<p className="text-gray-500">There are currently no public projects available to explore.</p>
</div>
);
}

return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{projects.map((project) => (
<div
key={project.id}
className="group relative bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden border border-gray-100 hover:border-blue-200 transform hover:-translate-y-1 z-10"
>
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 to-purple-50 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />

{/* Content */}
<div className="relative p-6">
{/* Project icon/avatar */}
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300">
<span className="text-white font-bold text-lg">{project.name.charAt(0).toUpperCase()}</span>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{projects.map((project) => (
<div
key={project.id}
className="group relative bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden border border-gray-100 hover:border-blue-200 transform hover:-translate-y-1 z-10"
>
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 to-purple-50 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />

{/* Content */}
<div className="relative p-6">
{/* Project icon/avatar */}
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300 overflow-hidden">
{project.avatar?.[0]?.url ? (
<GraphImage
src={project.avatar[0].url}
alt={`${project.name} avatar`}
className="w-full h-full object-cover"
width={200}
height={200}
/>
) : (
<span className="text-white font-bold text-lg">{project.name.charAt(0).toUpperCase()}</span>
)}
</div>

{/* Project name */}
<h3 className="text-xl font-bold text-gray-900 mb-2 group-hover:text-blue-600 transition-colors duration-300">
{project.name}
</h3>

{/* Project ID */}
<p className="text-[10px] text-gray-500 mb-2 font-mono">{project.id}</p>

{/* Project description */}
{project.description && <p className="text-sm text-gray-600 mb-2 line-clamp-2">{project.description}</p>}

{/* Project xUrl */}
{project.xUrl && (
<a
href={project.xUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 transition-colors duration-200 flex items-center gap-1"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
View on X
</a>
)}
</div>

{/* Project name */}
<h3 className="text-xl font-bold text-gray-900 mb-2 group-hover:text-blue-600 transition-colors duration-300">
{project.name}
</h3>
{/* Decorative corner accent */}
<div className="absolute top-0 right-0 w-16 h-16 bg-gradient-to-br from-blue-400 to-purple-500 opacity-10 group-hover:opacity-20 transition-opacity duration-300 transform rotate-45 translate-x-8 -translate-y-8" />
</div>
))}
</div>

{/* Decorative corner accent */}
<div className="absolute top-0 right-0 w-16 h-16 bg-gradient-to-br from-blue-400 to-purple-500 opacity-10 group-hover:opacity-20 transition-opacity duration-300 transform rotate-45 translate-x-8 -translate-y-8" />
{/* Empty state */}
{isPending === false && projects.length === 0 && (
<div className="text-center py-16">
<div className="w-24 h-24 bg-gradient-to-br from-blue-100 to-purple-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg className="w-12 h-12 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">No Projects Found</h3>
<p className="text-gray-500">There are currently no public projects available to explore.</p>
</div>
))}
</div>
)}
</>
);
}
11 changes: 11 additions & 0 deletions apps/create-hypergraph/template-nextjs/app/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@ export const mapping: Mapping.Mapping = {
description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'),
},
},
Image: {
typeIds: [Id('ba4e4146-0010-499d-a0a3-caaa7f579d0e')],
properties: {
url: Id('8a743832-c094-4a62-b665-0c3cc2f9c7bc'),
},
},
Project: {
typeIds: [Id('484a18c5-030a-499c-b0f2-ef588ff16d50')],
properties: {
name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'),
description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'),
xUrl: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'),
},
relations: {
avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'),
},
},
};
7 changes: 7 additions & 0 deletions apps/create-hypergraph/template-nextjs/app/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export class Address extends Entity.Class<Address>('Address')({
description: Type.String,
}) {}

export class Image extends Entity.Class<Image>('Image')({
url: Type.String,
}) {}

export class Project extends Entity.Class<Project>('Project')({
name: Type.String,
description: Type.optional(Type.String),
xUrl: Type.optional(Type.String),
avatar: Type.Relation(Image),
}) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
function getImageUrl(src: string | undefined) {
if (!src || typeof src !== 'string') return src;
const image = src.split('ipfs://');
if (image.length === 2) {
return `https://gateway.lighthouse.storage/ipfs/${image[1]}`;
}
return src;
}

export function GraphImage(
props: React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
) {
return <img {...props} src={getImageUrl(props.src)} />;
}
13 changes: 12 additions & 1 deletion apps/create-hypergraph/template-vite-react/src/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@ export const mapping: Mapping.Mapping = {
description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'),
},
},
Image: {
typeIds: [Id('ba4e4146-0010-499d-a0a3-caaa7f579d0e')],
properties: {
url: Id('8a743832-c094-4a62-b665-0c3cc2f9c7bc'),
},
},
Project: {
typeIds: [Id('484a18c5-030a-499c-b0f2-ef588ff16d50')],
properties: {
name: Id('a126ca53-0c8e-48d5-b888-82c734c38935'),
description: Id('9b1f76ff-9711-404c-861e-59dc3fa7d037'),
xUrl: Id('0d625978-4b3c-4b57-a86f-de45c997c73c'),
},
relations: {
avatar: Id('1155beff-fad5-49b7-a2e0-da4777b8792c'),
},
},
};
};
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { GraphImage } from '@/components/graph-image';
import { Project } from '@/schema';
import { useQuery } from '@graphprotocol/hypergraph-react';
import { createFileRoute } from '@tanstack/react-router';
import { useState } from 'react';

export const Route = createFileRoute('/explore-public-knowledge')({
component: ExplorePublicKnowledge,
});

function ExplorePublicKnowledge() {
const [searchTerm, setSearchTerm] = useState('');

const { data: projects, isPending } = useQuery(Project, {
mode: 'public',
space: 'b2565802-3118-47be-91f2-e59170735bac',
first: 40,
include: { avatar: {} },
filter: {
name: {
// contains is case sensitive
contains: searchTerm,
},
},
});

return (
Expand All @@ -24,6 +35,29 @@ function ExplorePublicKnowledge() {
</p>
</div>

{/* Search UI */}
<div className="max-w-md mx-auto mb-8">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search projects..."
className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200"
/>
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{projects.map((project) => (
<div
Expand All @@ -36,14 +70,43 @@ function ExplorePublicKnowledge() {
{/* Content */}
<div className="relative p-6">
{/* Project icon/avatar */}
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300">
<span className="text-white font-bold text-lg">{project.name.charAt(0).toUpperCase()}</span>
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300 overflow-hidden">
{project.avatar?.[0]?.url ? (
<GraphImage
src={project.avatar[0].url}
alt={`${project.name} avatar`}
className="w-full h-full object-cover"
/>
) : (
<span className="text-white font-bold text-lg">{project.name.charAt(0).toUpperCase()}</span>
)}
</div>

{/* Project name */}
<h3 className="text-xl font-bold text-gray-900 mb-2 group-hover:text-blue-600 transition-colors duration-300">
{project.name}
</h3>

{/* Project ID */}
<p className="text-[10px] text-gray-500 mb-2 font-mono">{project.id}</p>

{/* Project description */}
{project.description && <p className="text-sm text-gray-600 mb-2 line-clamp-2">{project.description}</p>}

{/* Project xUrl */}
{project.xUrl && (
<a
href={project.xUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 transition-colors duration-200 flex items-center gap-1"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
View on X
</a>
)}
</div>

{/* Decorative corner accent */}
Expand Down
7 changes: 7 additions & 0 deletions apps/create-hypergraph/template-vite-react/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export class Address extends Entity.Class<Address>('Address')({
description: Type.String,
}) {}

export class Image extends Entity.Class<Image>('Image')({
url: Type.String,
}) {}

export class Project extends Entity.Class<Project>('Project')({
name: Type.String,
description: Type.optional(Type.String),
xUrl: Type.optional(Type.String),
avatar: Type.Relation(Image),
}) {}
Loading