Reputation: 2880
The component should display the code.
Failed to read file
I have a server action that reads a static file ( component file ) that's exists in components/vibrant/component-name.tsx
:
"use server"
import { promises as fs } from "fs"
import path from "path"
import { z } from "zod"
// Define the response type
type CodeResponse = {
content?: string
error?: string
details?: string
}
// Validation schema
const fileSchema = z.object({
fileName: z.string().min(1),
})
export async function getFileContent(fileName: string): Promise<CodeResponse> {
// Validate input
try {
fileSchema.parse({ fileName })
} catch {
return {
error: "File parameter is required",
}
}
try {
// Use path.join for safe path concatenation
const filePath = path.join(process.cwd(), "components", "vibrant", fileName)
const content = await fs.readFile(filePath, "utf8")
return { content }
} catch (error) {
console.error("Error reading file:", error)
const errorMessage =
error instanceof Error ? error.message : "Unknown error"
return {
error: "Failed to read file",
details: errorMessage,
}
}
}
I call this function from a client component:
"use client"
import { getFileContent } from "@/app/actions/file"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { Check, Copy } from "lucide-react"
import { useEffect, useState } from "react"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"
type Props = {
source: string
language?: string
}
export const CodeBlock = ({ source, language = "typescript" }: Props) => {
const [code, setCode] = useState("")
const [error, setError] = useState("")
const [copied, setCopied] = useState(false)
const [isExpanded, setIsExpanded] = useState(false)
useEffect(() => {
const fetchCode = async () => {
const result = await getFileContent(source)
if (result.error) {
setError(result.error)
setCode("")
return
}
if (result.content) {
setCode(result.content)
setError("")
}
}
fetchCode()
}, [source])
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(code)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error("Failed to copy text: ", err)
}
}
if (error) {
return <div className="p-4 bg-red-50 text-red-600 rounded-lg">{error}</div>
}
return (
<div className="relative w-full">
<Button
size="icon"
onClick={copyToClipboard}
className={cn(
"absolute top-4 right-6 p-2 bg-white hover:bg-gray-100 transition-colors rounded-full",
isExpanded && "right-2"
)}
aria-label="Copy code"
>
{copied ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4 text-black" />
)}
</Button>
<SyntaxHighlighter
language={language}
style={oneDark}
className={cn("w-full", isExpanded ? "h-full" : "h-[480px]")}
>
{code}
</SyntaxHighlighter>
<div className="absolute bottom-0 left-0 right-4 flex justify-center pb-2">
<div
className={cn(
"backdrop-blur-sm bg-transparent p-1 w-full h-16 flex items-center justify-center",
isExpanded && "backdrop-blur-none"
)}
>
<Button
onClick={() => setIsExpanded(!isExpanded)}
className="bg-white hover:bg-gray-100 text-black rounded-full"
size="sm"
>
{isExpanded ? "Show Less" : "Show All"}
</Button>
</div>
</div>
</div>
)
}
In the local environment, it works with dev and prod commands:
However when I deployed the project, the fetch fails:
The full code is available on GitHub.
Upvotes: 0
Views: 33
Reputation: 1
I think this behavior is due to how Next.js handles static assets. Next.js removes static files from other directories and serves them through the /public
folder during the build process. So, when you're reading a file like ./assets/example.tsx
using fs
, Next.js essentially moves it into the /public
folder at build time.
To ensure it works, you should place these static files in the public
folder before the build process. This will allow Next.js to handle them correctly in both development and production.
Upvotes: 0