my-textarea-app/
├── package.json
├── .env.local
├── next.config.js
├── tailwind.config.ts
├── tsconfig.json
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ └── texts/
│ │ │ └── route.ts
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── texts/
│ │ └── page.tsx
│ ├── components/
│ │ ├── TextEditor.tsx
│ │ └── TextList.tsx
│ ├── lib/
│ │ └── mongodb.ts
│ └── models/
│ └── Text.ts
{
"name": "textarea-mongodb-nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.0.3",
"react": "^18",
"react-dom": "^18",
"mongodb": "^6.3.0",
"mongoose": "^8.0.3"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"eslint": "^8",
"eslint-config-next": "14.0.3"
}
}MONGODB_URI=mongodb://localhost:27017/textarea-app
# Or use MongoDB Atlas:
# MONGODB_URI=mongodb+srv://username:[email protected]/textarea-app/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverComponentsExternalPackages: ['mongoose']
}
}
module.exports = nextConfigimport type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
}
export default config{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}@tailwind base;
@tailwind components;
@tailwind utilities;import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI!;
if (!MONGODB_URI) {
throw new Error('Please define the MONGODB_URI environment variable inside .env.local');
}
let cached = (global as any).mongoose;
if (!cached) {
cached = (global as any).mongoose = { conn: null, promise: null };
}
async function connectDB() {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
return mongoose;
});
}
try {
cached.conn = await cached.promise;
} catch (e) {
cached.promise = null;
throw e;
}
return cached.conn;
}
export default connectDB;import mongoose from 'mongoose';
export interface IText extends mongoose.Document {
content: string;
title?: string;
createdAt: Date;
updatedAt: Date;
}
const TextSchema = new mongoose.Schema({
content: {
type: String,
required: [true, 'Content is required'],
},
title: {
type: String,
default: 'Untitled',
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
export default mongoose.models.Text || mongoose.model<IText>('Text', TextSchema);import { NextRequest, NextResponse } from 'next/server';
import connectDB from '@/lib/mongodb';
import Text from '@/models/Text';
// GET - Fetch all texts
export async function GET() {
try {
await connectDB();
const texts = await Text.find({}).sort({ createdAt: -1 });
return NextResponse.json({ success: true, data: texts });
} catch (error) {
console.error('Error fetching texts:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch texts' },
{ status: 500 }
);
}
}
// POST - Create new text
export async function POST(request: NextRequest) {
try {
const { content, title } = await request.json();
if (!content) {
return NextResponse.json(
{ success: false, error: 'Content is required' },
{ status: 400 }
);
}
await connectDB();
const newText = await Text.create({
content,
title: title || 'Untitled',
});
return NextResponse.json(
{ success: true, data: newText, message: 'Text saved successfully' },
{ status: 201 }
);
} catch (error) {
console.error('Error creating text:', error);
return NextResponse.json(
{ success: false, error: 'Failed to save text' },
{ status: 500 }
);
}
}
// PUT - Update text
export async function PUT(request: NextRequest) {
try {
const { id, content, title } = await request.json();
if (!id || !content) {
return NextResponse.json(
{ success: false, error: 'ID and content are required' },
{ status: 400 }
);
}
await connectDB();
const updatedText = await Text.findByIdAndUpdate(
id,
{
content,
title: title || 'Untitled',
updatedAt: new Date()
},
{ new: true }
);
if (!updatedText) {
return NextResponse.json(
{ success: false, error: 'Text not found' },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
data: updatedText,
message: 'Text updated successfully',
});
} catch (error) {
console.error('Error updating text:', error);
return NextResponse.json(
{ success: false, error: 'Failed to update text' },
{ status: 500 }
);
}
}
// DELETE - Delete text
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json(
{ success: false, error: 'ID is required' },
{ status: 400 }
);
}
await connectDB();
const deletedText = await Text.findByIdAndDelete(id);
if (!deletedText) {
return NextResponse.json(
{ success: false, error: 'Text not found' },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
message: 'Text deleted successfully',
});
} catch (error) {
console.error('Error deleting text:', error);
return NextResponse.json(
{ success: false, error: 'Failed to delete text' },
{ status: 500 }
);
}
}'use client';
import { useState } from 'react';
interface TextEditorProps {
onSave: () => void;
}
export default function TextEditor({ onSave }: TextEditorProps) {
const [content, setContent] = useState('');
const [title, setTitle] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!content.trim()) {
setMessage('Please enter some content');
return;
}
setIsLoading(true);
try {
const response = await fetch('/api/texts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: content.trim(),
title: title.trim() || 'Untitled',
}),
});
const result = await response.json();
if (result.success) {
setMessage('Text saved successfully!');
setContent('');
setTitle('');
onSave(); // Refresh the list
} else {
setMessage(result.error || 'Failed to save text');
}
} catch (error) {
setMessage('Error saving text');
console.error('Error:', error);
} finally {
setIsLoading(false);
// Clear message after 3 seconds
setTimeout(() => setMessage(''), 3000);
}
};
const handleClear = () => {
setContent('');
setTitle('');
setMessage('');
};
return (
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 className="text-xl font-semibold text-gray-700 mb-4">Add New Text</h2>
{message && (
<div className={`mb-4 p-4 rounded-lg ${
message.includes('successfully')
? 'bg-green-100 text-green-700 border border-green-300'
: 'bg-red-100 text-red-700 border border-red-300'
}`}>
{message}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<input
type="text"
placeholder="Enter title (optional)"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={isLoading}
/>
</div>
<textarea
placeholder="Enter your text content here..."
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full h-40 p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-vertical"
required
disabled={isLoading}
/>
<div className="mt-4 flex gap-4">
<button
type="submit"
disabled={isLoading}
className="bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-6 py-2 rounded-lg transition duration-200"
>
{isLoading ? 'Saving...' : 'Save Text'}
</button>
<button
type="button"
onClick={handleClear}
disabled={isLoading}
className="bg-gray-500 hover:bg-gray-600 disabled:bg-gray-300 text-white px-6 py-2 rounded-lg transition duration-200"
>
Clear
</button>
</div>
</form>
</div>
);
}'use client';
import { useState, useEffect } from 'react';
interface Text {
_id: string;
title: string;
content: string;
createdAt: string;
updatedAt: string;
}
interface TextListProps {
refresh: boolean;
onRefreshComplete: () => void;
}
export default function TextList({ refresh, onRefreshComplete }: TextListProps) {
const [texts, setTexts] = useState<Text[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const fetchTexts = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/texts');
const result = await response.json();
if (result.success) {
setTexts(result.data);
setError('');
} else {
setError(result.error || 'Failed to fetch texts');
}
} catch (err) {
setError('Error fetching texts');
console.error('Error:', err);
} finally {
setIsLoading(false);
}
};
const deleteText = async (id: string) => {
if (!confirm('Are you sure you want to delete this text?')) {
return;
}
try {
const response = await fetch(`/api/texts?id=${id}`, {
method: 'DELETE',
});
const result = await response.json();
if (result.success) {
setTexts(texts.filter(text => text._id !== id));
} else {
setError(result.error || 'Failed to delete text');
}
} catch (err) {
setError('Error deleting text');
console.error('Error:', err);
}
};
useEffect(() => {
fetchTexts();
}, []);
useEffect(() => {
if (refresh) {
fetchTexts();
onRefreshComplete();
}
}, [refresh, onRefreshComplete]);
if (isLoading) {
return (
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex justify-center items-center h-32">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<span className="ml-2">Loading texts...</span>
</div>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-700">Saved Texts ({texts.length})</h2>
<button
onClick={fetchTexts}
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg transition duration-200"
>
Refresh
</button>
</div>
{error && (
<div className="mb-4 p-4 bg-red-100 text-red-700 border border-red-300 rounded-lg">
{error}
</div>
)}
{texts.length === 0 ? (
<div className="text-center text-gray-500 py-8">
No texts saved yet. Create your first text above!
</div>
) : (
<div className="space-y-4">
{texts.map((text) => (
<div key={text._id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-2">
<h3 className="font-semibold text-gray-800">{text.title}</h3>
<button
onClick={() => deleteText(text._id)}
className="text-red-500 hover:text-red-700 transition-colors"
title="Delete text"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
<p className="text-gray-600 whitespace-pre-wrap">{text.content}</p>
<div className="mt-3 text-sm text-gray-400">
Created: {new Date(text.createdAt).toLocaleString()}
{text.updatedAt !== text.createdAt && (
<span className="ml-4">
Updated: {new Date(text.updatedAt).toLocaleString()}
</span>
)}
</div>
</div>
))}
</div>
)}
</div>
);
}import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'Textarea to MongoDB',
description: 'Simple text content manager with MongoDB',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="bg-gray-100 min-h-screen">{children}</body>
</html>
);
}'use client';
import { useState } from 'react';
import TextEditor from '@/components/TextEditor';
import TextList from '@/components/TextList';
export default function Home() {
const [refreshList, setRefreshList] = useState(false);
const handleTextSaved = () => {
setRefreshList(true);
};
const handleRefreshComplete = () => {
setRefreshList(false);
};
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold text-gray-800 mb-8 text-center">
Text Content Manager
</h1>
<TextEditor onSave={handleTextSaved} />
<TextList refresh={refreshList} onRefreshComplete={handleRefreshComplete} />
</div>
</div>
);
}import { redirect } from 'next/navigation';
export default function TextsPage() {
redirect('/');
}- Create the project:
npx create-next-app@latest textarea-mongodb-app --typescript --tailwind --eslint --app --src-dir
cd textarea-mongodb-app- Install dependencies:
npm install mongodb mongoose-
Create all the files above in their respective locations
-
Set up MongoDB:
- Install MongoDB locally or use MongoDB Atlas
- Create the
.env.localfile with your MongoDB connection string
-
Run the development server:
npm run dev- Visit your app: Open http://localhost:3000 in your browser
- ✅ Create and save text content with optional titles
- ✅ View all saved texts with timestamps
- ✅ Delete individual texts
- ✅ Responsive design with Tailwind CSS
- ✅ Loading states and error handling
- ✅ TypeScript support
- ✅ MongoDB connection with Mongoose
- ✅ RESTful API endpoints
- ✅ Server-side rendering with Next.js 14
Your textarea content will be automatically saved to your MongoDB database!