From 02d20ed100dbdfe295e7b32ffd9c0e0e6272a030 Mon Sep 17 00:00:00 2001 From: Parth Date: Wed, 24 Jun 2026 17:25:09 +0000 Subject: [PATCH 1/6] step 1: replace iqlabs stub with @iqlabs-official/git-sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installs @iqlabs-official/git-sdk and rewires lib/iqlabs.ts to use GitClient from the /browser entry. The publish flow now: 1. Exports site HTML via exportSiteHtml() 2. Creates an on-chain git repo (idempotent — checks existing repos first) 3. Commits index.html + iqpages.json via client.commit() 4. Calls deployPages() to register in IQ Pages gallery 5. Returns the deployPages sig as the on-chain pointer for the SNS record Mock mode (NEXT_PUBLIC_IQ_MOCK=1) path is unchanged. lib/publish.ts updated to pass connection through to publishToIQLabs. Co-Authored-By: Claude Sonnet 4.6 --- lib/iqlabs.ts | 127 ++++++++++++++++++++++++++++++++-------------- lib/publish.ts | 7 ++- package-lock.json | 21 ++++++++ package.json | 25 ++++----- 4 files changed, 129 insertions(+), 51 deletions(-) diff --git a/lib/iqlabs.ts b/lib/iqlabs.ts index 9c82555..64b15fe 100644 --- a/lib/iqlabs.ts +++ b/lib/iqlabs.ts @@ -1,59 +1,66 @@ // lib/iqlabs.ts // // WRITE layer for IQLabs onchain storage. (Reads live in lib/gateway.ts.) +// Single integration point for @iqlabs-official/git-sdk/browser — nothing else +// in the codebase imports the SDK directly. // -// Confirmed model (from the deployed gateway + iq-gateway repo): -// • A published site is a set of files written on-chain, finalized by a -// MANIFEST transaction. The manifest tx signature IS the on-chain path — -// exactly the "tail of the linked list becomes the path" rule. -// • The live gateway then serves it at {gateway}/site/{manifestSig}. -// • The .sol domain gets ONE URL record pointing at that manifest. -// -// ⚠️ The write itself is still stubbed: it requires the iqlabs-solana-sdk -// (github.com/IQCoreTeam/iqlabs-solana-sdk). Wiring real writes is a major -// step pending owner approval — see CHANGES.md. This file stays the single -// integration point; nothing else touches the SDK. +// Publish flow: +// exportSiteHtml() → commit(index.html + iqpages.json) → deployPages() +// deployPages() sig = the on-chain pointer stored in the SNS Url record. +import type { Connection } from "@solana/web3.js"; +import { GitClient, deployPages, readOwnerRepos } from "@iqlabs-official/git-sdk/browser"; +import { exportSiteHtml } from "./export-html"; import type { Site, StorageRef } from "./types"; +/** Wallet-adapter shape the git-sdk accepts as a SignerInput. */ +export interface PublishWallet { + publicKey: { toBase58(): string }; + signTransaction: (tx: unknown) => Promise; + signAllTransactions?: (txs: unknown[]) => Promise; +} + export interface PublishInput { site: Site; - /** Connected wallet adapter (signs the storage txs). */ - wallet: unknown; + wallet: PublishWallet; + connection: Connection; + onProgress?: (step: string, percent: number) => void; } -/** True for placeholder pointers created in demo mode. */ +/** True for placeholder pointers created in demo / mock mode. */ export function isMockPointer(pointer: string): boolean { return pointer.startsWith("iq:mock:"); } /** - * Serializes a site into the payload to be written on-chain. - * NOTE: the real pipeline publishes FILES (index.html + assets) under a - * manifest, not this JSON — pending the canonical-format decision, this JSON - * remains the draft/publish payload and the size estimator's input. + * Fast sync estimate of the published HTML size for the pre-publish summary. + * The actual HTML is ~10× larger than the raw content JSON due to markup and + * embedded styles, so we use that as a rough upper bound. */ -export function buildSitePayload(site: Site): string { - return JSON.stringify({ - v: 1, - templateId: site.templateId, - title: site.title, - theme: site.theme, - content: site.content, - publishedAt: Date.now(), - }); +export function estimatePayloadBytes(site: Site): number { + const contentBytes = new TextEncoder().encode(JSON.stringify(site.content)).length; + return contentBytes * 10; } -/** Estimated on-chain byte size of the current site payload. */ -export function estimatePayloadBytes(site: Site): number { - return new TextEncoder().encode(buildSitePayload(site)).length; +function slugify(s: string): string { + return s + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + .slice(0, 32) || "my-site"; +} + +function utf8ToBase64(str: string): string { + const bytes = new TextEncoder().encode(str); + let binary = ""; + for (const b of bytes) binary += String.fromCharCode(b); + return btoa(binary); } /** - * Writes the site on-chain and returns the manifest pointer. - * Real implementation (once approved): generate static files via - * lib/export-html.ts, write them + manifest with iqlabs-solana-sdk, return the - * manifest tx signature as `pointer`. + * Publishes a site on-chain via git-sdk and returns the deploy pointer. + * Mock mode (NEXT_PUBLIC_IQ_MOCK=1) skips all chain calls so the flow is + * demoable without a funded wallet. */ export async function publishToIQLabs(input: PublishInput): Promise { if (process.env.NEXT_PUBLIC_IQ_MOCK === "1") { @@ -65,8 +72,52 @@ export async function publishToIQLabs(input: PublishInput): Promise }; } - throw new Error( - "IQLabs SDK not wired yet. Set NEXT_PUBLIC_IQ_MOCK=1 to demo the flow, " + - "or implement publishToIQLabs() in lib/iqlabs.ts.", - ); + const { site, wallet, connection, onProgress } = input; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const signer = wallet as any; + + onProgress?.("Exporting site HTML…", 10); + const html = await exportSiteHtml(site); + + const repoName = slugify(site.title); + + const iqpagesConfig: Record = { + name: site.title, + version: "1.0.0", + description: `IQForge site — template ${site.templateId}`, + entry: "index.html", + }; + + const client = new GitClient({ connection, signer }); + + onProgress?.("Checking on-chain repo…", 20); + const ownerRepos = await readOwnerRepos(wallet.publicKey.toBase58()); + const repoExists = ownerRepos.some((r) => r.name === repoName); + + if (!repoExists) { + onProgress?.("Creating on-chain repo…", 30); + await client.createRepo({ + name: repoName, + description: `IQForge: ${site.title}`, + isPublic: true, + timestamp: Date.now(), + }); + } + + onProgress?.("Committing files on-chain…", 45); + const commit = await client.commit(repoName, "publish", { + "index.html": utf8ToBase64(html), + "iqpages.json": utf8ToBase64(JSON.stringify(iqpagesConfig, null, 2)), + }); + + onProgress?.("Deploying to IQ Pages…", 80); + const { sig } = await deployPages(signer, repoName); + + onProgress?.("Done!", 100); + + return { + provider: "iqlabs", + pointer: sig, + txSignature: commit.id, + }; } diff --git a/lib/publish.ts b/lib/publish.ts index 8365c74..408f941 100644 --- a/lib/publish.ts +++ b/lib/publish.ts @@ -26,7 +26,12 @@ export async function publishSite(args: { /** e.g. "alice.sol". Omit to publish onchain without attaching a domain yet. */ domain?: string; }): Promise { - const storage = await publishToIQLabs({ site: args.site, wallet: args.wallet }); + const storage = await publishToIQLabs({ + site: args.site, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + wallet: args.wallet as any, + connection: args.connection, + }); let domainTx: string | undefined; if (args.domain) { diff --git a/package-lock.json b/package-lock.json index b4e38ef..4d300aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@bonfida/spl-name-service": "^3.0.9", + "@iqlabs-official/git-sdk": "^0.1.18", "@iqlabs-official/solana-sdk": "^0.1.27", "@solana/wallet-adapter-base": "^0.9.23", "@solana/wallet-adapter-react": "^0.15.35", @@ -974,6 +975,26 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@iqlabs-official/git-sdk": { + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/@iqlabs-official/git-sdk/-/git-sdk-0.1.18.tgz", + "integrity": "sha512-N6nuHJTWv7wMfXD6mgsM5kDJSgdHeFerXiQtnFXj0ZSCZsvw2MsCGPagbCR4c9LgOamJUOIgG7x2lYRQFoV6yw==", + "peerDependencies": { + "@iqlabs-official/ethereum-sdk": "^0.2.2", + "@iqlabs-official/solana-sdk": "^0.1.27", + "@solana/web3.js": "^1.98.0", + "buffer": "^6.0.3", + "ethers": "^6.16.0" + }, + "peerDependenciesMeta": { + "@iqlabs-official/ethereum-sdk": { + "optional": true + }, + "ethers": { + "optional": true + } + } + }, "node_modules/@iqlabs-official/solana-sdk": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/@iqlabs-official/solana-sdk/-/solana-sdk-0.1.27.tgz", diff --git a/package.json b/package.json index 88199ef..a02e25d 100644 --- a/package.json +++ b/package.json @@ -9,28 +9,29 @@ "lint": "next lint" }, "dependencies": { - "next": "^15.1.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "@solana/web3.js": "^1.95.3", + "@bonfida/spl-name-service": "^3.0.9", + "@iqlabs-official/git-sdk": "^0.1.18", + "@iqlabs-official/solana-sdk": "^0.1.27", "@solana/wallet-adapter-base": "^0.9.23", "@solana/wallet-adapter-react": "^0.15.35", "@solana/wallet-adapter-react-ui": "^0.9.35", "@solana/wallet-adapter-wallets": "^0.19.32", - "@bonfida/spl-name-service": "^3.0.9", - "tailwindcss-animate": "^1.0.7", - "@iqlabs-official/solana-sdk": "^0.1.27", - "buffer": "^6.0.3" + "@solana/web3.js": "^1.95.3", + "buffer": "^6.0.3", + "next": "^15.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "typescript": "^5.6.3", "@types/node": "^20.14.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "tailwindcss": "^3.4.13", - "postcss": "^8.4.47", "autoprefixer": "^10.4.20", "eslint": "^8.57.0", - "eslint-config-next": "^15.1.0" + "eslint-config-next": "^15.1.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "typescript": "^5.6.3" } } From 48da7d5aabd9d0a120564424e0456d1fbfb5a822 Mon Sep 17 00:00:00 2001 From: Parth Date: Wed, 24 Jun 2026 17:40:45 +0000 Subject: [PATCH 2/6] step 2: split profile out of website template gallery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profile is now a distinct product path from the website builder: - lib/profile.ts: IQProfile type ({ version, format: 'react95', data, theme }), 5 built-in react95-format themes (IQ Dark, Windows 95, Midnight, Sakura, Hacker), publishProfile() commits iqprofile.json to 'iq-profile' repo via git-sdk - components/profile/theme-picker.tsx: visual theme selector with live color preview - components/profile/profile-form.tsx: fields form (name, handle, bio, avatar, links) - app/profile/page.tsx: full editor — theme picker + live preview + form + publish - app/page.tsx: updated homepage with two explicit paths (Profile vs Website) - lib/templates.ts: removed creator-profile from website gallery and TEMPLATE_DEFINITIONS - app/templates/page.tsx: relabeled as 'Website Templates', added link to profile flow Co-Authored-By: Claude Sonnet 4.6 --- app/page.tsx | 98 +++++++--- app/profile/page.tsx | 172 +++++++++++++++++ app/templates/page.tsx | 14 +- components/profile/profile-form.tsx | 129 +++++++++++++ components/profile/theme-picker.tsx | 53 ++++++ lib/profile.ts | 279 ++++++++++++++++++++++++++++ lib/templates.ts | 4 +- 7 files changed, 718 insertions(+), 31 deletions(-) create mode 100644 app/profile/page.tsx create mode 100644 components/profile/profile-form.tsx create mode 100644 components/profile/theme-picker.tsx create mode 100644 lib/profile.ts diff --git a/app/page.tsx b/app/page.tsx index 5702251..aed3f8a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,37 +1,87 @@ -// app/page.tsx import Link from "next/link"; export default function HomePage() { return ( -
+
-

