mirror of
https://github.com/AndersPier/v0-v0app.git
synced 2025-10-27 10:06:52 +00:00
fix: resolve Docker build failures and configuration issues
Add missing dependencies, fix Next.js config, update Docker Compose, and configure Tailwind CSS. #VERCEL_SKIP Co-authored-by: Anders Lehmann Pier <3219386+AndersPier@users.noreply.github.com>
This commit is contained in:
@@ -1,98 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useCallback } from "react"
|
||||
import { MarkdownToolbar } from "./markdown-toolbar"
|
||||
import { MarkdownInput } from "./markdown-input"
|
||||
import { MarkdownPreview } from "./markdown-preview"
|
||||
import { FileText, Eye, Edit3, Sparkles } from "lucide-react"
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
initialContent?: string
|
||||
}
|
||||
|
||||
export function MarkdownEditor({ initialContent = "" }: MarkdownEditorProps) {
|
||||
const [content, setContent] = useState(initialContent)
|
||||
const [textareaRef, setTextareaRef] = useState<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const insertText = useCallback(
|
||||
(before: string, after = "", placeholder = "") => {
|
||||
if (!textareaRef) return
|
||||
|
||||
const start = textareaRef.selectionStart
|
||||
const end = textareaRef.selectionEnd
|
||||
const selectedText = content.substring(start, end)
|
||||
const textToInsert = selectedText || placeholder
|
||||
const newText = content.substring(0, start) + before + textToInsert + after + content.substring(end)
|
||||
|
||||
setContent(newText)
|
||||
|
||||
// Set cursor position after insertion
|
||||
setTimeout(() => {
|
||||
const newCursorPos = start + before.length + textToInsert.length
|
||||
textareaRef.setSelectionRange(newCursorPos, newCursorPos)
|
||||
textareaRef.focus()
|
||||
}, 0)
|
||||
},
|
||||
[content, textareaRef],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-white/80 backdrop-blur-xl supports-[backdrop-filter]:bg-white/60 shadow-sm">
|
||||
<div className="flex h-16 items-center px-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<FileText className="h-7 w-7 text-blue-600" />
|
||||
<Sparkles className="h-3 w-3 text-yellow-500 absolute -top-1 -right-1" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 bg-clip-text text-transparent">
|
||||
Markdown Editor
|
||||
</h1>
|
||||
<p className="text-xs text-muted-foreground">Write • Preview • Create</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="border-b bg-gradient-to-r from-blue-50/50 via-purple-50/50 to-pink-50/50 shadow-sm">
|
||||
<MarkdownToolbar onInsertText={insertText} />
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Input Panel */}
|
||||
<div className="flex-1 flex flex-col border-r border-gray-200">
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-gradient-to-r from-green-50 to-emerald-50 border-b">
|
||||
<div className="p-1 rounded-full bg-green-100">
|
||||
<Edit3 className="h-4 w-4 text-green-600" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-green-700">Editor</span>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-green-400 animate-pulse"></div>
|
||||
<span className="text-xs text-green-600">Live</span>
|
||||
</div>
|
||||
</div>
|
||||
<MarkdownInput content={content} onChange={setContent} onTextareaRef={setTextareaRef} />
|
||||
</div>
|
||||
|
||||
{/* Preview Panel */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-gradient-to-r from-blue-50 to-indigo-50 border-b">
|
||||
<div className="p-1 rounded-full bg-blue-100">
|
||||
<Eye className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-blue-700">Preview</span>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-blue-400 animate-pulse"></div>
|
||||
<span className="text-xs text-blue-600">Real-time</span>
|
||||
</div>
|
||||
</div>
|
||||
<MarkdownPreview content={content} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
interface MarkdownInputProps {
|
||||
content: string
|
||||
onChange: (content: string) => void
|
||||
onTextareaRef: (ref: HTMLTextAreaElement | null) => void
|
||||
}
|
||||
|
||||
export function MarkdownInput({ content, onChange, onTextareaRef }: MarkdownInputProps) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
onTextareaRef(textareaRef.current)
|
||||
}, [onTextareaRef])
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault()
|
||||
const start = e.currentTarget.selectionStart
|
||||
const end = e.currentTarget.selectionEnd
|
||||
const newContent = content.substring(0, start) + " " + content.substring(end)
|
||||
onChange(newContent)
|
||||
|
||||
setTimeout(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.setSelectionRange(start + 2, start + 2)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 relative bg-gradient-to-br from-white to-gray-50/50">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={content}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-full h-full p-6 resize-none border-0 outline-none bg-transparent font-mono text-sm leading-relaxed
|
||||
placeholder:text-gray-400 focus:placeholder:text-gray-300 transition-all duration-200
|
||||
selection:bg-blue-100 selection:text-blue-900"
|
||||
placeholder="✨ Start writing your markdown here...
|
||||
|
||||
Try typing:
|
||||
# My Amazing Title
|
||||
**Bold text** and *italic text*
|
||||
- List items
|
||||
> Blockquotes
|
||||
```code blocks```"
|
||||
spellCheck={false}
|
||||
/>
|
||||
{/* Subtle grid pattern overlay */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none opacity-[0.02]"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle at 1px 1px, rgba(0,0,0,0.15) 1px, transparent 0)`,
|
||||
backgroundSize: "20px 20px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import remarkGfm from "remark-gfm"
|
||||
|
||||
interface MarkdownPreviewProps {
|
||||
content: string
|
||||
}
|
||||
|
||||
export function MarkdownPreview({ content }: MarkdownPreviewProps) {
|
||||
return (
|
||||
<div className="flex-1 overflow-auto bg-gradient-to-br from-white to-blue-50/30">
|
||||
<div className="p-6 prose prose-slate dark:prose-invert max-w-none">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 bg-clip-text text-transparent mb-4">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-2xl font-semibold text-gray-800 mb-3 pb-2 border-b-2 border-gradient-to-r from-blue-200 to-purple-200">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => <h3 className="text-xl font-semibold text-gray-700 mb-2">{children}</h3>,
|
||||
p: ({ children }) => <p className="text-gray-700 leading-relaxed mb-4">{children}</p>,
|
||||
strong: ({ children }) => <strong className="font-bold text-blue-700">{children}</strong>,
|
||||
em: ({ children }) => <em className="italic text-purple-600">{children}</em>,
|
||||
code({ inline, className, children, ...props }) {
|
||||
if (inline) {
|
||||
return (
|
||||
<code
|
||||
className="bg-gradient-to-r from-pink-100 to-purple-100 text-purple-800 px-2 py-1 rounded-md text-sm font-mono border border-purple-200"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
|
||||
// block code
|
||||
return (
|
||||
<div className="relative my-6 rounded-xl overflow-hidden shadow-lg">
|
||||
<div className="bg-gradient-to-r from-gray-800 to-gray-900 px-4 py-2 flex items-center gap-2">
|
||||
<div className="flex gap-1">
|
||||
<div className="w-3 h-3 rounded-full bg-red-400"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-400"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-green-400"></div>
|
||||
</div>
|
||||
<span className="text-gray-300 text-xs font-mono ml-2">code</span>
|
||||
</div>
|
||||
<pre className="bg-gradient-to-br from-gray-900 to-black p-4 overflow-x-auto text-sm text-gray-100 font-mono">
|
||||
<code {...props}>{children}</code>
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-gradient-to-b from-blue-400 to-purple-400 bg-gradient-to-r from-blue-50 to-purple-50 pl-4 py-2 my-4 rounded-r-lg">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
ul: ({ children }) => <ul className="list-none space-y-2 my-4">{children}</ul>,
|
||||
ol: ({ children }) => <ol className="list-none space-y-2 my-4 counter-reset-list">{children}</ol>,
|
||||
li: ({ children, ...props }) => {
|
||||
const isOrdered = props.node?.parent?.tagName === "ol"
|
||||
return (
|
||||
<li className={`flex items-start gap-3 ${isOrdered ? "counter-increment-list" : ""}`}>
|
||||
{isOrdered ? (
|
||||
<span className="flex-shrink-0 w-6 h-6 bg-gradient-to-r from-blue-500 to-purple-500 text-white text-xs font-bold rounded-full flex items-center justify-center counter-content">
|
||||
{/* Counter will be handled by CSS */}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex-shrink-0 w-2 h-2 bg-gradient-to-r from-pink-400 to-purple-400 rounded-full mt-2"></span>
|
||||
)}
|
||||
<div className="flex-1">{children}</div>
|
||||
</li>
|
||||
)
|
||||
},
|
||||
a: ({ children, href }) => (
|
||||
<a
|
||||
href={href}
|
||||
className="text-blue-600 hover:text-purple-600 underline decoration-2 underline-offset-2 hover:decoration-purple-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
table({ children }) {
|
||||
return (
|
||||
<div className="overflow-x-auto my-6 rounded-lg shadow-lg">
|
||||
<table className="min-w-full border-collapse bg-white">{children}</table>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
th({ children }) {
|
||||
return (
|
||||
<th className="border border-gray-200 px-4 py-3 bg-gradient-to-r from-blue-50 to-purple-50 font-semibold text-left text-gray-800">
|
||||
{children}
|
||||
</th>
|
||||
)
|
||||
},
|
||||
td({ children }) {
|
||||
return (
|
||||
<td className="border border-gray-200 px-4 py-3 text-gray-700 hover:bg-gray-50 transition-colors duration-150">
|
||||
{children}
|
||||
</td>
|
||||
)
|
||||
},
|
||||
hr: () => (
|
||||
<hr className="my-8 border-0 h-1 bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 rounded-full" />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{content || (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<div className="text-4xl mb-4">✨</div>
|
||||
<p className="text-lg">Start typing to see the magic happen...</p>
|
||||
<p className="text-sm mt-2">Your markdown will appear here in real-time!</p>
|
||||
</div>
|
||||
)}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Strikethrough,
|
||||
Code,
|
||||
Link,
|
||||
ImageIcon,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Minus,
|
||||
Table,
|
||||
} from "lucide-react"
|
||||
|
||||
interface MarkdownToolbarProps {
|
||||
onInsertText: (before: string, after?: string, placeholder?: string) => void
|
||||
}
|
||||
|
||||
export function MarkdownToolbar({ onInsertText }: MarkdownToolbarProps) {
|
||||
const toolbarItems = [
|
||||
{
|
||||
group: "text",
|
||||
color: "from-red-500 to-pink-500",
|
||||
items: [
|
||||
{ icon: Bold, label: "Bold", action: () => onInsertText("**", "**", "bold text") },
|
||||
{ icon: Italic, label: "Italic", action: () => onInsertText("*", "*", "italic text") },
|
||||
{ icon: Strikethrough, label: "Strikethrough", action: () => onInsertText("~~", "~~", "strikethrough text") },
|
||||
{ icon: Code, label: "Inline Code", action: () => onInsertText("`", "`", "code") },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "headings",
|
||||
color: "from-blue-500 to-cyan-500",
|
||||
items: [
|
||||
{ icon: Heading1, label: "Heading 1", action: () => onInsertText("# ", "", "Heading 1") },
|
||||
{ icon: Heading2, label: "Heading 2", action: () => onInsertText("## ", "", "Heading 2") },
|
||||
{ icon: Heading3, label: "Heading 3", action: () => onInsertText("### ", "", "Heading 3") },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "lists",
|
||||
color: "from-green-500 to-emerald-500",
|
||||
items: [
|
||||
{ icon: List, label: "Bullet List", action: () => onInsertText("- ", "", "List item") },
|
||||
{ icon: ListOrdered, label: "Numbered List", action: () => onInsertText("1. ", "", "List item") },
|
||||
{ icon: Quote, label: "Quote", action: () => onInsertText("> ", "", "Quote text") },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "media",
|
||||
color: "from-purple-500 to-indigo-500",
|
||||
items: [
|
||||
{ icon: Link, label: "Link", action: () => onInsertText("[", "](url)", "link text") },
|
||||
{ icon: ImageIcon, label: "Image", action: () => onInsertText("", "alt text") },
|
||||
{ icon: Minus, label: "Horizontal Rule", action: () => onInsertText("\n---\n") },
|
||||
{
|
||||
icon: Table,
|
||||
label: "Table",
|
||||
action: () => onInsertText("\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |\n"),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 p-3 flex-wrap">
|
||||
{toolbarItems.map((group, groupIndex) => (
|
||||
<div key={group.group} className="flex items-center gap-1">
|
||||
{group.items.map((item, itemIndex) => (
|
||||
<Button
|
||||
key={item.label}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={item.action}
|
||||
className={`h-9 w-9 p-0 rounded-xl transition-all duration-200 hover:scale-105 hover:shadow-lg group relative overflow-hidden
|
||||
${itemIndex === 0 ? `hover:bg-gradient-to-r hover:${group.color} hover:text-white` : "hover:bg-gray-100"}
|
||||
`}
|
||||
title={item.label}
|
||||
>
|
||||
<item.icon className="h-4 w-4 relative z-10 transition-transform group-hover:scale-110" />
|
||||
{itemIndex === 0 && (
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-r ${group.color} opacity-0 group-hover:opacity-100 transition-opacity duration-200`}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
{groupIndex < toolbarItems.length - 1 && (
|
||||
<Separator orientation="vertical" className="h-6 mx-2 bg-gradient-to-b from-gray-200 to-gray-300" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user