Add grouping categories feature
This commit is contained in:
		@@ -10,10 +10,12 @@ import {
 | 
				
			|||||||
  deleteFeed,
 | 
					  deleteFeed,
 | 
				
			||||||
  fetchAllEpisodes,
 | 
					  fetchAllEpisodes,
 | 
				
			||||||
  fetchEpisodesWithArticles,
 | 
					  fetchEpisodesWithArticles,
 | 
				
			||||||
 | 
					  getAllCategories,
 | 
				
			||||||
  getAllFeedsIncludingInactive,
 | 
					  getAllFeedsIncludingInactive,
 | 
				
			||||||
  getFeedById,
 | 
					 | 
				
			||||||
  getFeedByUrl,
 | 
					  getFeedByUrl,
 | 
				
			||||||
  getFeedRequests,
 | 
					  getFeedRequests,
 | 
				
			||||||
 | 
					  getFeedsByCategory,
 | 
				
			||||||
 | 
					  getFeedsGroupedByCategory,
 | 
				
			||||||
  toggleFeedActive,
 | 
					  toggleFeedActive,
 | 
				
			||||||
  updateFeedRequestStatus,
 | 
					  updateFeedRequestStatus,
 | 
				
			||||||
} from "./services/database.js";
 | 
					} from "./services/database.js";
 | 
				
			||||||
