PageSourceSearch

https://ucl.edu.pk/js/modules/blog-api.js

js ucl.edu.pk collected 2026-09-24 10:15:24 UTC 11,031 bytes, 310 lines download raw bytes

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.