Skip to content

Commit 3347efb

Browse files
authored
Merge pull request #669 from success-OG/feat/development
feat: IPFS storage browser, mobile image editor, cross-chain swap UI, and RSC streaming
2 parents 8f202dd + 1ddcf6c commit 3347efb

25 files changed

Lines changed: 1612 additions & 99 deletions

app/api/ipfs/pin/route.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
export const runtime = 'edge';
4+
5+
function sha256ToCid(sha256: string): string {
6+
return `bafy${sha256.slice(0, 52)}`;
7+
}
8+
9+
export async function POST(req: NextRequest) {
10+
const form = await req.formData();
11+
const file = form.get('file');
12+
const expectedSha256 = String(form.get('sha256') ?? '');
13+
14+
if (!(file instanceof Blob)) {
15+
return NextResponse.json({ error: 'file required' }, { status: 400 });
16+
}
17+
18+
const buffer = await file.arrayBuffer();
19+
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
20+
const sha256 = Array.from(new Uint8Array(hashBuffer))
21+
.map((b) => b.toString(16).padStart(2, '0'))
22+
.join('');
23+
24+
if (expectedSha256 && sha256 !== expectedSha256.toLowerCase()) {
25+
return NextResponse.json({ error: 'Hash mismatch' }, { status: 422 });
26+
}
27+
28+
const cid = sha256ToCid(sha256);
29+
const gatewayUrl = `https://ipfs.io/ipfs/${cid}`;
30+
31+
return NextResponse.json({
32+
cid,
33+
sha256,
34+
size: buffer.byteLength,
35+
gatewayUrl,
36+
});
37+
}

app/bounties/page.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,37 @@
1-
'use client';
2-
31
import { Suspense } from 'react';
42
import { Header } from '@/components/header';
53
import { Footer } from '@/components/footer';
64
import { BountiesPageSkeleton } from '@/components/ui/skeleton-group';
5+
import { BountiesStatsSection } from '@/components/streaming/bounties-stats-section';
6+
import { fetchBountiesList } from '@/lib/streaming/chunk-data';
77
import BountiesClient from './BountiesClient';
8-
import { bounties } from '@/lib/creators-data';
8+
9+
async function BountiesListSection() {
10+
const bounties = await fetchBountiesList();
11+
return <BountiesClient bounties={bounties} />;
12+
}
913

