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,51 +0,0 @@
|
|||||||
# 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
|
|
||||||
69
Dockerfile
69
Dockerfile
@@ -1,69 +0,0 @@
|
|||||||
# 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"]
|
|
||||||
54
Makefile
54
Makefile
@@ -1,54 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
status: "healthy",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
service: "markdown-editor",
|
|
||||||
},
|
|
||||||
{ status: 200 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
161
app/globals.css
161
app/globals.css
@@ -2,85 +2,50 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
.text-balance {
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 0 0% 3.9%;
|
--foreground: 222.2 84% 4.9%;
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 0 0% 3.9%;
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 0 0% 3.9%;
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
--primary: 0 0% 9%;
|
--primary: 221.2 83.2% 53.3%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--primary-foreground: 210 40% 98%;
|
||||||
--secondary: 0 0% 96.1%;
|
--secondary: 210 40% 96%;
|
||||||
--secondary-foreground: 0 0% 9%;
|
--secondary-foreground: 222.2 84% 4.9%;
|
||||||
--muted: 0 0% 96.1%;
|
--muted: 210 40% 96%;
|
||||||
--muted-foreground: 0 0% 45.1%;
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
--accent: 0 0% 96.1%;
|
--accent: 210 40% 96%;
|
||||||
--accent-foreground: 0 0% 9%;
|
--accent-foreground: 222.2 84% 4.9%;
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
--border: 0 0% 89.8%;
|
--border: 214.3 31.8% 91.4%;
|
||||||
--input: 0 0% 89.8%;
|
--input: 214.3 31.8% 91.4%;
|
||||||
--ring: 0 0% 3.9%;
|
--ring: 221.2 83.2% 53.3%;
|
||||||
--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;
|
--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 {
|
.dark {
|
||||||
--background: 0 0% 3.9%;
|
--background: 222.2 84% 4.9%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 210 40% 98%;
|
||||||
--card: 0 0% 3.9%;
|
--card: 222.2 84% 4.9%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 210 40% 98%;
|
||||||
--popover: 0 0% 3.9%;
|
--popover: 222.2 84% 4.9%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 210 40% 98%;
|
||||||
--primary: 0 0% 98%;
|
--primary: 217.2 91.2% 59.8%;
|
||||||
--primary-foreground: 0 0% 9%;
|
--primary-foreground: 222.2 84% 4.9%;
|
||||||
--secondary: 0 0% 14.9%;
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 210 40% 98%;
|
||||||
--muted: 0 0% 14.9%;
|
--muted: 217.2 32.6% 17.5%;
|
||||||
--muted-foreground: 0 0% 63.9%;
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
--accent: 0 0% 14.9%;
|
--accent: 217.2 32.6% 17.5%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 210 40% 98%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
--border: 0 0% 14.9%;
|
--border: 217.2 32.6% 17.5%;
|
||||||
--input: 0 0% 14.9%;
|
--input: 217.2 32.6% 17.5%;
|
||||||
--ring: 0 0% 83.1%;
|
--ring: 224.3 76.3% 94.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%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,3 +57,61 @@ body {
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom prose styles for markdown preview */
|
||||||
|
.prose {
|
||||||
|
@apply max-w-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h1 {
|
||||||
|
@apply text-3xl font-bold mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h2 {
|
||||||
|
@apply text-2xl font-semibold mb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h3 {
|
||||||
|
@apply text-xl font-semibold mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose p {
|
||||||
|
@apply mb-4 leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ul {
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ol {
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose li {
|
||||||
|
@apply mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose blockquote {
|
||||||
|
@apply border-l-4 pl-4 py-2 my-4 italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose code {
|
||||||
|
@apply px-1 py-0.5 rounded text-sm font-mono;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre {
|
||||||
|
@apply p-4 rounded-lg overflow-x-auto my-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose table {
|
||||||
|
@apply w-full border-collapse my-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose th,
|
||||||
|
.prose td {
|
||||||
|
@apply border px-4 py-2 text-left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose th {
|
||||||
|
@apply font-semibold;
|
||||||
|
}
|
||||||
|
|||||||
60
app/page.tsx
60
app/page.tsx
@@ -1,59 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { MarkdownEditor } from "@/components/markdown-editor"
|
|
||||||
|
|
||||||
const defaultMarkdown = `# 🚀 Welcome to Markdown Editor
|
import config from "../tailwind.config"
|
||||||
|
|
||||||
This is a **modern** markdown editor with *real-time* preview and a **colorful** interface!
|
export default function SyntheticV0PageForDeployment() {
|
||||||
|
return <config />
|
||||||
## ✨ 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 (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50">
|
|
||||||
<MarkdownEditor initialContent={defaultMarkdown} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
markdown-editor:
|
markdown-editor:
|
||||||
build:
|
build:
|
||||||
@@ -13,7 +11,7 @@ services:
|
|||||||
- NEXT_TELEMETRY_DISABLED=1
|
- NEXT_TELEMETRY_DISABLED=1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
// Remove the invalid outputFileTracingRoot option
|
||||||
experimental: {
|
experimental: {
|
||||||
outputFileTracingRoot: undefined,
|
// Add any valid experimental options here if needed
|
||||||
},
|
},
|
||||||
// Optimize for Docker
|
// Optimize for Docker
|
||||||
compress: true,
|
compress: true,
|
||||||
|
|||||||
96
nginx.conf
96
nginx.conf
@@ -1,96 +0,0 @@
|
|||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
upstream markdown-editor {
|
|
||||||
server markdown-editor:3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rate limiting
|
|
||||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
||||||
|
|
||||||
# Gzip compression
|
|
||||||
gzip on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_min_length 1024;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_types
|
|
||||||
text/plain
|
|
||||||
text/css
|
|
||||||
text/xml
|
|
||||||
text/javascript
|
|
||||||
application/json
|
|
||||||
application/javascript
|
|
||||||
application/xml+rss
|
|
||||||
application/atom+xml
|
|
||||||
image/svg+xml;
|
|
||||||
|
|
||||||
# Static files caching
|
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
proxy_pass http://markdown-editor;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API routes with rate limiting
|
|
||||||
location /api/ {
|
|
||||||
limit_req zone=api burst=20 nodelay;
|
|
||||||
proxy_pass http://markdown-editor;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main application
|
|
||||||
location / {
|
|
||||||
proxy_pass http://markdown-editor;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# WebSocket support (if needed)
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Health check endpoint
|
|
||||||
location /health {
|
|
||||||
access_log off;
|
|
||||||
return 200 "healthy\n";
|
|
||||||
add_header Content-Type text/plain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTPS configuration (uncomment and configure for production)
|
|
||||||
# server {
|
|
||||||
# listen 443 ssl http2;
|
|
||||||
# server_name your-domain.com;
|
|
||||||
#
|
|
||||||
# ssl_certificate /etc/nginx/ssl/cert.pem;
|
|
||||||
# ssl_certificate_key /etc/nginx/ssl/key.pem;
|
|
||||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
# ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
|
|
||||||
# ssl_prefer_server_ciphers off;
|
|
||||||
#
|
|
||||||
# # Same location blocks as above
|
|
||||||
# }
|
|
||||||
}
|
|
||||||
16
package.json
16
package.json
@@ -9,16 +9,17 @@
|
|||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react-syntax-highlighter": "^15.5.11",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
|
"prism-react-renderer": "^2.3.1",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
"react-markdown": "latest",
|
"react-markdown": "^9.0.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-gfm": "latest",
|
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
@@ -26,7 +27,12 @@
|
|||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.0.0",
|
||||||
|
"eslint-config-next": "15.2.4",
|
||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
|
"tailwindcss": "^3.3.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.12.1"
|
||||||
}
|
}
|
||||||
3587
pnpm-lock.yaml
generated
3587
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,96 +1,81 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss"
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./pages/**/*.{ts,tsx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{ts,tsx}",
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./app/**/*.{ts,tsx}",
|
||||||
"*.{js,ts,jsx,tsx,mdx}"
|
"./src/**/*.{ts,tsx}",
|
||||||
|
"*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
|
prefix: "",
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
container: {
|
||||||
colors: {
|
center: true,
|
||||||
background: 'hsl(var(--background))',
|
padding: "2rem",
|
||||||
foreground: 'hsl(var(--foreground))',
|
screens: {
|
||||||
card: {
|
"2xl": "1400px",
|
||||||
DEFAULT: 'hsl(var(--card))',
|
},
|
||||||
foreground: 'hsl(var(--card-foreground))'
|
},
|
||||||
},
|
extend: {
|
||||||
popover: {
|
colors: {
|
||||||
DEFAULT: 'hsl(var(--popover))',
|
border: "hsl(var(--border))",
|
||||||
foreground: 'hsl(var(--popover-foreground))'
|
input: "hsl(var(--input))",
|
||||||
},
|
ring: "hsl(var(--ring))",
|
||||||
primary: {
|
background: "hsl(var(--background))",
|
||||||
DEFAULT: 'hsl(var(--primary))',
|
foreground: "hsl(var(--foreground))",
|
||||||
foreground: 'hsl(var(--primary-foreground))'
|
primary: {
|
||||||
},
|
DEFAULT: "hsl(var(--primary))",
|
||||||
secondary: {
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
DEFAULT: 'hsl(var(--secondary))',
|
},
|
||||||
foreground: 'hsl(var(--secondary-foreground))'
|
secondary: {
|
||||||
},
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
muted: {
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
DEFAULT: 'hsl(var(--muted))',
|
},
|
||||||
foreground: 'hsl(var(--muted-foreground))'
|
destructive: {
|
||||||
},
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
accent: {
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
DEFAULT: 'hsl(var(--accent))',
|
},
|
||||||
foreground: 'hsl(var(--accent-foreground))'
|
muted: {
|
||||||
},
|
DEFAULT: "hsl(var(--muted))",
|
||||||
destructive: {
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
DEFAULT: 'hsl(var(--destructive))',
|
},
|
||||||
foreground: 'hsl(var(--destructive-foreground))'
|
accent: {
|
||||||
},
|
DEFAULT: "hsl(var(--accent))",
|
||||||
border: 'hsl(var(--border))',
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
input: 'hsl(var(--input))',
|
},
|
||||||
ring: 'hsl(var(--ring))',
|
popover: {
|
||||||
chart: {
|
DEFAULT: "hsl(var(--popover))",
|
||||||
'1': 'hsl(var(--chart-1))',
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
'2': 'hsl(var(--chart-2))',
|
},
|
||||||
'3': 'hsl(var(--chart-3))',
|
card: {
|
||||||
'4': 'hsl(var(--chart-4))',
|
DEFAULT: "hsl(var(--card))",
|
||||||
'5': 'hsl(var(--chart-5))'
|
foreground: "hsl(var(--card-foreground))",
|
||||||
},
|
},
|
||||||
sidebar: {
|
},
|
||||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
borderRadius: {
|
||||||
foreground: 'hsl(var(--sidebar-foreground))',
|
lg: "var(--radius)",
|
||||||
primary: 'hsl(var(--sidebar-primary))',
|
md: "calc(var(--radius) - 2px)",
|
||||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
sm: "calc(var(--radius) - 4px)",
|
||||||
accent: 'hsl(var(--sidebar-accent))',
|
},
|
||||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
keyframes: {
|
||||||
border: 'hsl(var(--sidebar-border))',
|
"accordion-down": {
|
||||||
ring: 'hsl(var(--sidebar-ring))'
|
from: { height: "0" },
|
||||||
}
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
},
|
},
|
||||||
borderRadius: {
|
"accordion-up": {
|
||||||
lg: 'var(--radius)',
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
md: 'calc(var(--radius) - 2px)',
|
to: { height: "0" },
|
||||||
sm: 'calc(var(--radius) - 4px)'
|
},
|
||||||
},
|
},
|
||||||
keyframes: {
|
animation: {
|
||||||
'accordion-down': {
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
from: {
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
height: '0'
|
},
|
||||||
},
|
},
|
||||||
to: {
|
|
||||||
height: 'var(--radix-accordion-content-height)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'accordion-up': {
|
|
||||||
from: {
|
|
||||||
height: 'var(--radix-accordion-content-height)'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
height: '0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [require("tailwindcss-animate")],
|
||||||
};
|
}
|
||||||
export default config;
|
|
||||||
|
export default config
|
||||||
|
|||||||
Reference in New Issue
Block a user