SNS × IQLabs

-

- Build a website that -
- lives forever. -

-

- Pick a template, make it yours, attach a .sol domain, and publish permanently - onchain. No code, no servers, no link rot. -

-
- +

SNS × IQLabs

+

+ Build things that +
+ live forever. +

+

+ Publish your identity and your sites permanently onchain. No code, no servers, + no link rot. Attach a .sol domain and you're done. +

+
+ + {/* Two paths */} +
+ + - Browse templates - - - My Sites - + badge="Website" + title="Build a Website" + description="Pick a template, make it yours, and publish a full page on-chain via IQ Pages. Attach a .sol domain and it lives forever." + cta="Browse templates →" + accent="#818CF8" + />
+ +

+ Already published something?{" "} + + View My Sites → + +

); } + +function PathCard({ + href, + badge, + title, + description, + cta, + accent, +}: { + href: string; + badge: string; + title: string; + description: string; + cta: string; + accent: string; +}) { + return ( + + + {badge} + +

{title}

+

{description}

+ + {cta} + + + ); +} diff --git a/app/profile/page.tsx b/app/profile/page.tsx new file mode 100644 index 0000000..01ed885 --- /dev/null +++ b/app/profile/page.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { useState } from "react"; +import { useConnection, useWallet } from "@solana/wallet-adapter-react"; +import { + DEFAULT_PROFILE_DATA, + PROFILE_THEMES, + publishProfile, + type IQProfile, + type ProfileData, + type ProfileTheme, +} from "@/lib/profile"; +import { ThemePicker } from "@/components/profile/theme-picker"; +import { ProfileForm } from "@/components/profile/profile-form"; + +type Phase = "edit" | "publishing" | "done" | "error"; + +export default function ProfilePage() { + const { connection } = useConnection(); + const wallet = useWallet(); + + const [data, setData] = useState(DEFAULT_PROFILE_DATA); + const [theme, setTheme] = useState(PROFILE_THEMES[0]); + const [phase, setPhase] = useState("edit"); + const [statusMsg, setStatusMsg] = useState(""); + const [pointer, setPointer] = useState(""); + + const publish = async () => { + if (!wallet.publicKey || !wallet.signTransaction) { + setStatusMsg("Connect your wallet first."); + setPhase("error"); + return; + } + + setPhase("publishing"); + + const profile: IQProfile = { version: 1, format: "react95", data, theme }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await publishProfile({ + profile, + wallet: wallet as any, + connection, + onProgress: (step) => setStatusMsg(step), + }); + setPointer(result.pointer); + setPhase("done"); + } catch (e) { + setStatusMsg(e instanceof Error ? e.message : "Something went wrong."); + setPhase("error"); + } + }; + + const isMock = process.env.NEXT_PUBLIC_IQ_MOCK === "1"; + + return ( +
+

+ IQForge · Profile +

+

Your on-chain identity

+

+ Pick a theme, fill in your details, and publish permanently onchain. Your profile + renders identically on every IQ surface — no link rot, no middleman. +

+ + {isMock && ( +
+ Demo mode — set NEXT_PUBLIC_IQ_MOCK=0 and connect a funded wallet to publish for real. +
+ )} + + {/* Theme picker */} +
+

Choose a theme

+ +
+ + {/* Live mini-preview */} +
+
+
+ {theme.name} +
+ {data.avatar && ( + // eslint-disable-next-line @next/next/no-img-element + avatar + )} +

+ {data.displayName || "Display Name"} +

+

+ {data.handle || "@handle"} +

+ {data.bio && ( +

+ {data.bio} +

+ )} + {data.links.length > 0 && ( +
+ {data.links.map((l, i) => ( + + {l.label || l.url} + + ))} +
+ )} +
+
+ + {/* Form */} +
+

Profile details

+ +
+ + {/* Publish */} +
+ {phase === "publishing" && ( +

{statusMsg}

+ )} + {phase === "done" && ( +
+

Profile published ✓

+

+ commit: {pointer} +

+
+ )} + {phase === "error" && ( +

+ {statusMsg} +

+ )} + +
+ + {!wallet.publicKey && ( +

Connect your wallet to publish.

+ )} +
+
+
+ ); +} diff --git a/app/templates/page.tsx b/app/templates/page.tsx index 23e5e6a..13254b6 100644 --- a/app/templates/page.tsx +++ b/app/templates/page.tsx @@ -36,16 +36,22 @@ export default function TemplateGalleryPage() {

- IQForge · Template Gallery + IQForge · Website Templates

- Pick a starting point + Pick a website template

- Beautiful, pre-built templates you can make yours in minutes. Customize text, - images, and colors — then publish permanently onchain.{" "} + Pre-built templates for tokens, NFTs, DAOs, and more. Customize and publish + permanently onchain via IQ Pages.{" "} {readyCount} live, more shipping weekly.

+

+ Looking for a personal profile?{" "} + + Build your on-chain identity → + +

{/* filters */} diff --git a/components/profile/profile-form.tsx b/components/profile/profile-form.tsx new file mode 100644 index 0000000..56bfb76 --- /dev/null +++ b/components/profile/profile-form.tsx @@ -0,0 +1,129 @@ +"use client"; + +import type { ProfileData, ProfileLink } from "@/lib/profile"; + +export function ProfileForm({ + data, + onChange, +}: { + data: ProfileData; + onChange: (data: ProfileData) => void; +}) { + const set = (key: K, value: ProfileData[K]) => + onChange({ ...data, [key]: value }); + + const setLink = (index: number, link: ProfileLink) => { + const next = [...data.links]; + next[index] = link; + set("links", next); + }; + + const addLink = () => set("links", [...data.links, { label: "", url: "https://" }]); + + const removeLink = (index: number) => + set("links", data.links.filter((_, i) => i !== index)); + + return ( +
+ + set("displayName", e.target.value)} + maxLength={40} + placeholder="Satoshi" + className={INPUT_CLS} + /> + + + + set("handle", e.target.value)} + maxLength={30} + placeholder="@satoshi" + className={INPUT_CLS} + /> + + + +