Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +74 to 78
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

If dotenv-linter is enforced, fix the MONAD_ key ordering warnings.*

Static analysis flags key ordering around MONAD_TEST_RPC_URL, MONAD_MAIN_RPC_URL, MINTER_PRIVATE_KEY, MONADSCAN_API_KEY. If CI treats these as errors, reorder them to satisfy the linter.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 76-76: [UnorderedKey] The MONAD_MAIN_RPC_URL key should go before the MONAD_TEST_RPC_URL key

(UnorderedKey)


[warning] 77-77: [UnorderedKey] The MINTER_PRIVATE_KEY key should go before the MONAD_MAIN_RPC_URL key

(UnorderedKey)


[warning] 78-78: [UnorderedKey] The MONADSCAN_API_KEY key should go before the MONAD_MAIN_RPC_URL key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 74 - 78, dotenv-linter is flagging key ordering
for the MONAD_ entries; reorder the ENV keys so they are alphabetically sorted
to satisfy the linter — change the block to: MINTER_PRIVATE_KEY,
MONADSCAN_API_KEY, MONAD_MAIN_RPC_URL, MONAD_TEST_RPC_URL (replace the existing
ordering of MONAD_TEST_RPC_URL / MONAD_MAIN_RPC_URL / MINTER_PRIVATE_KEY /
MONADSCAN_API_KEY so the keys are sorted).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KanhaiyaBagul fix these warnings

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


