diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..94305bb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Next.js +.next/ +out/ + +# Production +build + +# Misc +.DS_Store +*.tsbuildinfo + +# Debug +*.log + +# Local env files +.env*.local +.env + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts + +# IDE +.vscode +.idea + +# Git +.git +.gitignore +README.md + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Testing +coverage +.nyc_output + +# Linting +.eslintcache diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aab6617 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,69 @@ +# Use the official Node.js 18 Alpine image as base +FROM node:18-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN \ + if [ -f yarn.lock ]; then yarn run build; \ + elif [ -f package-lock.json ]; then npm run build; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +# Uncomment the following line in case you want to disable telemetry during runtime. +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +# set hostname to localhost +ENV HOSTNAME "0.0.0.0" + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/next-config-js/output +CMD ["node", "server.js"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3ef133c --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# Makefile for Markdown Editor Docker operations + +.PHONY: help build run stop clean logs shell health + +# Default target +help: + @echo "Available commands:" + @echo " build - Build the Docker image" + @echo " run - Run the application with docker-compose" + @echo " run-prod - Run with nginx reverse proxy" + @echo " stop - Stop all containers" + @echo " clean - Remove containers and images" + @echo " logs - Show application logs" + @echo " shell - Open shell in the app container" + @echo " health - Check application health" + +# Build the Docker image +build: + docker-compose build --no-cache + +# Run the application (development) +run: + docker-compose up -d markdown-editor + @echo "Application is running at http://localhost:3000" + +# Run with production setup (nginx + app) +run-prod: + docker-compose --profile production up -d + @echo "Application is running at http://localhost (port 80)" + +# Stop all containers +stop: + docker-compose down + +# Clean up containers and images +clean: + docker-compose down --rmi all --volumes --remove-orphans + docker system prune -f + +# Show logs +logs: + docker-compose logs -f markdown-editor + +# Open shell in the app container +shell: + docker-compose exec markdown-editor sh + +# Check application health +health: + @echo "Checking application health..." + @curl -f http://localhost:3000/api/health || echo "Health check failed" + +# Quick development cycle +dev: stop build run logs diff --git a/app/api/health/route.ts b/app/api/health/route.ts new file mode 100644 index 0000000..d223eeb --- /dev/null +++ b/app/api/health/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server" + +export async function GET() { + return NextResponse.json( + { + status: "healthy", + timestamp: new Date().toISOString(), + service: "markdown-editor", + }, + { status: 200 }, + ) +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..ac68442 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,94 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: Arial, Helvetica, sans-serif; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..17b2ce8 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from 'next' +import './globals.css' + +export const metadata: Metadata = { + title: 'v0 App', + description: 'Created with v0', + generator: 'v0.dev', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + {children} + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..7e3b74d --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,59 @@ +"use client" +import { MarkdownEditor } from "@/components/markdown-editor" + +const defaultMarkdown = `# 🚀 Welcome to Markdown Editor + +This is a **modern** markdown editor with *real-time* preview and a **colorful** interface! + +## ✨ Features + +- 🎨 Beautiful split-screen editing +- ⚡ Live preview with instant updates +- 🛠️ Rich formatting toolbar +- 🌈 Modern, colorful UI + +### 💻 Code Example + +\`\`\`javascript +function createAwesome() { + console.log("Building something amazing! 🎉"); + return "success"; +} +\`\`\` + +### 📝 Lists & More + +1. **First priority** - Get things done +2. **Second priority** - Make it beautiful +3. **Third priority** - Share with the world + +- 🎯 Bullet points with style +- 🚀 Another awesome point + - 💡 Nested brilliance + - ⭐ Even more nested goodness + +### 🔗 Links and Media + +[🌟 Visit our GitHub](https://github.com) for more amazing projects! + +> 💬 This is a beautiful blockquote with some **bold** text and *italic* styling. + +--- + +### 📊 Data Table + +| Feature | Status | Priority | +|---------|--------|----------| +| Editor | ✅ Complete | High | +| Preview | ✅ Complete | High | +| Export | 🚧 In Progress | Medium | + +Happy writing! ✨🎉` + +export default function Home() { + return ( +
+ +
+ ) +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..d9ef0ae --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/components/markdown-editor.tsx b/components/markdown-editor.tsx new file mode 100644 index 0000000..59db61d --- /dev/null +++ b/components/markdown-editor.tsx @@ -0,0 +1,98 @@ +"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(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 ( +
+ {/* Header */} +
+
+
+
+ + +
+
+

+ Markdown Editor +

+

Write • Preview • Create

+
+
+
+
+ + {/* Toolbar */} +
+ +
+ + {/* Editor */} +
+ {/* Input Panel */} +
+
+
+ +
+ Editor +
+
+ Live +
+
+ +
+ + {/* Preview Panel */} +
+
+
+ +
+ Preview +
+
+ Real-time +
+
+ +
+
+
+ ) +} diff --git a/components/markdown-input.tsx b/components/markdown-input.tsx new file mode 100644 index 0000000..3d7f0c2 --- /dev/null +++ b/components/markdown-input.tsx @@ -0,0 +1,65 @@ +"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(null) + + useEffect(() => { + onTextareaRef(textareaRef.current) + }, [onTextareaRef]) + + const handleKeyDown = (e: React.KeyboardEvent) => { + 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 ( +
+