Skip to content

Commit 64e06bd

Browse files
committed
feat(*): final app
1 parent 0686518 commit 64e06bd

File tree

5 files changed

+597
-98
lines changed

5 files changed

+597
-98
lines changed
+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
'use client'
2+
3+
import { useState, useEffect } from 'react'
4+
import { Button } from "@/components/ui/button"
5+
import { Input } from "@/components/ui/input"
6+
import { Textarea } from "@/components/ui/textarea"
7+
import { Label } from "@/components/ui/label"
8+
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
9+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
10+
import { Copy, Check } from 'lucide-react'
11+
import { useToast } from "../hooks/use-toast"
12+
13+
export default function BlogMarkdownGenerator() {
14+
const [mounted, setMounted] = useState(false)
15+
const [blogData, setBlogData] = useState({
16+
title: '',
17+
author: '',
18+
coverImage: '',
19+
introduction: '',
20+
sections: [{ title: '', content: '' }],
21+
conclusion: '',
22+
authorBio: '',
23+
twitterHandle: '',
24+
linkedinProfile: '',
25+
githubUsername: '',
26+
})
27+
const [generatedMarkdown, setGeneratedMarkdown] = useState('')
28+
const [isCopied, setIsCopied] = useState(false)
29+
const { toast } = useToast()
30+
31+
useEffect(() => {
32+
setMounted(true)
33+
}, [])
34+
35+
if (!mounted) {
36+
return (
37+
<Card className="w-full max-w-4xl mx-auto">
38+
<CardHeader>
39+
<CardTitle>Loading...</CardTitle>
40+
</CardHeader>
41+
<CardContent>
42+
<div className="h-96 flex items-center justify-center">
43+
<div className="animate-pulse">Loading editor...</div>
44+
</div>
45+
</CardContent>
46+
</Card>
47+
)
48+
}
49+
50+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
51+
const { name, value } = e.target
52+
setBlogData(prev => ({ ...prev, [name]: value }))
53+
}
54+
55+
const handleSectionChange = (index: number, field: 'title' | 'content', value: string) => {
56+
setBlogData(prev => ({
57+
...prev,
58+
sections: prev.sections.map((section, i) =>
59+
i === index ? { ...section, [field]: value } : section
60+
)
61+
}))
62+
}
63+
64+
const addSection = () => {
65+
setBlogData(prev => ({
66+
...prev,
67+
sections: [...prev.sections, { title: '', content: '' }]
68+
}))
69+
}
70+
71+
const generateMarkdown = () => {
72+
let markdown = `# ${blogData.title}\n\n`
73+
markdown += `*By [${blogData.author}](https://your-website.com)*\n\n`
74+
markdown += blogData.coverImage ? `![Blog Cover Image](${blogData.coverImage})\n\n` : ''
75+
76+
markdown += `## Table of Contents\n`
77+
markdown += `- [Introduction](#introduction)\n`
78+
blogData.sections.forEach((section, index) => {
79+
if (section.title) {
80+
markdown += `- [${section.title}](#section-${index + 1}-${section.title.toLowerCase().replace(/\s+/g, '-')})\n`
81+
}
82+
})
83+
markdown += `- [Conclusion](#conclusion)\n\n`
84+
85+
markdown += `## Introduction\n\n${blogData.introduction}\n\n`
86+
87+
blogData.sections.forEach((section, index) => {
88+
if (section.title) {
89+
markdown += `## Section ${index + 1}: ${section.title}\n\n${section.content}\n\n`
90+
}
91+
})
92+
93+
markdown += `## Conclusion\n\n${blogData.conclusion}\n\n---\n\n`
94+
95+
if (blogData.author || blogData.authorBio) {
96+
markdown += `### About the Author\n\n`
97+
markdown += `**${blogData.author}** ${blogData.authorBio}\n\n`
98+
}
99+
100+
if (blogData.twitterHandle || blogData.linkedinProfile || blogData.githubUsername) {
101+
markdown += `Connect with me:\n`
102+
if (blogData.twitterHandle) markdown += `- [Twitter](https://twitter.com/${blogData.twitterHandle})\n`
103+
if (blogData.linkedinProfile) markdown += `- [LinkedIn](${blogData.linkedinProfile})\n`
104+
if (blogData.githubUsername) markdown += `- [GitHub](https://github.com/${blogData.githubUsername})\n`
105+
markdown += '\n'
106+
}
107+
108+
markdown += `---\n\n`
109+
if (blogData.twitterHandle) {
110+
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`
111+
}
112+
markdown += `*For more content like this, [subscribe to my newsletter](https://your-newsletter-url.com).*`
113+
114+
setGeneratedMarkdown(markdown)
115+
}
116+
117+
const copyToClipboard = async () => {
118+
try {
119+
await navigator.clipboard.writeText(generatedMarkdown)
120+
setIsCopied(true)
121+
toast({
122+
title: "Copied!",
123+
description: "Markdown has been copied to clipboard.",
124+
})
125+
setTimeout(() => setIsCopied(false), 2000)
126+
} catch (err) {
127+
console.error('Failed to copy text: ', err)
128+
toast({
129+
title: "Error",
130+
description: "Failed to copy. Please try again.",
131+
variant: "destructive",
132+
})
133+
}
134+
}
135+
136+
return (
137+
<Card className="w-full max-w-4xl mx-auto">
138+
<CardHeader>
139+
<CardTitle>Blog Markdown Generator</CardTitle>
140+
</CardHeader>
141+
<CardContent>
142+
<Tabs defaultValue="input" className="space-y-4">
143+
<TabsList className="grid w-full grid-cols-2">
144+
<TabsTrigger value="input">Input</TabsTrigger>
145+
<TabsTrigger value="preview">Preview</TabsTrigger>
146+
</TabsList>
147+
<TabsContent value="input">
148+
<form className="space-y-4">
149+
<div>
150+
<Label htmlFor="title">Blog Title</Label>
151+
<Input id="title" name="title" value={blogData.title} onChange={handleInputChange} />
152+
</div>
153+
<div>
154+
<Label htmlFor="author">Author Name</Label>
155+
<Input id="author" name="author" value={blogData.author} onChange={handleInputChange} />
156+
</div>
157+
<div>
158+
<Label htmlFor="coverImage">Cover Image URL</Label>
159+
<Input id="coverImage" name="coverImage" value={blogData.coverImage} onChange={handleInputChange} />
160+
</div>
161+
<div>
162+
<Label htmlFor="introduction">Introduction</Label>
163+
<Textarea id="introduction" name="introduction" value={blogData.introduction} onChange={handleInputChange} />
164+
</div>
165+
{blogData.sections.map((section, index) => (
166+
<div key={index} className="space-y-2">
167+
<Label htmlFor={`section-${index}-title`}>Section {index + 1} Title</Label>
168+
<Input
169+
id={`section-${index}-title`}
170+
value={section.title}
171+
onChange={(e) => handleSectionChange(index, 'title', e.target.value)}
172+
/>
173+
<Label htmlFor={`section-${index}-content`}>Section {index + 1} Content</Label>
174+
<Textarea
175+
id={`section-${index}-content`}
176+
value={section.content}
177+
onChange={(e) => handleSectionChange(index, 'content', e.target.value)}
178+
/>
179+
</div>
180+
))}
181+
<Button type="button" onClick={addSection}>Add Section</Button>
182+
<div>
183+
<Label htmlFor="conclusion">Conclusion</Label>
184+
<Textarea id="conclusion" name="conclusion" value={blogData.conclusion} onChange={handleInputChange} />
185+
</div>
186+
<div>
187+
<Label htmlFor="authorBio">Author Bio</Label>
188+
<Textarea id="authorBio" name="authorBio" value={blogData.authorBio} onChange={handleInputChange} />
189+
</div>
190+
<div>
191+
<Label htmlFor="twitterHandle">Twitter Handle</Label>
192+
<Input id="twitterHandle" name="twitterHandle" value={blogData.twitterHandle} onChange={handleInputChange} />
193+
</div>
194+
<div>
195+
<Label htmlFor="linkedinProfile">LinkedIn Profile URL</Label>
196+
<Input id="linkedinProfile" name="linkedinProfile" value={blogData.linkedinProfile} onChange={handleInputChange} />
197+
</div>
198+
<div>
199+
<Label htmlFor="githubUsername">GitHub Username</Label>
200+
<Input id="githubUsername" name="githubUsername" value={blogData.githubUsername} onChange={handleInputChange} />
201+
</div>
202+
</form>
203+
</TabsContent>
204+
<TabsContent value="preview">
205+
<div className="bg-muted p-4 rounded-md relative">
206+
<pre className="whitespace-pre-wrap">{generatedMarkdown}</pre>
207+
{generatedMarkdown && (
208+
<Button
209+
className="absolute top-2 right-2"
210+
size="icon"
211+
onClick={copyToClipboard}
212+
aria-label="Copy to clipboard"
213+
>
214+
{isCopied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
215+
</Button>
216+
)}
217+
</div>
218+
</TabsContent>
219+
</Tabs>
220+
</CardContent>
221+
<CardFooter className="flex justify-between">
222+
<Button onClick={generateMarkdown}>Generate Markdown</Button>
223+
<Button
224+
onClick={copyToClipboard}
225+
disabled={!generatedMarkdown}
226+
className="ml-2"
227+
>
228+
{isCopied ? 'Copied!' : 'Copy Markdown'}
229+
</Button>
230+
</CardFooter>
231+
</Card>
232+
)
233+
}

Diff for: blog-markdown-generator/app/page.tsx

+6-98
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,9 @@
1-
import Image from "next/image";
1+
import BlogMarkdownGenerator from './blog-markdown-generator'
22

33
export default function Home() {
44
return (
5-
<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)]">
6-
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
7-
<Image
8-
className="dark:invert"
9-
src="/next.svg"
10-
alt="Next.js logo"
11-
width={180}
12-
height={38}
13-
priority
14-
/>
15-
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
16-
<li className="mb-2">
17-
Get started by editing{" "}
18-
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
19-
app/page.tsx
20-
</code>
21-
.
22-
</li>
23-
<li>Save and see your changes instantly.</li>
24-
</ol>
25-
26-
<div className="flex gap-4 items-center flex-col sm:flex-row">
27-
<a
28-
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"
29-
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
30-
target="_blank"
31-
rel="noopener noreferrer"
32-
>
33-
<Image
34-
className="dark:invert"
35-
src="/vercel.svg"
36-
alt="Vercel logomark"
37-
width={20}
38-
height={20}
39-
/>
40-
Deploy now
41-
</a>
42-
<a
43-
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"
44-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
45-
target="_blank"
46-
rel="noopener noreferrer"
47-
>
48-
Read our docs
49-
</a>
50-
</div>
51-
</main>
52-
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
53-
<a
54-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
55-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
56-
target="_blank"
57-
rel="noopener noreferrer"
58-
>
59-
<Image
60-
aria-hidden
61-
src="/file.svg"
62-
alt="File icon"
63-
width={16}
64-
height={16}
65-
/>
66-
Learn
67-
</a>
68-
<a
69-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
70-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
71-
target="_blank"
72-
rel="noopener noreferrer"
73-
>
74-
<Image
75-
aria-hidden
76-
src="/window.svg"
77-
alt="Window icon"
78-
width={16}
79-
height={16}
80-
/>
81-
Examples
82-
</a>
83-
<a
84-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
85-
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
86-
target="_blank"
87-
rel="noopener noreferrer"
88-
>
89-
<Image
90-
aria-hidden
91-
src="/globe.svg"
92-
alt="Globe icon"
93-
width={16}
94-
height={16}
95-
/>
96-
Go to nextjs.org →
97-
</a>
98-
</footer>
99-
</div>
100-
);
101-
}
5+
<main className="container mx-auto p-4">
6+
<BlogMarkdownGenerator />
7+
</main>
8+
)
9+
}

0 commit comments

Comments
 (0)