diff --git a/apps/example/microblog-ui/app/Post/page.tsx b/apps/example/microblog-ui/app/Post/page.tsx deleted file mode 100644 index 91aa12d..0000000 --- a/apps/example/microblog-ui/app/Post/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Suspense } from 'react'; - -import MicroblogPostRouteClient from '../../src/components/MicroblogPostRouteClient'; - -export default function PostPage() { - return ( - - - - ); -} diff --git a/apps/example/microblog-ui/app/page.tsx b/apps/example/microblog-ui/app/page.tsx deleted file mode 100644 index 6bfea9a..0000000 --- a/apps/example/microblog-ui/app/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import MicroblogHomeClient from '../src/components/MicroblogHomeClient'; - -export default function HomePage() { - return ; -} diff --git a/apps/example/microblog-ui/src/components/MicroblogComposeClient.tsx b/apps/example/microblog-ui/src/components/MicroblogComposeClient.tsx deleted file mode 100644 index 3676518..0000000 --- a/apps/example/microblog-ui/src/components/MicroblogComposeClient.tsx +++ /dev/null @@ -1,363 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { useEffect, useMemo, useState } from 'react'; -import { useRouter } from 'next/navigation'; - -import ImageFieldInput from './ImageFieldInput'; -import TxStatus, { type TxPhase } from './TxStatus'; -import { fnCreate } from '../lib/app'; -import { chainWithRpcOverride, requestWalletAddress } from '../lib/clients'; -import { getReadRpcUrl } from '../lib/manifest'; -import { submitWriteTx } from '../lib/tx'; -import { listOwnedProfiles, loadMicroblogRuntime, profileHandle, profileLabel, type ProfileRecord } from '../lib/microblog'; - -type ComposeState = { - loading: boolean; - runtimeError: string | null; - connectError: string | null; - submitError: string | null; -}; - -const PROFILE_STORAGE_PREFIX = 'TH_MICROBLOG_PROFILE_ID:'; - -export default function MicroblogComposeClient() { - const router = useRouter(); - const [state, setState] = useState({ - loading: true, - runtimeError: null, - connectError: null, - submitError: null - }); - const [runtime, setRuntime] = useState(null); - const [account, setAccount] = useState(null); - const [profiles, setProfiles] = useState([]); - const [selectedProfileId, setSelectedProfileId] = useState(''); - const [body, setBody] = useState(''); - const [image, setImage] = useState(''); - const [imageUploadBusy, setImageUploadBusy] = useState(false); - const [txStatus, setTxStatus] = useState(null); - const [txPhase, setTxPhase] = useState('idle'); - const [txHash, setTxHash] = useState(null); - - useEffect(() => { - let cancelled = false; - - (async () => { - try { - const loadedRuntime = await loadMicroblogRuntime(); - if (cancelled) return; - setRuntime(loadedRuntime); - - try { - const cached = localStorage.getItem('TH_ACCOUNT'); - if (cached && !cancelled) setAccount(cached); - } catch { - // ignore - } - } catch (error: any) { - if (cancelled) return; - setState((prev) => ({ ...prev, runtimeError: String(error?.message ?? error), loading: false })); - return; - } - - if (!cancelled) setState((prev) => ({ ...prev, loading: false })); - })(); - - return () => { - cancelled = true; - }; - }, []); - - useEffect(() => { - let cancelled = false; - if (!runtime || !account) { - setProfiles([]); - setSelectedProfileId(''); - return; - } - - setState((prev) => ({ ...prev, loading: true, connectError: null })); - void (async () => { - try { - const ownedProfiles = await listOwnedProfiles(runtime, account); - if (cancelled) return; - setProfiles(ownedProfiles); - - let preferred = ''; - try { - const stored = localStorage.getItem(`${PROFILE_STORAGE_PREFIX}${account.toLowerCase()}`) ?? ''; - if (stored && ownedProfiles.some((entry) => String(entry.id) === stored)) preferred = stored; - } catch { - // ignore - } - if (!preferred && ownedProfiles[0]) preferred = String(ownedProfiles[0].id); - setSelectedProfileId(preferred); - } catch (error: any) { - if (cancelled) return; - setState((prev) => ({ ...prev, connectError: String(error?.message ?? error) })); - } finally { - if (!cancelled) setState((prev) => ({ ...prev, loading: false })); - } - })(); - - return () => { - cancelled = true; - }; - }, [runtime, account]); - - useEffect(() => { - if (!account || !selectedProfileId) return; - try { - localStorage.setItem(`${PROFILE_STORAGE_PREFIX}${account.toLowerCase()}`, selectedProfileId); - } catch { - // ignore - } - }, [account, selectedProfileId]); - - const selectedProfile = useMemo( - () => profiles.find((entry) => String(entry.id) === selectedProfileId) ?? null, - [profiles, selectedProfileId] - ); - const walletChain = useMemo( - () => (runtime ? chainWithRpcOverride(runtime.chain, getReadRpcUrl(runtime.manifest) || undefined) : null), - [runtime] - ); - - async function connectWallet() { - if (!walletChain) return; - setState((prev) => ({ ...prev, connectError: null })); - try { - const nextAccount = await requestWalletAddress(walletChain); - setAccount(nextAccount); - try { - localStorage.setItem('TH_ACCOUNT', nextAccount); - } catch { - // ignore - } - } catch (error: any) { - setState((prev) => ({ ...prev, connectError: String(error?.message ?? error) })); - } - } - - async function submit() { - if (!runtime || !walletChain || !selectedProfile || !body.trim() || imageUploadBusy) return; - - setState((prev) => ({ ...prev, submitError: null })); - setTxStatus(null); - setTxPhase('idle'); - setTxHash(null); - - try { - const result = await submitWriteTx({ - manifest: runtime.manifest, - deployment: runtime.deployment, - chain: walletChain, - publicClient: runtime.publicClient, - address: runtime.appAddress, - abi: runtime.abi, - functionName: fnCreate('Post'), - contractArgs: [ - { - authorProfile: selectedProfile.id, - body: body.trim(), - image: image.trim() - } - ], - setStatus: setTxStatus, - onPhase: setTxPhase, - onHash: setTxHash - }); - - setTxStatus(`Posted (${result.hash.slice(0, 10)}…).`); - router.push('/'); - router.refresh(); - } catch (error: any) { - setState((prev) => ({ ...prev, submitError: String(error?.message ?? error) })); - setTxStatus(null); - setTxPhase('failed'); - } - } - - if (state.loading && !runtime) { - return ( -
-

Loading composer…

-

Resolving the active deployment and wallet state.

-
- ); - } - - if (state.runtimeError) { - return ( -
-
/compose/error
-

Unable to load composer

-

{state.runtimeError}

-
- ); - } - - return ( -
-
-
-
-
- /post/compose -
- normalized author identity - profile-linked posts -
-
-

- Compose as a profile -
- not as a copied handle string. -

-

- Posts now store authorProfile as an on-chain reference to Profile, - so handle and avatar changes flow through existing posts automatically. -

-
- Back to feed - Browse profiles -
-
- -
-
/identity
-
-
-
{account ? 1 : 0}
-
Wallet linked
-
-
-
{profiles.length}
-
Owned profiles
-
-
-
- posts reference profiles - profile changes propagate -
-
-
-
- - {!account ? ( -
-
/wallet
-

Connect a wallet to compose

-

Posting now requires selecting one of your on-chain profiles. Connect the wallet that owns the profile first.

-
- -
- {state.connectError ?

{state.connectError}

: null} -
- ) : null} - - {account && !profiles.length ? ( -
-
/profiles/empty
-

No owned profiles found

-

Create a profile first. Once it exists on-chain under this wallet, you can compose posts as that profile.

-
- Create profile -
- {state.connectError ?

{state.connectError}

: null} -
- ) : null} - - {account && profiles.length ? ( -
-
-

Compose Post

-

Choose the on-chain profile identity for this post, then write the post body and optional image.

-
- -
-
- - -
- -
- -
- {selectedProfile ? ( -
-
- profile #{String(selectedProfile.id)} - {profileHandle(selectedProfile.record) ? @{profileHandle(selectedProfile.record)} : null} -
- {profileLabel(selectedProfile.record)} - {String(selectedProfile.record?.bio ?? '').trim() ? ( -

{String(selectedProfile.record.bio)}

- ) : null} -
- ) : ( - Select a profile. - )} -
-
- -
- -