@@ -199,6 +201,38 @@ app.patch("/api/admin/feeds/:id/toggle", async (c) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Categories management
 | 
				
			||||||
 | 
					app.get("/api/admin/categories", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const categories = await getAllCategories();
 | 
				
			||||||
 | 
					    return c.json(categories);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching categories:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch categories" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.get("/api/admin/feeds/by-category", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const category = c.req.query("category");
 | 
				
			||||||
 | 
					    const feeds = await getFeedsByCategory(category);
 | 
				
			||||||
 | 
					    return c.json(feeds);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching feeds by category:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch feeds by category" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.get("/api/admin/feeds/grouped-by-category", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const groupedFeeds = await getFeedsGroupedByCategory();
 | 
				
			||||||
 | 
					    return c.json(groupedFeeds);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching feeds grouped by category:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch feeds grouped by category" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Episodes management
 | 
					// Episodes management
 | 
				
			||||||
app.get("/api/admin/episodes", async (c) => {
 | 
					app.get("/api/admin/episodes", async (c) => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
@@ -349,7 +383,10 @@ app.get("/api/admin/db-diagnostic", async (c) => {
 | 
				
			|||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error("Error running database diagnostic:", error);
 | 
					    console.error("Error running database diagnostic:", error);
 | 
				
			||||||
    return c.json(
 | 
					    return c.json(
 | 
				
			||||||
      { error: "Failed to run database diagnostic", details: error.message },
 | 
					      { 
 | 
				
			||||||
 | 
					        error: "Failed to run database diagnostic", 
 | 
				
			||||||
 | 
					        details: error instanceof Error ? error.message : String(error) 
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      500,
 | 
					      500,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -635,16 +672,6 @@ app.get("/", serveAdminIndex);
 | 
				
			|||||||
app.get("/index.html", serveAdminIndex);
 | 
					app.get("/index.html", serveAdminIndex);
 | 
				
			||||||
app.get("*", serveAdminIndex);
 | 
					app.get("*", serveAdminIndex);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Utility functions
 | 
					 | 
				
			||||||
async function runBatchProcess(): Promise<void> {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    await batchProcess();
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    console.error("Admin batch process failed:", error);
 | 
					 | 
				
			||||||
    throw error;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Start admin server
 | 
					// Start admin server
 | 
				
			||||||
serve(
 | 
					serve(
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										34
									
								
								server.ts
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								server.ts
									
									
									
									
									
								
							@@ -449,6 +449,40 @@ app.get("/api/feeds/:feedId/episodes", async (c) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.get("/api/categories", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { getAllCategories } = await import("./services/database.js");
 | 
				
			||||||
 | 
					    const categories = await getAllCategories();
 | 
				
			||||||
 | 
					    return c.json({ categories });
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching categories:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch categories" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.get("/api/feeds/by-category", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const category = c.req.query("category");
 | 
				
			||||||
 | 
					    const { getFeedsByCategory } = await import("./services/database.js");
 | 
				
			||||||
 | 
					    const feeds = await getFeedsByCategory(category);
 | 
				
			||||||
 | 
					    return c.json({ feeds });
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching feeds by category:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch feeds by category" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.get("/api/feeds/grouped-by-category", async (c) => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { getFeedsGroupedByCategory } = await import("./services/database.js");
 | 
				
			||||||
 | 
					    const groupedFeeds = await getFeedsGroupedByCategory();
 | 
				
			||||||
 | 
					    return c.json({ groupedFeeds });
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error fetching feeds grouped by category:", error);
 | 
				
			||||||
 | 
					    return c.json({ error: "Failed to fetch feeds grouped by category" }, 500);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.get("/api/episode-with-source/:episodeId", async (c) => {
 | 
					app.get("/api/episode-with-source/:episodeId", async (c) => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const episodeId = c.req.param("episodeId");
 | 
					    const episodeId = c.req.param("episodeId");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -213,6 +213,7 @@ export interface Feed {
 | 
				
			|||||||
  url: string;
 | 
					  url: string;
 | 
				
			||||||
  title?: string;
 | 
					  title?: string;
 | 
				
			||||||
  description?: string;
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  category?: string;
 | 
				
			||||||
  lastUpdated?: string;
 | 
					  lastUpdated?: string;
 | 
				
			||||||
  createdAt: string;
 | 
					  createdAt: string;
 | 
				
			||||||
  active: boolean;
 | 
					  active: boolean;
 | 
				
			||||||
@@ -325,6 +326,7 @@ export async function getFeedByUrl(url: string): Promise<Feed | null> {
 | 
				
			|||||||
      url: row.url,
 | 
					      url: row.url,
 | 
				
			||||||
      title: row.title,
 | 
					      title: row.title,
 | 
				
			||||||
      description: row.description,
 | 
					      description: row.description,
 | 
				
			||||||
 | 
					      category: row.category,
 | 
				
			||||||
      lastUpdated: row.last_updated,
 | 
					      lastUpdated: row.last_updated,
 | 
				
			||||||
      createdAt: row.created_at,
 | 
					      createdAt: row.created_at,
 | 
				
			||||||
      active: Boolean(row.active),
 | 
					      active: Boolean(row.active),
 | 
				
			||||||
@@ -346,6 +348,7 @@ export async function getFeedById(id: string): Promise<Feed | null> {
 | 
				
			|||||||
      url: row.url,
 | 
					      url: row.url,
 | 
				
			||||||
      title: row.title,
 | 
					      title: row.title,
 | 
				
			||||||
      description: row.description,
 | 
					      description: row.description,
 | 
				
			||||||
 | 
					      category: row.category,
 | 
				
			||||||
      lastUpdated: row.last_updated,
 | 
					      lastUpdated: row.last_updated,
 | 
				
			||||||
      createdAt: row.created_at,
 | 
					      createdAt: row.created_at,
 | 
				
			||||||
      active: Boolean(row.active),
 | 
					      active: Boolean(row.active),
 | 
				
			||||||
@@ -368,6 +371,7 @@ export async function getAllFeeds(): Promise<Feed[]> {
 | 
				
			|||||||
      url: row.url,
 | 
					      url: row.url,
 | 
				
			||||||
      title: row.title,
 | 
					      title: row.title,
 | 
				
			||||||
      description: row.description,
 | 
					      description: row.description,
 | 
				
			||||||
 | 
					      category: row.category,
 | 
				
			||||||
      lastUpdated: row.last_updated,
 | 
					      lastUpdated: row.last_updated,
 | 
				
			||||||
      createdAt: row.created_at,
 | 
					      createdAt: row.created_at,
 | 
				
			||||||
      active: Boolean(row.active),
 | 
					      active: Boolean(row.active),
 | 
				
			||||||
@@ -549,6 +553,7 @@ export async function getAllFeedsIncludingInactive(): Promise<Feed[]> {
 | 
				
			|||||||
      url: row.url,
 | 
					      url: row.url,
 | 
				
			||||||
      title: row.title,
 | 
					      title: row.title,
 | 
				
			||||||
      description: row.description,
 | 
					      description: row.description,
 | 
				
			||||||
 | 
					      category: row.category,
 | 
				
			||||||
      lastUpdated: row.last_updated,
 | 
					      lastUpdated: row.last_updated,
 | 
				
			||||||
      createdAt: row.created_at,
 | 
					      createdAt: row.created_at,
 | 
				
			||||||
      active: Boolean(row.active),
 | 
					      active: Boolean(row.active),
 | 
				
			||||||
@@ -559,6 +564,68 @@ export async function getAllFeedsIncludingInactive(): Promise<Feed[]> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getFeedsByCategory(category?: string): Promise<Feed[]> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    let stmt;
 | 
				
			||||||
 | 
					    let rows;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (category) {
 | 
				
			||||||
 | 
					      stmt = db.prepare("SELECT * FROM feeds WHERE category = ? AND active = 1 ORDER BY created_at DESC");
 | 
				
			||||||
 | 
					      rows = stmt.all(category) as any[];
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // If no category specified, return all active feeds
 | 
				
			||||||
 | 
					      stmt = db.prepare("SELECT * FROM feeds WHERE active = 1 ORDER BY created_at DESC");
 | 
				
			||||||
 | 
					      rows = stmt.all() as any[];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return rows.map((row) => ({
 | 
				
			||||||
 | 
					      id: row.id,
 | 
				
			||||||
 | 
					      url: row.url,
 | 
				
			||||||
 | 
					      title: row.title,
 | 
				
			||||||
 | 
					      description: row.description,
 | 
				
			||||||
 | 
					      category: row.category,
 | 
				
			||||||
 | 
					      lastUpdated: row.last_updated,
 | 
				
			||||||
 | 
					      createdAt: row.created_at,
 | 
				
			||||||
 | 
					      active: Boolean(row.active),
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error getting feeds by category:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getAllCategories(): Promise<string[]> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const stmt = db.prepare("SELECT DISTINCT category FROM feeds WHERE category IS NOT NULL AND active = 1 ORDER BY category");
 | 
				
			||||||
 | 
					    const rows = stmt.all() as any[];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return rows.map((row) => row.category).filter(Boolean);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error getting all categories:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getFeedsGroupedByCategory(): Promise<{ [category: string]: Feed[] }> {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const feeds = await getAllFeeds();
 | 
				
			||||||
 | 
					    const grouped: { [category: string]: Feed[] } = {};
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    for (const feed of feeds) {
 | 
				
			||||||
 | 
					      const category = feed.category || 'Uncategorized';
 | 
				
			||||||
 | 
					      if (!grouped[category]) {
 | 
				
			||||||
 | 
					        grouped[category] = [];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      grouped[category].push(feed);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return grouped;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    console.error("Error getting feeds grouped by category:", error);
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function deleteFeed(feedId: string): Promise<boolean> {
 | 
					export async function deleteFeed(feedId: string): Promise<boolean> {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Start transaction
 | 
					    // Start transaction
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user