|
1 | 1 | "use client" |
2 | 2 |
|
3 | | -import { useState, useEffect, useCallback, useMemo } from "react" |
| 3 | +import { useState, useEffect, useCallback, useMemo, useRef } from "react" |
4 | 4 | import { createClient } from "@/lib/supabase/client" |
5 | 5 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" |
6 | 6 | import { Badge } from "@/components/ui/badge" |
@@ -156,6 +156,53 @@ const BlogPostForm = ({ |
156 | 156 | onFormChange({ featured: checked }) |
157 | 157 | } |
158 | 158 |
|
| 159 | + // ref for the content textarea |
| 160 | + const contentRef = useRef<HTMLTextAreaElement>(null); |
| 161 | + |
| 162 | + // image upload handler with auto-insert HTML <img> tag |
| 163 | + const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { |
| 164 | + const file = e.target.files?.[0]; |
| 165 | + if (!file) return; |
| 166 | + |
| 167 | + const supabase = createClient(); |
| 168 | + const filePath = `public/${Date.now()}-${file.name}`; |
| 169 | + |
| 170 | + // upload to supabase storage |
| 171 | + const { error } = await supabase.storage |
| 172 | + .from('blog-images') |
| 173 | + .upload(filePath, file); |
| 174 | + |
| 175 | + if (error) { |
| 176 | + alert("Image upload failed: " + error.message); |
| 177 | + return; |
| 178 | + } |
| 179 | + |
| 180 | + // Get public url |
| 181 | + const { data: publicUrlData } = supabase.storage |
| 182 | + .from('blog-images') |
| 183 | + .getPublicUrl(filePath); |
| 184 | + |
| 185 | + if (publicUrlData?.publicUrl) { |
| 186 | + onFormChange({ image: publicUrlData.publicUrl }); |
| 187 | + |
| 188 | + // insert html <img> tag at cursor in content |
| 189 | + if (contentRef.current) { |
| 190 | + const textarea = contentRef.current; |
| 191 | + const start = textarea.selectionStart; |
| 192 | + const end = textarea.selectionEnd; |
| 193 | + const before = formData.content.slice(0, start); |
| 194 | + const after = formData.content.slice(end); |
| 195 | + const htmlImg = `<img src="${publicUrlData.publicUrl}" alt="Alt text" style="max-width:100%;height:auto;" />\n`; |
| 196 | + const newContent = before + htmlImg + after; |
| 197 | + onFormChange({ content: newContent }); |
| 198 | + setTimeout(() => { |
| 199 | + textarea.focus(); |
| 200 | + textarea.selectionStart = textarea.selectionEnd = start + htmlImg.length; |
| 201 | + }, 0); |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | + |
159 | 206 | return ( |
160 | 207 | <div className="grid gap-4 py-4"> |
161 | 208 | <div className="grid gap-2"> |
@@ -184,6 +231,7 @@ const BlogPostForm = ({ |
184 | 231 | <Label htmlFor="content">Content *</Label> |
185 | 232 | <Textarea |
186 | 233 | id="content" |
| 234 | + ref={contentRef} |
187 | 235 | placeholder="Write your blog post content here..." |
188 | 236 | value={formData.content} |
189 | 237 | onChange={handleInputChange('content')} |
@@ -268,15 +316,24 @@ const BlogPostForm = ({ |
268 | 316 | </Select> |
269 | 317 | </div> |
270 | 318 |
|
| 319 | + {/* Image upload section */} |
271 | 320 | <div className="grid gap-2"> |
272 | | - <Label htmlFor="image">Image URL</Label> |
| 321 | + <Label htmlFor="image">Image</Label> |
273 | 322 | <Input |
274 | 323 | id="image" |
275 | | - placeholder="https://example.com/image.jpg" |
276 | | - value={formData.image} |
277 | | - onChange={handleInputChange('image')} |
| 324 | + type="file" |
| 325 | + accept="image/*" |
| 326 | + onChange={handleImageUpload} |
278 | 327 | className="text-sm" |
279 | 328 | /> |
| 329 | + {formData.image && ( |
| 330 | + <div> |
| 331 | + <img src={formData.image} alt="Preview" className="mt-2 max-h-32" /> |
| 332 | + <div className="mt-2 flex items-center gap-2"> |
| 333 | + <Input value={formData.image} readOnly className="text-xs" /> |
| 334 | + </div> |
| 335 | + </div> |
| 336 | + )} |
280 | 337 | </div> |
281 | 338 |
|
282 | 339 | <div className="flex items-center space-x-2"> |
|
0 commit comments