11"use client"
22
3- import { useState , useEffect , useCallback , useMemo , useRef } from "react"
3+ import { useState , useEffect , useCallback , useMemo } from "react"
44import { createClient } from "@/lib/supabase/client"
55import { Card , CardContent , CardDescription , CardHeader , CardTitle } from "@/components/ui/card"
66import { Badge } from "@/components/ui/badge"
77import { Button } from "@/components/ui/button"
88import { Input } from "@/components/ui/input"
9- import { Textarea } from "@/components/ui/textarea"
109import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from "@/components/ui/table"
1110import { DropdownMenu , DropdownMenuContent , DropdownMenuItem , DropdownMenuLabel , DropdownMenuSeparator , DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
1211import { Dialog , DialogContent , DialogDescription , DialogFooter , DialogHeader , DialogTitle } from "@/components/ui/dialog"
@@ -17,6 +16,7 @@ import { AlertCircle, FileText, Search, MoreHorizontal, Edit, Star, Trash2, Plus
1716import { Alert , AlertDescription } from "@/components/ui/alert"
1817import { categories , BlogPost } from "@/components/data/blog-posts"
1918import { OptimizedImage } from '@/components/ui/optimized-image'
19+ import { RichTextEditor } from '@/components/ui/rich-text-editor'
2020
2121// types
2222interface BlogFormData {
@@ -86,17 +86,17 @@ const useBlogPosts = () => {
8686 try {
8787 setIsLoading ( true )
8888 setError ( null )
89-
89+
9090 // First, fetch all blog posts
9191 const { data : postsData , error : fetchError } = await supabase
9292 . from ( "blogs" )
9393 . select ( "*" )
9494 . order ( "date" , { ascending : false } )
95-
95+
9696 if ( fetchError ) {
9797 throw new Error ( fetchError . message )
9898 }
99-
99+
100100 if ( postsData ) {
101101 // Fetch real like counts and ensure views are up-to-date for each blog post
102102 const postsWithRealCounts = await Promise . all (
@@ -106,30 +106,30 @@ const useBlogPosts = () => {
106106 . from ( 'blog_likes' )
107107 . select ( '*' , { count : 'exact' , head : true } )
108108 . eq ( 'blog_slug' , post . slug || post . title . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 ] + / g, '-' ) . replace ( / - + / g, '-' ) . replace ( / ^ - | - $ / g, '' ) )
109-
109+
110110 if ( likeError ) {
111111 console . error ( 'Error fetching likes for post:' , post . title , likeError )
112112 }
113-
113+
114114 // Get the most up-to-date view count from blogs table
115115 const { data : viewData , error : viewError } = await supabase
116116 . from ( 'blogs' )
117117 . select ( 'views' )
118118 . eq ( 'id' , post . id )
119119 . single ( )
120-
120+
121121 if ( viewError ) {
122122 console . error ( 'Error fetching views for post:' , post . title , viewError )
123123 }
124-
125- return {
126- ...post ,
124+
125+ return {
126+ ...post ,
127127 likes : likeCount || 0 ,
128128 views : viewData ?. views ?. toString ( ) || post . views || '0'
129129 }
130130 } )
131131 )
132-
132+
133133 setBlogPosts ( postsWithRealCounts as BlogPost [ ] )
134134 }
135135 } catch ( err ) {
@@ -168,8 +168,8 @@ const FeaturedBadge = ({ featured }: { featured: boolean }) => (
168168 )
169169)
170170
171- const BlogPostForm = ( {
172- formData,
171+ const BlogPostForm = ( {
172+ formData,
173173 onFormChange
174174} : {
175175 formData : BlogFormData ;
@@ -189,9 +189,6 @@ const BlogPostForm = ({
189189 onFormChange ( { featured : checked } )
190190 }
191191
192- // ref for the content textarea
193- const contentRef = useRef < HTMLTextAreaElement > ( null ) ;
194-
195192 // Article image upload handler (for main blog image)
196193 const handleArticleImageUpload = async ( e : React . ChangeEvent < HTMLInputElement > ) => {
197194 const file = e . target . files ?. [ 0 ] ;
@@ -220,48 +217,6 @@ const BlogPostForm = ({
220217 }
221218 }
222219
223- // image upload handler for inserting into content
224- const handleContentImageUpload = async ( e : React . ChangeEvent < HTMLInputElement > ) => {
225- const file = e . target . files ?. [ 0 ] ;
226- if ( ! file ) return ;
227-
228- const supabase = createClient ( ) ;
229- const filePath = `public/${ Date . now ( ) } -${ file . name } ` ;
230-
231- // upload to supabase storage
232- const { error } = await supabase . storage
233- . from ( 'blog-images' )
234- . upload ( filePath , file ) ;
235-
236- if ( error ) {
237- alert ( "Image upload failed: " + error . message ) ;
238- return ;
239- }
240-
241- // Get public url
242- const { data : publicUrlData } = supabase . storage
243- . from ( 'blog-images' )
244- . getPublicUrl ( filePath ) ;
245-
246- if ( publicUrlData ?. publicUrl ) {
247- // insert html <img> tag at cursor in content
248- if ( contentRef . current ) {
249- const textarea = contentRef . current ;
250- const start = textarea . selectionStart ;
251- const end = textarea . selectionEnd ;
252- const before = formData . content . slice ( 0 , start ) ;
253- const after = formData . content . slice ( end ) ;
254- const htmlImg = `<img src="${ publicUrlData . publicUrl } " alt="Alt text" style="max-width:100%;height:auto;" />\n` ;
255- const newContent = before + htmlImg + after ;
256- onFormChange ( { content : newContent } ) ;
257- setTimeout ( ( ) => {
258- textarea . focus ( ) ;
259- textarea . selectionStart = textarea . selectionEnd = start + htmlImg . length ;
260- } , 0 ) ;
261- }
262- }
263- }
264-
265220 return (
266221 < div className = "grid gap-4 py-4" >
267222 { /* Article Image upload section */ }
@@ -312,25 +267,16 @@ const BlogPostForm = ({
312267 />
313268 </ div >
314269
315- { /* Content image upload button */ }
270+ { /* Rich Text Editor for Content */ }
316271 < div className = "grid gap-2" >
317272 < Label htmlFor = "content" > Content *</ Label >
318- < Textarea
319- id = "content"
320- ref = { contentRef }
321- placeholder = "Write your blog post content here..."
322- value = { formData . content }
323- onChange = { handleInputChange ( 'content' ) }
324- className = "text-sm min-h-[120px]"
273+ < RichTextEditor
274+ content = { formData . content }
275+ onChange = { ( content ) => onFormChange ( { content } ) }
325276 />
326- < Input
327- id = "content-image"
328- type = "file"
329- accept = "image/*"
330- onChange = { handleContentImageUpload }
331- className = "text-sm mt-2"
332- />
333- < span className = "text-xs text-muted-foreground" > Upload and insert image at cursor in content</ span >
277+ < p className = "text-xs text-muted-foreground" >
278+ Use the toolbar to format text and insert images directly into your content.
279+ </ p >
334280 </ div >
335281
336282 < div className = "grid grid-cols-2 gap-4" >
@@ -372,8 +318,8 @@ const BlogPostForm = ({
372318
373319 < div className = "grid gap-2" >
374320 < Label htmlFor = "category" > Category</ Label >
375- < Select
376- value = { formData . category }
321+ < Select
322+ value = { formData . category }
377323 onValueChange = { handleSelectChange ( 'category' ) }
378324 >
379325 < SelectTrigger className = "text-sm" >
@@ -436,7 +382,7 @@ const EmptyState = ({ title, description, action }: {
436382export default function AdminBlogPage ( ) {
437383 const { blogPosts, isLoading, error, refetch } = useBlogPosts ( )
438384 const supabase = useSupabase ( )
439-
385+
440386
441387 const [ searchTerm , setSearchTerm ] = useState ( "" )
442388 const [ categoryFilter , setCategoryFilter ] = useState ( "All" )
@@ -455,12 +401,12 @@ export default function AdminBlogPage() {
455401 post . excerpt ,
456402 post . author ,
457403 ...parseTags ( post . tags )
458- ] . some ( field =>
404+ ] . some ( field =>
459405 field . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) )
460406 )
461-
407+
462408 const matchesCategory = categoryFilter === "All" || post . category === categoryFilter
463-
409+
464410 return matchesSearch && matchesCategory
465411 } )
466412 } , [ blogPosts , searchTerm , categoryFilter ] )
@@ -750,7 +696,7 @@ export default function AdminBlogPage() {
750696 < EmptyState
751697 title = { blogPosts . length === 0 ? "No blog posts yet" : "No posts match your filters" }
752698 description = {
753- blogPosts . length === 0
699+ blogPosts . length === 0
754700 ? "Create your first blog post to get started."
755701 : "Try adjusting your search or filter criteria."
756702 }
@@ -828,8 +774,8 @@ export default function AdminBlogPage() {
828774 Edit Post
829775 </ DropdownMenuItem >
830776 < DropdownMenuSeparator />
831- < DropdownMenuItem
832- className = "text-xs text-red-600 focus:text-red-600"
777+ < DropdownMenuItem
778+ className = "text-xs text-red-600 focus:text-red-600"
833779 onClick = { ( ) => setShowDelete ( post ) }
834780 >
835781 < Trash2 className = "mr-2 h-4 w-4" />
@@ -856,7 +802,7 @@ export default function AdminBlogPage() {
856802 Fill out the form below to create a new blog post.
857803 </ DialogDescription >
858804 </ DialogHeader >
859-
805+
860806 { formError && (
861807 < Alert variant = "destructive" >
862808 < AlertCircle className = "h-4 w-4" />
@@ -868,7 +814,7 @@ export default function AdminBlogPage() {
868814 formData = { formData }
869815 onFormChange = { handleFormChange }
870816 />
871-
817+
872818 < DialogFooter >
873819 < Button variant = "outline" onClick = { closeCreate } disabled = { formLoading } >
874820 Cancel
@@ -890,7 +836,7 @@ export default function AdminBlogPage() {
890836 Update the blog post information below.
891837 </ DialogDescription >
892838 </ DialogHeader >
893-
839+
894840 { formError && (
895841 < Alert variant = "destructive" >
896842 < AlertCircle className = "h-4 w-4" />
@@ -902,7 +848,7 @@ export default function AdminBlogPage() {
902848 formData = { formData }
903849 onFormChange = { handleFormChange }
904850 />
905-
851+
906852 < DialogFooter >
907853 < Button variant = "outline" onClick = { closeEdit } disabled = { formLoading } >
908854 Cancel
@@ -924,7 +870,7 @@ export default function AdminBlogPage() {
924870 This action cannot be undone. The blog post will be permanently deleted.
925871 </ DialogDescription >
926872 </ DialogHeader >
927-
873+
928874 < div className = "py-4" >
929875 < div className = "p-4 bg-muted rounded-lg" >
930876 < p className = "font-medium text-sm" > { showDelete ?. title } </ p >
@@ -933,18 +879,18 @@ export default function AdminBlogPage() {
933879 </ p >
934880 </ div >
935881 </ div >
936-
882+
937883 < DialogFooter >
938- < Button
939- variant = "outline"
940- onClick = { ( ) => setShowDelete ( null ) }
884+ < Button
885+ variant = "outline"
886+ onClick = { ( ) => setShowDelete ( null ) }
941887 disabled = { formLoading }
942888 >
943889 Cancel
944890 </ Button >
945- < Button
946- onClick = { handleDelete }
947- disabled = { formLoading }
891+ < Button
892+ onClick = { handleDelete }
893+ disabled = { formLoading }
948894 variant = "destructive"
949895 >
950896 { formLoading && < Loader2 className = "animate-spin h-4 w-4 mr-2" /> }
0 commit comments