1/** 2 * UCL Modular Blog API Client 3 * Wraps all database, storage, and authentication operations using supabase-client 4 */ 5 6(function () { 7 'use strict'; 8 9 class UCLBlogAPI { 10 static get client() { 11 if (!window.supabaseClient) { 12 throw new Error("Supabase Client is not initialized. Ensure js/modules/supabase-client.js is loaded first."); 13 } 14 return window.supabaseClient; 15 } 16 17 /* ========================================================================== 18 1. Authentication API 19 ========================================================================== */ 20 21 static async signIn(email, password) { 22 const { data, error } = await this.client.auth.signInWithPassword({ email, password }); 23 if (error) throw error; 24 return data.user; 25 } 26 27 static async signOut() { 28 const { error } = await this.client.auth.signOut(); 29 if (error) throw error; 30 return true; 31 } 32 33 static async getCurrentUser() { 34 const { data: { user }, error } = await this.client.auth.getUser(); 35 if (error) return null; 36 return user; 37 } 38 39 /* ========================================================================== 40 2. Categories CRUD API 41 ========================================================================== */ 42 43 static async getCategories() { 44 const { data, error } = await this.client 45 .from('categories') 46 .select('*') 47 .order('name', { ascending: true }); 48 if (error) throw error; 49 return data; 50 } 51 52 static async createCategory(name, slug, description = '') { 53 const { data, error } = await this.client 54 .from('categories') 55 .insert([{ name, slug, description }]) 56 .select(); 57 if (error) throw error; 58 return data[0]; 59 } 60 61 static async deleteCategory(id) { 62 const { error } = await this.client 63 .from('categories') 64 .delete() 65 .eq('id', id); 66 if (error) throw error; 67 return true; 68 } 69 70 /* ========================================================================== 71 3. Tags CRUD API 72 ========================================================================== */ 73 74 static async getTags() { 75 const { data, error } = await this.client 76 .from('tags') 77 .select('*') 78 .order('name', { ascending: true }); 79 if (error) throw error; 80 return data; 81 } 82 83 static async createTag(name, slug) { 84 const { data, error } = await this.client 85 .from('tags') 86 .insert([{ name, slug }]) 87 .select(); 88 if (error) throw error; 89 return data[0]; 90 } 91 92 static async deleteTag(id) { 93 const { error } = await this.client 94 .from('tags') 95 .delete() 96 .eq('id', id); 97 if (error) throw error; 98 return true; 99 } 100 101 /* ========================================================================== 102 4. Blogs (Posts) CRUD API 103 ========================================================================== */ 104 105 // Get list of blogs (supporting filters for category, tag, status, search, limit) 106 static async getBlogs(filters = {}) { 107 let query = this.client 108 .from('blogs') 109 .select(` 110 *, 111 categories (id, name, slug) 112 `); 113 114 // Apply filters 115 if (filters.status) { 116 query = query.eq('status', filters.status); 117 } 118 if (filters.category_id) { 119 query = query.eq('category_id', filters.category_id); 120 } 121 if (filters.search) { 122 query = query.or(`title.ilike.%${filters.search}%,excerpt.ilike.%${filters.search}%`); 123 } 124 125 // Order newest first 126 query = query.order('published_at', { ascending: false, nullsFirst: false }) 127 .order('created_at', { ascending: false }); 128 129 if (filters.limit) { 130 query = query.limit(filters.limit); 131 } 132 133 const { data: blogs, error } = await query; 134 if (error) throw error; 135 136 // If a tag filter is active, filter the results using the join table 137 if (filters.tag_id) { 138 const { data: mappings, error: mapErr } = await this.client 139 .from('blog_tags') 140 .select('blog_id') 141 .eq('tag_id', filters.tag_id); 142 if (mapErr) throw mapErr; 143 const blogIds = mappings.map(m => m.blog_id); 144 return blogs.filter(b => blogIds.includes(b.id)); 145 } 146 147 return blogs; 148 } 149 150 // Fetch single post details with associated category and tags by slug 151 static async getBlogBySlug(slug) { 152 const { data: blogs, error } = await this.client 153 .from('blogs') 154 .select(` 155 *, 156 categories (id, name, slug) 157 `) 158 .eq('slug', slug); 159 160 if (error) throw error; 161 if (!blogs || blogs.length === 0) throw new Error("Blog post not found."); 162 163 const blog = blogs[0]; 164 165 // Fetch associated tags 166 const { data: tagMappings, error: tagErr } = await this.client 167 .from('blog_tags') 168 .select(` 169 tags (id, name, slug) 170 `) 171 .eq('blog_id', blog.id); 172 173 if (tagErr) throw tagErr; 174 blog.tags = tagMappings.map(m =>
174 m.tags).filter(Boolean); 175 176 return blog; 177 } 178 179 // Fetch tags specifically associated with a blog ID 180 static async getBlogTags(blogId) { 181 const { data, error } = await this.client 182 .from('blog_tags') 183 .select('tag_id') 184 .eq('blog_id', blogId); 185 if (error) throw error; 186 return data.map(d => d.tag_id); 187 } 188 189 // Helper to generate a clean URL slug from title 190 static generateSlug(title) { 191 return title 192 .toLowerCase() 193 .replace(/[^a-z0-9 -]/g, '') // remove invalid chars 194 .replace(/\s+/g, '-') // collapse whitespace and replace by - 195 .replace(/-+/g, '-') // collapse dashes 196 .trim(); 197 } 198 199 // Create a new post and map its tag relations 200 static async createBlog(blogData, selectedTagIds = []) { 201 // Generate clean slug if not explicitly set 202 if (!blogData.slug) { 203 blogData.slug = this.generateSlug(blogData.title) + '-' + Date.now().toString().slice(-4); 204 } 205 if (blogData.status === 'published' && !blogData.published_at) { 206 blogData.published_at = new Date().toISOString(); 207 } 208 209 const { data: newBlogs, error: insertErr } = await this.client 210 .from('blogs') 211 .insert([blogData]) 212 .select(); 213 214 if (insertErr) throw insertErr; 215 const newBlog = newBlogs[0]; 216 217 // Insert tag associations in join table 218 if (selectedTagIds.length > 0) { 219 const relations = selectedTagIds.map(tagId => ({ 220 blog_id: newBlog.id, 221 tag_id: tagId 222 })); 223 const { error: relErr } = await this.client 224 .from('blog_tags') 225 .insert(relations); 226 if (relErr) throw relErr; 227 } 228 229 return newBlog; 230 } 231 232 // Update an existing post and reconcile tags 233 static async updateBlog(id, blogData, selectedTagIds = []) { 234 blogData.updated_at = new Date().toISOString(); 235 if (blogData.status === 'published' && !blogData.published_at) { 236 blogData.published_at = new Date().toISOString(); 237 } 238 239 const { data: updatedBlogs, error: updateErr } = await this.client 240 .from('blogs') 241 .update(blogData) 242 .eq('id', id) 243 .select(); 244 245 if (updateErr) throw updateErr; 246 const updatedBlog = updatedBlogs[0]; 247 248 // Reconcile Tag Relations 249 // 1. Delete all existing relations 250 const { error: delErr } = await this.client 251 .from('blog_tags') 252 .delete() 253 .eq('blog_id', id); 254 if (delErr) throw delErr; 255 256 // 2. Insert new relations 257 if (selectedTagIds.length > 0) { 258 const relations = selectedTagIds.map(tagId => ({ 259 blog_id: id, 260 tag_id: tagId 261 })); 262 const { error: relErr } = await this.client 263 .from('blog_tags') 264 .insert(relations); 265 if (relErr) throw relErr; 266 } 267 268 return updatedBlog; 269 } 270 271 // Delete a post (cascades automatically to tag associations in join table) 272 static async deleteBlog(id) { 273 const { error } = await this.client 274 .from('blogs') 275 .delete() 276 .eq('id', id); 277 if (error) throw error; 278 return true; 279 } 280 281 /* ========================================================================== 282 5. Image Storage Upload API 283 ========================================================================== */ 284 285 static async uploadThumbnail(file) { 286 const config = window.UCL_BLOG_CONFIG; 287 const fileExt = file.name.split('.').pop(); 288 const fileName = `${Math.random().toString(36).substring(2)}-${Date.now()}.${fileExt}`; 289 const uploadPath = `${fileName}`; 290 291 // Upload directly to Supabase Storage bucket named 'thumbnails' 292 const { data, error } = await this.client.storage 293 .from('thumbnails') 294 .upload(uploadPath, file, { 295 contentType: file.type, 296 cacheControl: '3600', 297 upsert: false 298 }); 299 300 if (error) throw error; 301 302 // Return the public URL of the uploaded image 303 const publicUrl = `${config.SUPABASE_URL}/storage/v1/object/public/thumbnails/${uploadPath}`; 304 return publicUrl; 305 } 306 } 307 308 // Export globally 309 window.UCLBlogAPI = UCLBlogAPI; 310})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.