diff --git a/.env.example b/.env.example index 49934206..e93e7f00 100644 --- a/.env.example +++ b/.env.example @@ -71,13 +71,15 @@ NEXT_PUBLIC_BASE_RPC_URL=your_base_rpc_url_here NEXT_PUBLIC_BASE_CHAIN_ID=your_base_chain_id_here # Monad Network Configuration -MONAD_TEST_RPC_URL=your_monad_rpc_url_here +# Required for viem integration (Issue #414) +MONAD_TEST_RPC_URL=https://testnet-rpc.monad.xyz MONAD_MAIN_RPC_URL=your_monad_rpc_url_here MINTER_PRIVATE_KEY=your_minter_private_key_here MONADSCAN_API_KEY=your_monadscan_api_key_here # Contract addresses -NEXT_PUBLIC_STORY_NFT_CONTRACT=0x... +# Used by viem service integration for minting and fetching (Issue #414) +NEXT_PUBLIC_STORY_NFT_CONTRACT=0xYourErc721ContractAddressHere NEXT_PUBLIC_MARKETPLACE_CONTRACT=0x... # Wallet Connect (note: correct spelling) diff --git a/app/story/[id]/page.tsx b/app/story/[id]/page.tsx new file mode 100644 index 00000000..56e0201e --- /dev/null +++ b/app/story/[id]/page.tsx @@ -0,0 +1,168 @@ +import { Metadata, ResolvingMetadata } from 'next'; +import Image from 'next/image'; +import Link from 'next/link'; +import { notFound } from 'next/navigation'; +import { Share2, Heart, MessageSquare, ArrowLeft, Twitter } from 'lucide-react'; + +import { fetchStoryById } from '@/lib/mock-data'; +import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Badge } from '@/components/ui/badge'; + +type Props = { + params: { id: string }; +}; + +export async function generateMetadata( + { params }: Props, + parent: ResolvingMetadata +): Promise { + const story = fetchStoryById(params.id); + + if (!story) { + return { + title: 'Story Not Found | GroqTales', + }; + } + + const description = story.excerpt || story.description || story.content?.substring(0, 150) || 'A beautiful story on GroqTales'; + const coverImage = story.coverImage || '/default-og.png'; // Should use absolute URL for real prod + + return { + title: `${story.title} | GroqTales`, + description, + openGraph: { + title: story.title, + description, + images: [coverImage], + type: 'article', + authors: [story.author], + }, + twitter: { + card: 'summary_large_image', + title: story.title, + description, + images: [coverImage], + }, + }; +} + +export default async function StoryPage({ params }: Props) { + const story = fetchStoryById(params.id); + + if (!story) { + notFound(); + } + + // Calculate formatted date if it exists + const dateStr = story.createdAt instanceof Date + ? story.createdAt.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) + : story.createdAt + ? new Date(story.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) + : null; + + return ( +
+ {/* Top Header / Navigation */} +
+
+ +
+ +
+
+ +
+
+ {story.genre && ( +
+ + {story.genre} + +
+ )} + +

+ {story.title} +

+ +
+ + + {story.author?.charAt(0) || 'U'} + +
+

{story.author}

+

+ {story.authorUsername || '@' + story.author?.toLowerCase().replace(/\s+/g, '')} + {dateStr && ` • ${dateStr}`} +

+
+
+ + {story.coverImage && ( +
+ {story.title} +
+ )} + +
+ {/* Split content by newlines and render paragraphs, fallback to description */} + {(story.content || story.description || '') + .split('\n') + .filter((p: string) => p.trim() !== '') + .map((paragraph: string, i: number) => ( +

{paragraph}

+ ))} +
+
+ +
+ +
+
+
+ + {story.likes || 12} +
+
+ + {story.comments || 4} +
+
+ + +
+
+
+ ); +} diff --git a/components/story-feed.tsx b/components/story-feed.tsx index 86913fb4..9ab2cc15 100644 --- a/components/story-feed.tsx +++ b/components/story-feed.tsx @@ -223,7 +223,7 @@ export function StoryCard({ story }: { story: Story }) {