Skip to content

Commit

Permalink
feat(*): final app
Browse files Browse the repository at this point in the history
  • Loading branch information
XynoxTheDev committed Nov 8, 2024
1 parent 0686518 commit 64e06bd
Show file tree
Hide file tree
Showing 5 changed files with 597 additions and 98 deletions.
233 changes: 233 additions & 0 deletions blog-markdown-generator/app/blog-markdown-generator.tsx
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 ? `![Blog Cover Image](${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>
)
}
104 changes: 6 additions & 98 deletions blog-markdown-generator/app/page.tsx
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>
)
}
Loading

0 comments on commit 64e06bd

Please sign in to comment.