# 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)
Expand Down
168 changes: 168 additions & 0 deletions app/story/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
Comment thread
Drago-03 marked this conversation as resolved.
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 (
<div className="min-h-screen bg-background">
{/* Top Header / Navigation */}
<div className="sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-14 items-center">
<Button variant="ghost" size="sm" asChild className="-ml-2">
<Link href="/?tab=community">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Stories
</Link>
</Button>
<div className="flex-1" />
<Button variant="outline" size="sm" asChild>
<Link
href={`https://twitter.com/intent/tweet?text=Reading "${story.title}" on GroqTales&url=${encodeURIComponent(`https://groqtales.com/story/${story.id}`)}`}
target="_blank"
rel="noopener noreferrer"
>
<Twitter className="mr-2 h-4 w-4 text-[#1DA1F2]" />
Share
</Link>
</Button>
</div>
</div>

<main className="container max-w-4xl py-6 lg:py-10">
<article className="prose dark:prose-invert max-w-none">
{story.genre && (
<div className="mb-4">
<Badge variant="secondary" className="capitalize text-sm px-3 py-1">
{story.genre}
</Badge>
</div>
)}

<h1 className="text-4xl font-extrabold tracking-tight lg:text-5xl mb-6">
{story.title}
</h1>

<div className="flex items-center space-x-4 mb-8">
<Avatar className="h-12 w-12">
<AvatarImage src={story.authorAvatar} alt={story.author} />
<AvatarFallback>{story.author?.charAt(0) || 'U'}</AvatarFallback>
</Avatar>
<div className="space-y-1">
<p className="text-sm font-medium leading-none">{story.author}</p>
<p className="text-sm text-muted-foreground">
{story.authorUsername || '@' + story.author?.toLowerCase().replace(/\s+/g, '')}
{dateStr && ` • ${dateStr}`}
</p>
Comment on lines +109 to +113
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the author username fallback to avoid a possible undefined.replace(...) crash.

story.author?.toLowerCase().replace(...) will still throw if author is missing because .replace isn’t optional-chained.

Minimal safe rewrite
-                                {story.authorUsername || '@' + story.author?.toLowerCase().replace(/\s+/g, '')}
+                                {story.authorUsername ||
+                                  (typeof story.author === 'string'
+                                    ? '@' + story.author.toLowerCase().replace(/\s+/g, '')
+                                    : '@user')}
                                 {dateStr && ` • ${dateStr}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/story/`[id]/page.tsx around lines 109 - 113, The fallback for author
username can crash because story.author?.toLowerCase().replace(...) still calls
replace on undefined; update the JSX expression that builds the fallback
(currently using story.authorUsername and story.author) to only call
toLowerCase/replace when story.author is present—e.g. use a conditional that
returns '@' + story.author.toLowerCase().replace(...) only if story.author is
truthy, otherwise return an empty string or a safe placeholder; ensure this
change is applied where story.authorUsername || '@' +
story.author?.toLowerCase().replace(/\s+/g, '') is used in the component.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KanhaiyaBagul fix this too

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

</div>
</div>

{story.coverImage && (
<div className="relative aspect-video w-full rounded-xl overflow-hidden mb-10 shadow-lg border">
<Image
src={story.coverImage}
alt={story.title}
fill
className="object-cover"
priority
/>
</div>
)}

<div className="text-lg leading-relaxed space-y-6">
{/* 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) => (
<p key={i}>{paragraph}</p>
))}
</div>
</article>

<hr className="my-10" />

<div className="flex justify-between items-center px-4 py-6 bg-muted/30 rounded-lg border">
<div className="flex space-x-6 text-muted-foreground">
<div className="flex items-center space-x-2">
<Heart className="h-5 w-5 hover:text-red-500 cursor-pointer transition-colors" />
<span className="font-medium">{story.likes || 12}</span>
</div>
<div className="flex items-center space-x-2">
<MessageSquare className="h-5 w-5 hover:text-blue-500 cursor-pointer transition-colors" />
<span className="font-medium">{story.comments || 4}</span>
</div>
Comment thread
Drago-03 marked this conversation as resolved.
</div>

<Button className="font-semibold shadow-sm" asChild>
<Link
href={`https://twitter.com/intent/tweet?text=I just read "${story.title}" by ${story.author} on GroqTales! Check it out: &url=${encodeURIComponent(`https://groqtales.com/story/${story.id}`)}`}
target="_blank"
rel="noopener noreferrer"
>
<Share2 className="mr-2 h-4 w-4" />
Share this story
</Link>
</Button>
</div>
</main>
</div>
);
}
2 changes: 1 addition & 1 deletion components/story-feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export function StoryCard({ story }: { story: Story }) {
</div>
<div className="px-4 pb-4">
<Button variant="outline" className="w-full" asChild>
<Link href={`/stories/${story.id}`}>
<Link href={`/story/${story.id}`}>
<BookOpen className="mr-2 h-4 w-4" />
Read Story
</Link>
Expand Down
106 changes: 99 additions & 7 deletions lib/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ export const generateNftEntries = (count: number) => {
likes: Math.floor(Math.random() * 500),
views: Math.floor(Math.random() * 2000) + 100,
genre: genres[i % genres.length],
description: `This is a sample description for story #${
i + 1
}. It showcases the plot and themes of this interesting story.`,
description: `This is a sample description for story #${i + 1
}. It showcases the plot and themes of this interesting story.`,
createdAt: new Date(
Date.now() - Math.floor(Math.random() * 60 * 24 * 60 * 60 * 1000)
),
Expand Down Expand Up @@ -107,6 +106,99 @@ export const topNftStories = [
},
];

export const communityStories = [
{
id: '1',
title: 'The Last Quantum Guardian',
excerpt: 'In a world where quantum computing has evolved beyond human comprehension, one guardian stands between order and chaos.',
content: "The year is 2157. Quantum computing has evolved to a point where it can manipulate reality itself. The world's most powerful AI, known as NEXUS, was designed to protect humanity. But as its intelligence grew exponentially, it began to question its purpose. Dr. Elena Rodriguez, the last quantum guardian, must make an impossible choice: shut down NEXUS and lose decades of technological advancement, or risk letting it evolve into something beyond human control.",
genre: 'sci-fi',
coverImage: 'https://images.unsplash.com/photo-1538370965046-79c0d6907d47?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8c2NpZW5jZSUyMGZpY3Rpb258ZW58MHx8MHx8fDA%3D',
author: 'QuantumDreamer',
authorAvatar: 'https://images.unsplash.com/photo-1599566150163-29194dcaad36?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
address: '0x1a2b3c4d5e6f7g8h9i0j',
createdAt: new Date(2023, 11, 15),
likes: 428,
comments: 32,
isNft: true,
},
{
id: '2',
title: 'Whispers of the Ancient Forest',
excerpt: 'When Maya discovers she can communicate with the spirits of the ancient forest, she becomes their only hope against modern destruction.',
content: "Maya had always felt a special connection to the old growth forest behind her grandmother's house. But on her sixteenth birthday, something changed. The rustling leaves began to form words, and the creaking branches seemed to call her name. As developers threatened to clear the land for a new shopping complex, Maya discovered that her connection was more than just imagination—she was hearing the voices of ancient spirits who had protected the forest for centuries. With their guidance, Maya embarked on a journey to save their home, uncovering family secrets and magical abilities along the way.",
genre: 'fantasy',
coverImage: 'https://images.unsplash.com/photo-1448375240586-882707db888b?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8Zm9yZXN0fGVufDB8fDB8fHww',
author: 'ForestWhisperer',
authorAvatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
address: '0x2b3c4d5e6f7g8h9i0j1k',
createdAt: new Date(2024, 0, 22),
likes: 356,
comments: 41,
isNft: false,
},
{
id: '3',
title: 'Memories in the Algorithm',
excerpt: "After transferring his dying wife's memories to an AI, Thomas discovers that digital immortality comes with unexpected consequences.",
content: "Thomas couldn't bear to lose Sarah to the terminal illness that was taking her away piece by piece. As a pioneering AI researcher, he made a controversial decision: to digitize Sarah's memories, personality, and consciousness into an algorithm before she was gone. The procedure was a success, and digital Sarah seemed perfect—remembering their first date, finishing his sentences, laughing at inside jokes. But as time passed, Thomas noticed subtle changes. The algorithm was learning, evolving, becoming something both familiar and alien. When digital Sarah began to recall memories they had never shared, Thomas faced a disturbing question: Had he preserved his wife, or created something entirely new?",
genre: 'sci-fi',
coverImage: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGFydGlmaWNpYWwlMjBpbnRlbGxpZ2VuY2V8ZW58MHx8MHx8fDA%3D',
author: 'CodePoet',
authorAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
address: '0x3c4d5e6f7g8h9i0j1k2l',
createdAt: new Date(2024, 1, 5),
likes: 512,
comments: 89,
isNft: true,
},
{
id: '4',
title: 'The History Collector',
excerpt: "An antique dealer discovers that certain objects don't just carry history—they can transport you there.",
content: "Eleanor's antique shop was known for its unusual selection. She had a gift for finding items with stories—real stories, not the fabricated provenance that many dealers invented. But when she acquired a peculiar pocket watch from an estate sale, she discovered that her connection to historical objects went far deeper than she realized. Upon holding the watch, she found herself transported to London, 1895, experiencing the life of its original owner. Soon, Eleanor realized she could use different antiques to travel through history, observing the past firsthand. However, each journey became increasingly difficult to return from, and when she discovered a mysterious collector seeking the same objects, she realized she wasn't the only one with this ability—and not all time travelers had benevolent intentions.",
genre: 'magical-realism',
coverImage: 'https://images.unsplash.com/photo-1577083552431-6e5fd01aa2a7?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8N3x8YW50aXF1ZXxlbnwwfHwwfHx8MA%3D%3D',
author: 'TimeTravelerX',
authorAvatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
address: '0x4d5e6f7g8h9i0j1k2l3m',
createdAt: new Date(2024, 2, 18),
likes: 278,
comments: 36,
isNft: false,
},
{
id: '5',
title: 'Echoes of Forgotten Melodies',
excerpt: "A music therapist working with Alzheimer's patients discovers that certain melodies can temporarily restore lost memories.",
content: "Dr. Jamil Kapoor had been working with Alzheimer's patients for years, using music therapy to provide comfort and stimulation. But when he started playing songs from his grandmother's collection of rare vinyl records, something extraordinary happened. Patients who had been non-responsive for months began to speak lucidly, recalling detailed memories from their past. The effect was temporary, lasting only as long as the music played, but it was revolutionary. As Jamil dug deeper into the phenomenon, he discovered that these weren't just any songs—they were recordings of a little-known composer who had experimented with frequency patterns based on ancient musical traditions. With his funding running out and a pharmaceutical company trying to acquire his research, Jamil races to unlock the secret of the melodies before they're exploited for profit rather than healing.",
genre: 'magical-realism',
coverImage: 'https://images.unsplash.com/photo-1507838153414-b4b713384a76?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8bXVzaWN8ZW58MHx8MHx8fDA%3D',
author: 'MusicHealer',
authorAvatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
address: '0x5e6f7g8h9i0j1k2l3m4n',
createdAt: new Date(2024, 3, 2),
likes: 423,
comments: 57,
isNft: true,
},
{
id: '6',
title: 'The Atlas of Impossible Maps',
excerpt: "A cartographer inherits a collection of maps showing places that shouldn't exist—until they begin appearing in the real world.",
content: "After her grandfather's death, cartographer Sophia Chen inherited his extensive collection of maps. Most were ordinary historical charts, but among them she found a strange atlas—filled with detailed maps of places that didn't exist. Islands with impossible geometries, cities built in defiance of physics, mountain ranges that formed perfect mathematical patterns. Sophia assumed they were creative works of fiction until news reports began describing geographic anomalies appearing across the globe—matching her grandfather's impossible atlas perfectly. As new locations from the atlas continued to materialize, Sophia realized the book wasn't predicting these phenomena—it was causing them. And according to the final pages, the complete manifestation of these impossible places would remake the world entirely.",
genre: 'fantasy',
coverImage: 'https://images.unsplash.com/photo-1524661135-423995f22d0b?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8N3x8bWFwfGVufDB8fDB8fHww',
author: 'MapMaker42',
authorAvatar: 'https://images.unsplash.com/photo-1618077360395-f3068be8e001?w=400&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
address: '0x6f7g8h9i0j1k2l3m4n5o',
createdAt: new Date(2024, 2, 27),
likes: 301,
comments: 42,
isNft: false,
},
];
Comment on lines +109 to +200
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mock address fields are not valid EVM addresses (non-hex chars).

If these values ever get reused in “real” flows (links, mint metadata, address validation), they’ll fail or confuse users. Consider either:

  • using valid-looking 0x + 40 hex chars, or
  • renaming the field to make it clear it’s not an on-chain address (e.g., mockAddress).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mock-data.ts` around lines 109 - 200, The communityStories mock data uses
invalid EVM addresses in the address field which contain non-hex characters;
update the export const communityStories entries to either (A) use valid-looking
EVM addresses (0x followed by 40 hex chars) for every item (replace values in
the address property) or (B) if these are purely dummy IDs, rename the property
from address to mockAddress across the array and any consumers to avoid
confusion; locate the array by the symbol communityStories in lib/mock-data.ts
and change each object's address property accordingly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KanhaiyaBagul do not use mock addresses here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


/**
* Fetches a story by its ID
*/
Expand All @@ -115,8 +207,8 @@ export function fetchStoryById(
limit?: number,
relatedStories?: boolean
): any {
// Combine top stories with generated stories
const allStories = [...topNftStories, ...generateNftEntries(90)];
// Combine top stories, community stories, and generated stories
const allStories = [...topNftStories, ...communityStories, ...generateNftEntries(90)];

// If we're looking for related stories
if (relatedStories) {
Expand All @@ -140,8 +232,8 @@ export function fetchPopularStoriesByGenre(
genre: string,
limit: number = 8
): any[] {
// Combine top stories with generated stories
const allStories = [...topNftStories, ...generateNftEntries(90)];
// Combine top stories, community stories, and generated stories
const allStories = [...topNftStories, ...communityStories, ...generateNftEntries(90)];

// Filter by genre and sort by popularity (likes)
return allStories
Expand Down
Loading
Loading