Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/.well-known/stellar.toml/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ ${currencyBlocks}
status: 200,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": String(new TextEncoder().encode(body).byteLength),
"Cache-Control": "no-store, max-age=0",
// Freighter / dApp (Origin www.phasee.xyz) y stellar.expert deben poder leer el TOML (CORS).
"Access-Control-Allow-Origin": "*",
Expand Down
3 changes: 3 additions & 0 deletions app/chamber/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function ChamberLoading() {
return <main className="min-h-screen animate-pulse bg-black p-6" aria-busy="true"><div className="mx-auto aspect-video max-w-5xl bg-zinc-900/50" /></main>
}
4 changes: 4 additions & 0 deletions app/chamber/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { FusionChamber } from "@/components/fusion-chamber"
import { fetchCollectionInfo } from "@/lib/phase-protocol"
import { publicPhaseSiteBaseUrl } from "@/lib/phase-nft-metadata-build"
import { getWorldForCollection } from "@/lib/narrative-world-store"
import { preparePhaseStreaming } from "@/lib/phase-streaming"

export const dynamic = "force-dynamic"

type Props = {
searchParams: Promise<Record<string, string | string[] | undefined>>
Expand Down Expand Up @@ -101,6 +104,7 @@ export async function generateMetadata({ searchParams }: Props): Promise<Metadat
}

