-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0686518
commit 64e06bd
Showing
5 changed files
with
597 additions
and
98 deletions.
There are no files selected for viewing
233 changes: 233 additions & 0 deletions
233
blog-markdown-generator/app/blog-markdown-generator.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
'use client' | ||
|
||
import { useState, useEffect } from 'react' | ||
import { Button } from "@/components/ui/button" | ||
import { Input } from "@/components/ui/input" | ||
import { Textarea } from "@/components/ui/textarea" | ||
import { Label } from "@/components/ui/label" | ||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" | ||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" | ||
import { Copy, Check } from 'lucide-react' | ||
import { useToast } from "../hooks/use-toast" | ||
|
||
export default function BlogMarkdownGenerator() { | ||
const [mounted, setMounted] = useState(false) | ||
const [blogData, setBlogData] = useState({ | ||
title: '', | ||
author: '', | ||
coverImage: '', | ||
introduction: '', | ||
sections: [{ title: '', content: '' }], | ||
conclusion: '', | ||
authorBio: '', | ||
twitterHandle: '', | ||
linkedinProfile: '', | ||
githubUsername: '', | ||
}) | ||
const [generatedMarkdown, setGeneratedMarkdown] = useState('') | ||
const [isCopied, setIsCopied] = useState(false) | ||
const { toast } = useToast() | ||
|
||
useEffect(() => { | ||
setMounted(true) | ||
}, []) | ||
|
||
if (!mounted) { | ||
return ( | ||
<Card className="w-full max-w-4xl mx-auto"> | ||
<CardHeader> | ||
<CardTitle>Loading...</CardTitle> | ||
</CardHeader> | ||
<CardContent> | ||
<div className="h-96 flex items-center justify-center"> | ||
<div className="animate-pulse">Loading editor...</div> | ||
</div> | ||
</CardContent> | ||
</Card> | ||
) | ||
} | ||
|
||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | ||
const { name, value } = e.target | ||
setBlogData(prev => ({ ...prev, [name]: value })) | ||
} | ||
|
||
const handleSectionChange = (index: number, field: 'title' | 'content', value: string) => { | ||
setBlogData(prev => ({ | ||
...prev, | ||
sections: prev.sections.map((section, i) => | ||
i === index ? { ...section, [field]: value } : section | ||
) | ||
})) | ||
} | ||
|
||
const addSection = () => { | ||
setBlogData(prev => ({ | ||
...prev, | ||
sections: [...prev.sections, { title: '', content: '' }] | ||
})) | ||
} | ||
|
||
const generateMarkdown = () => { | ||
let markdown = `# ${blogData.title}\n\n` | ||
markdown += `*By [${blogData.author}](https://your-website.com)*\n\n` | ||
markdown += blogData.coverImage ? `\n\n` : '' | ||
|
||
markdown += `## Table of Contents\n` | ||
markdown += `- [Introduction](#introduction)\n` | ||
blogData.sections.forEach((section, index) => { | ||
if (section.title) { | ||
markdown += `- [${section.title}](#section-${index + 1}-${section.title.toLowerCase().replace(/\s+/g, '-')})\n` | ||
} | ||
}) | ||
markdown += `- [Conclusion](#conclusion)\n\n` | ||
|
||
markdown += `## Introduction\n\n${blogData.introduction}\n\n` | ||
|
||
blogData.sections.forEach((section, index) => { | ||
if (section.title) { | ||
markdown += `## Section ${index + 1}: ${section.title}\n\n${section.content}\n\n` | ||
} | ||
}) | ||
|
||
markdown += `## Conclusion\n\n${blogData.conclusion}\n\n---\n\n` | ||
|
||
if (blogData.author || blogData.authorBio) { | ||
markdown += `### About the Author\n\n` | ||
markdown += `**${blogData.author}** ${blogData.authorBio}\n\n` | ||
} | ||
|
||
if (blogData.twitterHandle || blogData.linkedinProfile || blogData.githubUsername) { | ||
markdown += `Connect with me:\n` | ||
if (blogData.twitterHandle) markdown += `- [Twitter](https://twitter.com/${blogData.twitterHandle})\n` | ||
if (blogData.linkedinProfile) markdown += `- [LinkedIn](${blogData.linkedinProfile})\n` | ||
if (blogData.githubUsername) markdown += `- [GitHub](https://github.com/${blogData.githubUsername})\n` | ||
markdown += '\n' | ||
} | ||
|
||
markdown += `---\n\n` | ||
if (blogData.twitterHandle) { | ||
markdown += `*Did you find this blog post helpful? [Share it on Twitter](https://twitter.com/intent/tweet?text=Check%20out%20this%20amazing%20blog%20post%20by%20@${blogData.twitterHandle}:%20https://gist.github.com/your-gist-url)*\n\n` | ||
} | ||
markdown += `*For more content like this, [subscribe to my newsletter](https://your-newsletter-url.com).*` | ||
|
||
setGeneratedMarkdown(markdown) | ||
} | ||
|
||
const copyToClipboard = async () => { | ||
try { | ||
await navigator.clipboard.writeText(generatedMarkdown) | ||
setIsCopied(true) | ||
toast({ | ||
title: "Copied!", | ||
description: "Markdown has been copied to clipboard.", | ||
}) | ||
setTimeout(() => setIsCopied(false), 2000) | ||
} catch (err) { | ||
console.error('Failed to copy text: ', err) | ||
toast({ | ||
title: "Error", | ||
description: "Failed to copy. Please try again.", | ||
variant: "destructive", | ||
}) | ||
} | ||
} | ||
|
||
return ( | ||
<Card className="w-full max-w-4xl mx-auto"> | ||
<CardHeader> | ||
<CardTitle>Blog Markdown Generator</CardTitle> | ||
</CardHeader> | ||
<CardContent> | ||
<Tabs defaultValue="input" className="space-y-4"> | ||
<TabsList className="grid w-full grid-cols-2"> | ||
<TabsTrigger value="input">Input</TabsTrigger> | ||
<TabsTrigger value="preview">Preview</TabsTrigger> | ||
</TabsList> | ||
<TabsContent value="input"> | ||
<form className="space-y-4"> | ||
<div> | ||
<Label htmlFor="title">Blog Title</Label> | ||
<Input id="title" name="title" value={blogData.title} onChange={handleInputChange} /> | ||
</div> | ||
<div> | ||
<Label htmlFor="author">Author Name</Label> | ||
<Input id="author" name="author" value={blogData.author} onChange={handleInputChange} /> | ||
</div> | ||
<div> | ||
<Label htmlFor="coverImage">Cover Image URL</Label> | ||
<Input id="coverImage" name="coverImage" value={blogData.coverImage} onChange={handleInputChange} /> | ||
</div> | ||
<div> | ||
<Label htmlFor="introduction">Introduction</Label> | ||
<Textarea id="introduction" name="introduction" value={blogData.introduction} onChange={handleInputChange} /> | ||
</div> | ||
{blogData.sections.map((section, index) => ( | ||
<div key={index} className="space-y-2"> | ||
<Label htmlFor={`section-${index}-title`}>Section {index + 1} Title</Label> | ||
<Input | ||
id={`section-${index}-title`} | ||
value={section.title} | ||
onChange={(e) => handleSectionChange(index, 'title', e.target.value)} | ||
/> | ||
<Label htmlFor={`section-${index}-content`}>Section {index + 1} Content</Label> | ||
<Textarea | ||
id={`section-${index}-content`} | ||
value={section.content} | ||
onChange={(e) => handleSectionChange(index, 'content', e.target.value)} | ||
/> | ||
</div> | ||
))} | ||
<Button type="button" onClick={addSection}>Add Section</Button> | ||
<div> | ||
<Label htmlFor="conclusion">Conclusion</Label> | ||
<Textarea id="conclusion" name="conclusion" value={blogData.conclusion} onChange={handleInputChange} /> | ||
</div> | ||
<div> | ||
<Label htmlFor="authorBio">Author Bio</Label> | ||
<Textarea id="authorBio" name="authorBio" value={blogData.authorBio} onChange={handleInputChange} /> | ||
</div> | ||
<div> | ||
<Label htmlFor="twitterHandle">Twitter Handle</Label> | ||
<Input id="twitterHandle" name="twitterHandle" value={blogData.twitterHandle} onChange={handleInputChange} /> | ||
</div> | ||
<div> | ||
<Label htmlFor="linkedinProfile">LinkedIn Profile URL</Label> | ||
<Input id="linkedinProfile" name="linkedinProfile" value={blogData.linkedinProfile} onChange={handleInputChange} /> | ||
</div> | ||
<div> | ||
<Label htmlFor="githubUsername">GitHub Username</Label> | ||
<Input id="githubUsername" name="githubUsername" value={blogData.githubUsername} onChange={handleInputChange} /> | ||
</div> | ||
</form> | ||
</TabsContent> | ||
<TabsContent value="preview"> | ||
<div className="bg-muted p-4 rounded-md relative"> | ||
<pre className="whitespace-pre-wrap">{generatedMarkdown}</pre> | ||
{generatedMarkdown && ( | ||
<Button | ||
className="absolute top-2 right-2" | ||
size="icon" | ||
onClick={copyToClipboard} | ||
aria-label="Copy to clipboard" | ||
> | ||
{isCopied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />} | ||
</Button> | ||
)} | ||
</div> | ||
</TabsContent> | ||
</Tabs> | ||
</CardContent> | ||
<CardFooter className="flex justify-between"> | ||
<Button onClick={generateMarkdown}>Generate Markdown</Button> | ||
<Button | ||
onClick={copyToClipboard} | ||
disabled={!generatedMarkdown} | ||
className="ml-2" | ||
> | ||
{isCopied ? 'Copied!' : 'Copy Markdown'} | ||
</Button> | ||
</CardFooter> | ||
</Card> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,101 +1,9 @@ | ||
import Image from "next/image"; | ||
import BlogMarkdownGenerator from './blog-markdown-generator' | ||
|
||
export default function Home() { | ||
return ( | ||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]"> | ||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start"> | ||
<Image | ||
className="dark:invert" | ||
src="/next.svg" | ||
alt="Next.js logo" | ||
width={180} | ||
height={38} | ||
priority | ||
/> | ||
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]"> | ||
<li className="mb-2"> | ||
Get started by editing{" "} | ||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold"> | ||
app/page.tsx | ||
</code> | ||
. | ||
</li> | ||
<li>Save and see your changes instantly.</li> | ||
</ol> | ||
|
||
<div className="flex gap-4 items-center flex-col sm:flex-row"> | ||
<a | ||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5" | ||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
<Image | ||
className="dark:invert" | ||
src="/vercel.svg" | ||
alt="Vercel logomark" | ||
width={20} | ||
height={20} | ||
/> | ||
Deploy now | ||
</a> | ||
<a | ||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44" | ||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
Read our docs | ||
</a> | ||
</div> | ||
</main> | ||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center"> | ||
<a | ||
className="flex items-center gap-2 hover:underline hover:underline-offset-4" | ||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
<Image | ||
aria-hidden | ||
src="/file.svg" | ||
alt="File icon" | ||
width={16} | ||
height={16} | ||
/> | ||
Learn | ||
</a> | ||
<a | ||
className="flex items-center gap-2 hover:underline hover:underline-offset-4" | ||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
<Image | ||
aria-hidden | ||
src="/window.svg" | ||
alt="Window icon" | ||
width={16} | ||
height={16} | ||
/> | ||
Examples | ||
</a> | ||
<a | ||
className="flex items-center gap-2 hover:underline hover:underline-offset-4" | ||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
<Image | ||
aria-hidden | ||
src="/globe.svg" | ||
alt="Globe icon" | ||
width={16} | ||
height={16} | ||
/> | ||
Go to nextjs.org → | ||
</a> | ||
</footer> | ||
</div> | ||
); | ||
} | ||
<main className="container mx-auto p-4"> | ||
<BlogMarkdownGenerator /> | ||
</main> | ||
) | ||
} |
Oops, something went wrong.