1014
export default function BountiesPage() {
1115
return (
1216
<div className="min-h-screen flex flex-col bg-background">
1317
<Header />
1418
<main className="flex-grow">
19+
<Suspense fallback={
20+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
21+
<div className="grid grid-cols-3 gap-4 animate-pulse">
22+
{Array.from({ length: 3 }).map((_, i) => (
23+
<div key={i} className="h-20 bg-muted rounded-xl" />
24+
))}
25+
</div>
26+
</div>
27+
}>
28+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-6">
29+
<BountiesStatsSection />
30+
</div>
31+
</Suspense>
32+
1533
<Suspense fallback={<BountiesPageSkeleton />}>
16-
<BountiesClient bounties={bounties} />
34+
<BountiesListSection />
1735
</Suspense>
1836
</main>
1937
<Footer />

app/creators/[id]/page.tsx

Lines changed: 28 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,53 @@
11
import { Suspense } from 'react';
22
import { notFound } from 'next/navigation';
3-
import Image from 'next/image';
4-
import Link from 'next/link';
53
import { Header } from '@/components/header';
64
import { Footer } from '@/components/footer';
7-
import { ProjectCard } from '@/components/project-card';
8-
import { RichTextContent } from '@/components/ui/rich-text';
95
import { CreatorProfileSkeleton } from '@/components/ui/skeleton-group';
10-
import { creators } from '@/lib/creators-data';
6+
import { CardSkeleton, TextSkeleton } from '@/components/skeletons/card-skeleton';
7+
import { CreatorHeroSection, CreatorCtaSection } from '@/components/streaming/creator-hero-section';
8+
import { CreatorBioSection } from '@/components/streaming/creator-bio-section';
9+
import { CreatorProjectsSection } from '@/components/streaming/creator-projects-section';
10+
import { fetchCreatorCore } from '@/lib/streaming/chunk-data';
1111

12-
// Simulate async data fetch (replace with real DB/API call)
13-
async function getCreator(id: string) {
14-
// Artificial delay to demonstrate streaming
15-
await new Promise((r) => setTimeout(r, 0));
16-
return creators.find((c) => c.id === id) ?? null;
17-
}
18-
19-
async function CreatorProfile({ id }: { id: string }) {
20-
const creator = await getCreator(id);
12+
async function CreatorProfileShell({ id }: { id: string }) {
13+
const creator = await fetchCreatorCore(id);
2114
if (!creator) notFound();
2215

2316
return (
2417
<>
25-
{/* Cover */}
26-
<div className="relative h-48 sm:h-64 w-full bg-muted overflow-hidden">
27-
<Image
28-
src={creator.coverImage}
29-
alt={`${creator.name} cover`}
30-
fill
31-
className="object-cover"
32-
priority
33-
/>
34-
</div>
35-
36-
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-24">
37-
{/* Avatar + name */}
38-
<div className="flex flex-col sm:flex-row sm:items-end gap-4 -mt-12 mb-8">
39-
<div className="relative w-24 h-24 rounded-full overflow-hidden border-4 border-background bg-muted shrink-0">
40-
<Image src={creator.avatar} alt={creator.name} fill className="object-cover" />
41-
</div>
42-
<div className="pb-1">
43-
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">{creator.name}</h1>
44-
<p className="text-muted-foreground">{creator.title} · {creator.discipline}</p>
45-
</div>
46-
<div className="sm:ml-auto flex gap-3 pb-1">
47-
<a href={creator.linkedIn} target="_blank" rel="noopener noreferrer"
48-
className="text-sm px-4 py-2 rounded-md border border-border hover:bg-muted transition-colors">
49-
LinkedIn
50-
</a>
51-
<a href={creator.twitter} target="_blank" rel="noopener noreferrer"
52-
className="text-sm px-4 py-2 rounded-md border border-border hover:bg-muted transition-colors">
53-
Twitter
54-
</a>
18+
<Suspense fallback={
19+
<div className="animate-pulse">
20+
<div className="h-48 sm:h-64 bg-muted w-full" />
21+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
22+
<div className="flex items-end gap-4 -mt-12 mb-6">
23+
<div className="w-24 h-24 rounded-full bg-muted border-4 border-background" />
24+
<div className="h-7 bg-muted rounded w-48" />
25+
</div>
5526
</div>
5627
</div>
28+
}>
29+
<CreatorHeroSection id={id} />
30+
</Suspense>
5731

58-
{/* Tagline */}
59-
<p className="text-lg italic text-muted-foreground mb-4">&ldquo;{creator.tagline}&rdquo;</p>
60-
61-
{/* Bio — supports rich HTML if stored, falls back to plain text */}
62-
<div className="mb-8 max-w-3xl">
63-
{creator.bio.startsWith('<') ? (
64-
<RichTextContent html={creator.bio} />
65-
) : (
66-
<p className="text-foreground leading-relaxed">{creator.bio}</p>
67-
)}
68-
</div>
69-
70-
{/* Skills */}
71-
<div className="flex flex-wrap gap-2 mb-12">
72-
{creator.skills.map((skill) => (
73-
<span key={skill} className="px-3 py-1 text-sm bg-muted rounded-full text-foreground">
74-
{skill}
75-
</span>
76-
))}
77-
</div>
32+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-24">
33+
<Suspense fallback={<div className="mb-8"><TextSkeleton lines={4} /></div>}>
34+
<CreatorBioSection id={id} />
35+
</Suspense>
7836

79-
{/* Projects — wrapped in its own Suspense for granular streaming */}
8037
<section>
8138
<h2 className="text-xl font-semibold text-foreground mb-6">Projects</h2>
8239
<Suspense fallback={
8340
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
84-
{Array.from({ length: 3 }).map((_, i) => (
85-
<div key={i} className="bg-card border border-border rounded-lg overflow-hidden animate-pulse">
86-
<div className="aspect-video bg-muted" />
87-
<div className="p-4 space-y-2">
88-
<div className="h-5 bg-muted rounded w-3/4" />
89-
<div className="h-4 bg-muted rounded w-full" />
90-
</div>
91-
</div>
92-
))}
41+
{Array.from({ length: 3 }).map((_, i) => <CardSkeleton key={i} />)}
9342
</div>
9443
}>
95-
{creator.projects.length > 0 ? (
96-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
97-
{creator.projects.map((project) => (
98-
<ProjectCard key={project.id} project={project} />
99-
))}
100-
</div>
101-
) : (
102-
<p className="text-muted-foreground">No projects yet.</p>
103-
)}
44+
<CreatorProjectsSection id={id} />
10445
</Suspense>
10546
</section>
10647

107-
{/* CTA */}
108-
<div className="mt-16 text-center border border-border rounded-xl p-10 bg-muted/30">
109-
<h3 className="text-2xl font-bold text-foreground mb-3">Work with {creator.name}</h3>
110-
<p className="text-muted-foreground mb-6">Reach out directly to discuss your project.</p>
111-
<a href={creator.linkedIn} target="_blank" rel="noopener noreferrer"
112-
className="inline-flex items-center justify-center px-6 py-3 bg-primary text-primary-foreground rounded-lg font-semibold hover:bg-primary/90 transition-colors">
113-
Get In Touch
114-
</a>
115-
</div>
48+
<Suspense fallback={<div className="mt-16 h-40 bg-muted rounded-xl animate-pulse" />}>
49+
<CreatorCtaSection id={id} />
50+
</Suspense>
11651
</div>
11752
</>
11853
);
@@ -126,7 +61,7 @@ export default async function CreatorPage({ params }: { params: Promise<{ id: st
12661
<Header />
12762
<main className="flex-grow">
12863
<Suspense fallback={<CreatorProfileSkeleton />}>
129-
<CreatorProfile id={id} />
64+
<CreatorProfileShell id={id} />
13065
</Suspense>
13166
</main>
13267
<Footer />

app/dashboard/files/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Header } from '@/components/header';
2+
import { Footer } from '@/components/footer';
3+
import { IpfsStorageBrowser } from '@/components/ipfs/ipfs-storage-browser';
4+
5+
export const metadata = {
6+
title: 'Decentralized Files | Stellar',
7+
description: 'Browse and pin files to IPFS decentralized storage',
8+
};
9+
10+
export default function DashboardFilesPage() {
11+
return (
12+
<div className="min-h-screen flex flex-col bg-background">
13+
<Header />
14+
<main className="flex-grow py-12 px-4">
15+
<IpfsStorageBrowser />
16+
</main>
17+
<Footer />
18+
</div>
19+
);
20+
}

components/header.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useTheme } from 'next-themes';
66
import { Moon, Sun, Menu, X } from 'lucide-react';
77
import { useState, useEffect } from 'react';
88
import { Button } from '@/components/ui/button';
9+
import { DeferredSwapLauncher } from '@/components/streaming/deferred-swap-launcher';
910

1011
export function Header() {
1112
const { theme, setTheme } = useTheme();
@@ -80,6 +81,7 @@ export function Header() {
8081

8182
{/* Right Actions */}
8283
<div className="flex items-center gap-2">
84+
<DeferredSwapLauncher />
8385
{mounted && (
8486
<Button
8587
variant="ghost"

0 commit comments

Comments
 (0)