export default function ChamberPage() {
preparePhaseStreaming()
return (
<Suspense fallback={<ChamberBootFallback />}>
<FusionChamber />
Expand Down
3 changes: 3 additions & 0 deletions app/explore/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function ExploreLoading() {
return <main className="min-h-screen animate-pulse bg-black p-6" aria-busy="true"><div className="h-8 w-48 bg-cyan-950/50" /><div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-3"><div className="aspect-[4/3] bg-cyan-950/30" /><div className="aspect-[4/3] bg-cyan-950/30" /><div className="aspect-[4/3] bg-cyan-950/30" /></div></main>
}
3 changes: 3 additions & 0 deletions app/profile/[wallet]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function ProfileLoading() {
return <main className="min-h-screen animate-pulse bg-black p-6" aria-busy="true"><div className="h-24 w-full bg-zinc-900/60" /><div className="mt-6 grid grid-cols-2 gap-4 md:grid-cols-4"><div className="aspect-square bg-zinc-900/50" /><div className="aspect-square bg-zinc-900/50" /><div className="aspect-square bg-zinc-900/50" /><div className="aspect-square bg-zinc-900/50" /></div></main>
}
18 changes: 11 additions & 7 deletions app/profile/[wallet]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { getSignals } from "@/lib/signal-store"
import { getAchievements } from "@/lib/achievement-store"
import { SocialChipsPublic } from "./social-chips-public"
import { FollowButton, FollowSuggestions } from "./follow-button"
import { preparePhaseStreaming } from "@/lib/phase-streaming"
import { IpfsDisplayImg } from "@/components/ipfs-display-img"

export const dynamic = "force-dynamic"

type Props = {
params: Promise<{ wallet: string }>
Expand Down Expand Up @@ -73,6 +77,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
}

export default async function ProfilePage({ params }: Props) {
preparePhaseStreaming()
const { wallet } = await params

if (!wallet || wallet.length < 10) notFound()
Expand Down Expand Up @@ -159,10 +164,7 @@ export default async function ProfilePage({ params }: Props) {
<span className="font-mono text-[8px] uppercase tracking-widest text-[#0F6E56] shrink-0">
MINT
</span>
{nft.image && (
// eslint-disable-next-line @next/next/no-img-element
<img src={nft.image} alt={nft.name} className="h-6 w-6 object-cover" />
)}
{nft.image && <IpfsDisplayImg uri={nft.image} alt={nft.name} className="h-6 w-6 object-cover" width={24} height={24} sizes="24px" />}
<p className="font-mono text-[10px] text-zinc-300 truncate">{nft.name}</p>
</div>
))}
Expand Down Expand Up @@ -216,11 +218,13 @@ export default async function ProfilePage({ params }: Props) {
title={nft.name}
>
{nft.image ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={nft.image}
<IpfsDisplayImg
uri={nft.image}
alt={nft.name}
className="h-full w-full object-cover"
width={640}
height={640}
sizes="(max-width: 768px) 33vw, 25vw"
/>
) : (
<div className="h-full w-full flex items-center justify-center">
Expand Down
3 changes: 2 additions & 1 deletion app/signals/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getSignal, getReplies, getSignalContributors, computeCreditLedger, isPh
import { ModerationHoldNotice } from "@/components/moderation-hold-notice"
import { isFaucetDenyListEnabled, isWalletDenied } from "@/lib/faucet-deny-list"
import { SignalDetailClient } from "./signal-detail-client"
import { IpfsDisplayImg } from "@/components/ipfs-display-img"

export const dynamic = "force-dynamic"

Expand Down Expand Up @@ -140,7 +141,7 @@ export default async function SignalDetailPage({ params }: Props) {
<div className="flex items-center gap-3 border border-[var(--color-border-tertiary)] p-2">
{nftImageSrc && (
// eslint-disable-next-line @next/next/no-img-element
<img src={nftImageSrc} alt={signal.nft_name} className="h-10 w-10 object-cover" />
<IpfsDisplayImg uri={nftImageSrc} alt={signal.nft_name} className="h-10 w-10 object-cover" width={40} height={40} sizes="40px" />
)}
<div className="flex flex-col">
<span className="font-mono text-[10px] text-foreground/80">{signal.nft_name}</span>
Expand Down
3 changes: 3 additions & 0 deletions app/signals/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function SignalsLoading() {
return <main className="min-h-screen animate-pulse bg-black p-6" aria-busy="true"><div className="h-8 w-48 bg-cyan-950/50" /><div className="mt-6 space-y-3"><div className="h-24 bg-zinc-900/50" /><div className="h-24 bg-zinc-900/50" /><div className="h-24 bg-zinc-900/50" /></div></main>
}
3 changes: 2 additions & 1 deletion app/signals/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SignalCompose } from "@/components/signal-compose"
import { SocialChips } from "@/components/profile-panel"
import { WalletAvatar } from "@/components/wallet-avatar"
import type { Signal } from "@/lib/signal-store"
import { IpfsDisplayImg } from "@/components/ipfs-display-img"

type ChannelStat = { id: string; label: string; count: number }

Expand Down Expand Up @@ -240,7 +241,7 @@ function PostCard({
>
{signal.nft_image && (
// eslint-disable-next-line @next/next/no-img-element
<img src={signal.nft_image} alt={signal.nft_name} className="h-8 w-8 object-cover" />
<IpfsDisplayImg uri={signal.nft_image} alt={signal.nft_name} className="h-8 w-8 object-cover" width={32} height={32} sizes="32px" />
)}
<span className="font-mono text-[10px] text-foreground/80">{signal.nft_name}</span>
{nftVerified === "pending" ? (
Expand Down
3 changes: 3 additions & 0 deletions app/world/[collection_id]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function WorldCollectionLoading() {
return <main className="min-h-screen animate-pulse bg-black p-6" aria-busy="true"><div className="h-8 w-56 bg-violet-950/50" /><div className="mt-6 grid grid-cols-2 gap-4 md:grid-cols-4"><div className="aspect-square bg-violet-950/30" /><div className="aspect-square bg-violet-950/30" /><div className="aspect-square bg-violet-950/30" /><div className="aspect-square bg-violet-950/30" /></div></main>
}
14 changes: 10 additions & 4 deletions app/world/[collection_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import {
buildPhaseTokenMetadataJson,
publicPhaseSiteBaseUrl,
} from "@/lib/phase-nft-metadata-build"
import { preparePhaseStreaming } from "@/lib/phase-streaming"
import { IpfsDisplayImg } from "@/components/ipfs-display-img"

export const dynamic = "force-dynamic"

const navLink =
"inline-flex min-h-[36px] items-center border border-zinc-700 bg-zinc-900/60 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-widest text-zinc-400 transition-colors hover:border-violet-500/50 hover:text-violet-300"
Expand Down Expand Up @@ -119,6 +123,7 @@ function StoryTimelineVisualizer({
}

export default async function WorldGalleryPage({ params }: Props) {
preparePhaseStreaming()
const { collection_id } = await params
const collectionId = parseInt(collection_id, 10)

Expand Down Expand Up @@ -244,13 +249,14 @@ export default async function WorldGalleryPage({ params }: Props) {
>
<div className="relative border-b border-violet-500/20">
{nft.image ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={nft.image}
<IpfsDisplayImg
uri={nft.image}
alt={nft.name}
loading="lazy"
decoding="async"
className="aspect-square w-full object-cover opacity-80 transition-opacity group-hover:opacity-95"
width={640}
height={640}
sizes="(max-width: 768px) 50vw, 25vw"
/>
) : (
<div className="flex aspect-square w-full items-center justify-center bg-violet-950/20">
Expand Down
10 changes: 7 additions & 3 deletions components/crt-collection-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import Link from "next/link"
import { useToastQueue } from "@/hooks/use-toast-queue"
import { IpfsDisplayImg } from "@/components/ipfs-display-img"

type Attribute = {
trait_type: string
Expand Down Expand Up @@ -136,11 +137,14 @@ export function CrtCollectionPreview({
style={CARD_GLOW}
>
{image ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={image}
<IpfsDisplayImg
uri={image}
alt={name}
className="w-full h-full object-cover"
priority
width={720}
height={720}
sizes="(max-width: 768px) 90vw, 720px"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-purple-950/10">
Expand Down
35 changes: 33 additions & 2 deletions components/ipfs-display-img.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use client"

import Image from "next/image"
import { ipfsHttpsGatewayUrls } from "@/lib/phase-protocol"
import { isFeatureEnabled } from "@/lib/feature-flags"
import { useEffect, useMemo, useState, type ReactNode } from "react"

type Props = {
Expand All @@ -18,6 +20,10 @@ type Props = {
* in an ordered hang (phase-142) and shifts every later work.
*/
fallback?: ReactNode
priority?: boolean
sizes?: string
width?: number
height?: number
}

export function IpfsDisplayImg({ uri, alt = "", className, loading = "lazy", variant, focalPoint, onExhausted, fallback }: Props) {
Expand All @@ -43,8 +49,33 @@ export function IpfsDisplayImg({ uri, alt = "", className, loading = "lazy", var

if (!uri.trim() || !src || exhausted) return fallback ? <>{fallback}</> : null

return (
// eslint-disable-next-line @next/next/no-img-element -- remote IPFS / HTTPS previews
const optimized = isFeatureEnabled("phase-171")

return optimized ? (
<Image
key={`${uri}:${idx}`}
src={src}
alt={alt}
className={className}
width={width}
height={height}
sizes={sizes ?? "(max-width: 768px) 100vw, 33vw"}
priority={priority}
loading={priority ? "eager" : loading}
decoding="async"
unoptimized={false}
style={{ objectFit: "cover", aspectRatio: `${width} / ${height}` }}
referrerPolicy="no-referrer"
onError={() => {
if (idx + 1 < candidates.length) setIdx((i) => i + 1)
else {
if (fallback) setExhausted(true)
onExhausted?.()
}
}}
/>
) : (
// eslint-disable-next-line @next/next/no-img-element -- legacy flag-off path
<img
key={`${uri}:${idx}`}
src={src}
Expand Down
13 changes: 8 additions & 5 deletions components/wallet-avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState, useEffect, useRef, useCallback } from "react"
import { nftGridOverscanPx } from "@/lib/nft-grid-virtualization"
import { IpfsDisplayImg } from "@/components/ipfs-display-img"

// ??? phase-117: multi-gateway fallback client wiring ????????????????????????
// Preserves original lazy-load + initials fallback. When an image fails,
Expand Down Expand Up @@ -179,13 +180,15 @@ export function WalletAvatar({
style={{ width: size, height: size }}
title={avatar?.locale ? `${avatar.name} (${avatar.locale})` : avatar?.name}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={avatar.image}
<IpfsDisplayImg
uri={avatar.image}
alt={avatar.name}
className="w-full h-full object-cover"
onError={handleImgError}
onExhausted={handleImgError}
loading="lazy"
width={size}
height={size}
sizes={`${size}px`}
/>
</div>
)
Expand All @@ -209,4 +212,4 @@ export function WalletAvatar({
{initials}
</div>
)
}
}
19 changes: 19 additions & 0 deletions lib/phase-streaming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { cache } from "react"
import { unstable_noStore as noStore } from "next/cache"
import { isFeatureEnabled } from "@/lib/feature-flags"

/** Shared RSC primitives for latency-heavy pages. Disabled flags preserve legacy rendering. */
export function preparePhaseStreaming(): void {
if (isFeatureEnabled("phase-170")) noStore()
}

/** React.cache dedupes metadata reads within one server render request. */
export const cachePhaseRead = <TArgs extends readonly unknown[], TResult>(
reader: (...args: TArgs) => Promise<TResult>,
) => cache(reader)

export function phaseReadHeaders(): HeadersInit {
return isFeatureEnabled("phase-170")
? { "Cache-Control": "private, no-store", Vary: "RSC, Next-Router-State-Tree" }
: {}
}
21 changes: 21 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
const phase172Enabled = ["1", "true", "yes", "on"].includes(
(process.env.NEXT_PUBLIC_FEATURE_PHASE_172 ?? "").trim().toLowerCase(),
)
const phase171Enabled = ["1", "true", "yes", "on"].includes(
(process.env.NEXT_PUBLIC_FEATURE_PHASE_171 ?? "").trim().toLowerCase(),
)

/** @type {import('next').NextConfig} */
const nextConfig = {
// Spike 608 (phase-175): bonded with next 16.2.2 + react 19.2.0 + @stellar/stellar-sdk ^15.0.1
transpilePackages: ["@creit.tech/stellar-wallets-kit"],
...(phase172Enabled
? {
optimizePackageImports: ["lucide-react", "@radix-ui/react-icons", "recharts", "date-fns"],
bundlePagesRouterDependencies: true,
}
: {}),
// Rust build trees are committed in this repo; exclude from serverless traces (250 MB limit on Vercel).
outputFileTracingExcludes: {
"*": ["./contracts/**/*", "./scripts/**/*"],
},
images: {
...(phase171Enabled
? {
formats: ["image/avif", "image/webp"],
minimumCacheTTL: 86400,
deviceSizes: [320, 480, 640, 768, 1024, 1280, 1536, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
}
: {}),
remotePatterns: [
{
protocol: "https",
Expand Down
1 change: 1 addition & 0 deletions scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"issue:sac": "tsx issue-sac-token.ts",
"setup:phase-v2": "tsx setup-phase-v2.ts",
"set:issuer-home-domain": "tsx set-issuer-home-domain.ts",
"verify:issuer-home-domain": "FEATURE_PHASE_173=1 ISSUER_HOME_DOMAIN_DRY_RUN=1 tsx set-issuer-home-domain.ts",
"issuer-to-distributor": "tsx issuer-pay-distributor.ts",
"distributor-trust-and-pay": "tsx distributor-trust-and-payment.ts",
"reset:phase": "tsx reset-phase.ts",
Expand Down
Loading
Loading