From 4f610d87cc9c0a27aaf7f80c38972ec1b24cac19 Mon Sep 17 00:00:00 2001 From: kevin8181 <66894759+kevin8181@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:22:11 -0400 Subject: [PATCH 1/8] change header --- src/pages/pulse/subscribe/index.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/pulse/subscribe/index.astro b/src/pages/pulse/subscribe/index.astro index 79dc922..0ca5ae4 100644 --- a/src/pages/pulse/subscribe/index.astro +++ b/src/pages/pulse/subscribe/index.astro @@ -5,7 +5,7 @@ import { FeedManager } from "@/features/feeds/feedManager";
-

Subscribe to Scouting411 Pulse

+

Subscribe with your Feed Reader

Although the upstream sources on Pulse are published in a variety of From 4378c408915c7fb8224054d7428461ec23201241 Mon Sep 17 00:00:00 2001 From: kevin8181 <66894759+kevin8181@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:23:24 -0400 Subject: [PATCH 2/8] change query api to POST, add bruno --- bruno/bruno/bruno.json | 6 ++++++ bruno/bruno/collection.bru | 4 ++++ bruno/bruno/query-posts.bru | 17 +++++++++++++++++ src/features/postsQuery/query.ts | 6 ++---- src/pages/api/posts.ts | 25 +++++-------------------- 5 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 bruno/bruno/bruno.json create mode 100644 bruno/bruno/collection.bru create mode 100644 bruno/bruno/query-posts.bru diff --git a/bruno/bruno/bruno.json b/bruno/bruno/bruno.json new file mode 100644 index 0000000..565cc3e --- /dev/null +++ b/bruno/bruno/bruno.json @@ -0,0 +1,6 @@ +{ + "version": "1", + "name": "bruno", + "type": "collection", + "ignore": ["node_modules", ".git"] +} diff --git a/bruno/bruno/collection.bru b/bruno/bruno/collection.bru new file mode 100644 index 0000000..e3e7518 --- /dev/null +++ b/bruno/bruno/collection.bru @@ -0,0 +1,4 @@ +meta { + name: bruno + type: collection +} diff --git a/bruno/bruno/query-posts.bru b/bruno/bruno/query-posts.bru new file mode 100644 index 0000000..0a191f8 --- /dev/null +++ b/bruno/bruno/query-posts.bru @@ -0,0 +1,17 @@ +meta { + name: Query Posts + type: http + seq: 1 +} + +post { + url: http://localhost:4321/api/posts + body: json + auth: none +} + +body:json { + { + + } +} diff --git a/src/features/postsQuery/query.ts b/src/features/postsQuery/query.ts index 6465d00..9e22918 100644 --- a/src/features/postsQuery/query.ts +++ b/src/features/postsQuery/query.ts @@ -19,8 +19,6 @@ export async function queryPosts( } type QueryOpts = z.infer; -//todo -//eslint-disable-next-line -const queryOptsSchema = z.object({ - paginate: paginateOptsSchema, +export const queryOptsSchema = z.object({ + paginate: paginateOptsSchema.default({ page: 1, pageSize: 20 }), }); diff --git a/src/pages/api/posts.ts b/src/pages/api/posts.ts index a0a50a5..6a706fc 100644 --- a/src/pages/api/posts.ts +++ b/src/pages/api/posts.ts @@ -1,16 +1,11 @@ export const prerender = false; import type { APIRoute } from "astro"; -import { z } from "astro/zod"; -import { queryPosts } from "@/features/postsQuery/query"; +import { queryPosts, queryOptsSchema } from "@/features/postsQuery/query"; -export const GET: APIRoute = async (context) => { - const params = context.url.searchParams; +export const POST: APIRoute = async (context) => { + const body = await context.request.json(); - console.log(params); - - const paramsObj = Object.fromEntries(params); - - const { error, data: query } = postsQueryParamsSchema.safeParse(paramsObj); + const { error, data: query } = queryOptsSchema.safeParse(body); if (error) { return new Response(JSON.stringify({ error }), { @@ -21,12 +16,7 @@ export const GET: APIRoute = async (context) => { }); } - const posts = await queryPosts({ - paginate: { - page: query.page, - pageSize: query.pageSize, - }, - }); + const posts = await queryPosts(query); return new Response(JSON.stringify(posts), { status: 200, @@ -35,8 +25,3 @@ export const GET: APIRoute = async (context) => { }, }); }; - -const postsQueryParamsSchema = z.object({ - page: z.coerce.number().min(1), - pageSize: z.coerce.number().min(1).max(1000).default(20), -}); From bc36f1921925f426523658e37970ca3f36b6c7c4 Mon Sep 17 00:00:00 2001 From: kevin8181 <66894759+kevin8181@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:33:18 -0400 Subject: [PATCH 3/8] fix bruno folders --- bruno/{bruno => }/bruno.json | 0 bruno/{bruno => }/collection.bru | 0 bruno/{bruno => }/query-posts.bru | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename bruno/{bruno => }/bruno.json (100%) rename bruno/{bruno => }/collection.bru (100%) rename bruno/{bruno => }/query-posts.bru (100%) diff --git a/bruno/bruno/bruno.json b/bruno/bruno.json similarity index 100% rename from bruno/bruno/bruno.json rename to bruno/bruno.json diff --git a/bruno/bruno/collection.bru b/bruno/collection.bru similarity index 100% rename from bruno/bruno/collection.bru rename to bruno/collection.bru diff --git a/bruno/bruno/query-posts.bru b/bruno/query-posts.bru similarity index 100% rename from bruno/bruno/query-posts.bru rename to bruno/query-posts.bru From fd3a533ba3a3a220aea78b59a6bc049f9df2299f Mon Sep 17 00:00:00 2001 From: kevin8181 <66894759+kevin8181@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:54:30 -0400 Subject: [PATCH 4/8] add sorting to query api --- src/features/postsQuery/query.ts | 14 +++++++----- src/features/postsQuery/sort.ts | 25 ++++++++++++++++++++++ src/util/{paginate.ts => paginateArray.ts} | 6 +++--- 3 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 src/features/postsQuery/sort.ts rename src/util/{paginate.ts => paginateArray.ts} (91%) diff --git a/src/features/postsQuery/query.ts b/src/features/postsQuery/query.ts index 9e22918..7d05283 100644 --- a/src/features/postsQuery/query.ts +++ b/src/features/postsQuery/query.ts @@ -1,24 +1,28 @@ import { FeedManager } from "@/features/feeds/feedManager"; import { z } from "astro/zod"; import { - paginate, + paginateArray, paginateOptsSchema, type PaginatedResults, -} from "@/util/paginate"; +} from "@/util/paginateArray"; +import { sortPosts, sortOptsSchema } from "@/features/postsQuery/sort"; import { Post } from "@/features/posts/post"; export async function queryPosts( opts: QueryOpts, ): Promise> { - // get all the posts. todo make it so you can start with only a subset of the feeds + // todo make it so you can start with only a subset of the feeds const posts = await FeedManager.allPosts(); - // todo add filtering and sorting + // todo add filtering - return paginate(posts, opts.paginate); + const sortedPosts = sortPosts(posts, opts.sort); + + return paginateArray(sortedPosts, opts.paginate); } type QueryOpts = z.infer; export const queryOptsSchema = z.object({ + sort: sortOptsSchema.default({ mode: "date", direction: "desc" }), paginate: paginateOptsSchema.default({ page: 1, pageSize: 20 }), }); diff --git a/src/features/postsQuery/sort.ts b/src/features/postsQuery/sort.ts new file mode 100644 index 0000000..1f65794 --- /dev/null +++ b/src/features/postsQuery/sort.ts @@ -0,0 +1,25 @@ +import { z } from "astro/zod"; +import { Post } from "@/features/posts/post"; + +export function sortPosts(posts: Post[], opts: z.infer) { + let sortedPosts; + + switch (opts.mode) { + case "date": + sortedPosts = posts.sort((a, b) => a.date.getTime() - b.date.getTime()); + break; + default: + return posts; + } + + if (opts.direction === "desc") { + sortedPosts.reverse(); + } + + return sortedPosts; +} + +export const sortOptsSchema = z.object({ + mode: z.enum(["date"]), + direction: z.enum(["asc", "desc"]), +}); diff --git a/src/util/paginate.ts b/src/util/paginateArray.ts similarity index 91% rename from src/util/paginate.ts rename to src/util/paginateArray.ts index e0f06f3..1be235b 100644 --- a/src/util/paginate.ts +++ b/src/util/paginateArray.ts @@ -1,7 +1,7 @@ import { z } from "astro/zod"; /** paginate an array of items */ -export function paginate( +export function paginateArray( data: T[], opts: PaginateOpts, ): PaginatedResults { @@ -27,9 +27,9 @@ export function paginate( type PaginateOpts = z.infer; export const paginateOptsSchema = z.object({ /** the page size */ - pageSize: z.number().min(1), + pageSize: z.coerce.number().min(1), /** the page number */ - page: z.number().min(1), + page: z.coerce.number().min(1), }); export type PaginatedResults = { From d0dded0246fcb9f1274ef51e72f6c4138f031040 Mon Sep 17 00:00:00 2001 From: kevin8181 <66894759+kevin8181@users.noreply.github.com> Date: Fri, 13 Mar 2026 00:37:01 -0400 Subject: [PATCH 5/8] add filtering to post query api --- bruno/query-posts.bru | 12 ++++- src/features/postsQuery/filter.ts | 87 ++++++++++++++++++------------- src/features/postsQuery/query.ts | 8 +-- src/util/utilTypes.ts | 3 ++ 4 files changed, 71 insertions(+), 39 deletions(-) diff --git a/bruno/query-posts.bru b/bruno/query-posts.bru index 0a191f8..d0361ff 100644 --- a/bruno/query-posts.bru +++ b/bruno/query-posts.bru @@ -12,6 +12,16 @@ post { body:json { { - + "filter": { + "keyword": "scouting" + }, + "sort": { + "mode": "date", + "direction": "desc" + }, + "paginate": { + "page": 1, + "pageSize": 20 + } } } diff --git a/src/features/postsQuery/filter.ts b/src/features/postsQuery/filter.ts index 235061e..c16d019 100644 --- a/src/features/postsQuery/filter.ts +++ b/src/features/postsQuery/filter.ts @@ -1,35 +1,52 @@ -// import { z } from "astro/zod"; -// import { Post } from "@/features/posts/post"; - -// export function filterPosts({ -// posts, -// filterOpts, -// }: { -// posts: Post[]; -// filterOpts: z.infer; -// }) { -// return posts.filter((_post) => { -// const predicates = buildPredicates(filterOpts); - -// // if all of the predicates are true, return true -// if (Object.values(predicates).every((predicate) => predicate)) { -// return true; -// } - -// // if any of the predicates are false, return false -// return false; -// }); -// } - -// /** check a post against each filter. return an object with a bool for each predicate */ -// function buildPredicates(filters: FilterOpts) { -// return { - -// }; -// } - -// export const filterOptsSchema = z.object({ -// dateBefore: z.coerce.date().optional(), -// dateAfter: z.coerce.date().optional(), -// }).strict(); -// export type FilterOpts = z.infer; +import { z } from "astro/zod"; +import { Post } from "@/features/posts/post"; +import type { Predicate } from "@/util/utilTypes"; + +export function filterPosts( + posts: Post[], + opts: z.infer, +) { + const predicates = buildPredicates(opts); + + return posts.filter((post) => + predicates.every((predicate) => predicate(post)), + ); +} + +/** turn a filter config into an array of predicates */ +function buildPredicates(filter: FilterOpts): Predicate[] { + const predicates: Predicate[] = []; + + for (const [key, value] of Object.entries(filter) as [ + keyof FilterOpts, + FilterOpts[keyof FilterOpts], + ][]) { + if (value !== undefined) { + const factory = predicateFactories[key]; + predicates.push(factory(value as NonNullable)); + } + } + + return predicates; +} + +const predicateFactories: PredicateFactories = { + keyword: (value: string): Predicate => { + return (post) => { + const titleMatch = post.title.toUpperCase().includes(value.toUpperCase()); + const descriptionMatch = !!post.description + ?.toUpperCase() + .includes(value.toUpperCase()); + + return titleMatch || descriptionMatch; + }; + }, +}; +type PredicateFactories = { + [K in keyof F]-?: (value: NonNullable) => Predicate; +}; + +type FilterOpts = z.infer; +export const filterOptsSchema = z.object({ + keyword: z.string().optional(), +}); diff --git a/src/features/postsQuery/query.ts b/src/features/postsQuery/query.ts index 7d05283..e9f239a 100644 --- a/src/features/postsQuery/query.ts +++ b/src/features/postsQuery/query.ts @@ -1,11 +1,12 @@ import { FeedManager } from "@/features/feeds/feedManager"; import { z } from "astro/zod"; +import { sortPosts, sortOptsSchema } from "@/features/postsQuery/sort"; import { paginateArray, paginateOptsSchema, type PaginatedResults, } from "@/util/paginateArray"; -import { sortPosts, sortOptsSchema } from "@/features/postsQuery/sort"; +import { filterPosts, filterOptsSchema } from "@/features/postsQuery/filter"; import { Post } from "@/features/posts/post"; export async function queryPosts( @@ -14,15 +15,16 @@ export async function queryPosts( // todo make it so you can start with only a subset of the feeds const posts = await FeedManager.allPosts(); - // todo add filtering + const filteredPosts = filterPosts(posts, opts.filter); - const sortedPosts = sortPosts(posts, opts.sort); + const sortedPosts = sortPosts(filteredPosts, opts.sort); return paginateArray(sortedPosts, opts.paginate); } type QueryOpts = z.infer; export const queryOptsSchema = z.object({ + filter: filterOptsSchema.default({}), sort: sortOptsSchema.default({ mode: "date", direction: "desc" }), paginate: paginateOptsSchema.default({ page: 1, pageSize: 20 }), }); diff --git a/src/util/utilTypes.ts b/src/util/utilTypes.ts index f15465f..fdbe7f2 100644 --- a/src/util/utilTypes.ts +++ b/src/util/utilTypes.ts @@ -1 +1,4 @@ export type UrlShaped = `${"http" | "https"}://${string}`; + +/** a predicate function that takes an item and returns a boolean */ +export type Predicate = (item: T) => boolean; From 78d83ae78a933b1ad747224fa00d5e7d44031a36 Mon Sep 17 00:00:00 2001 From: kevin8181 <66894759+kevin8181@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:21:16 -0400 Subject: [PATCH 6/8] improve opml --- public/xslt/opml.xslt | 15 +++++++++++++++ src/pages/feeds/all/opml.ts | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/public/xslt/opml.xslt b/public/xslt/opml.xslt index 59bd3a7..23eeb63 100644 --- a/public/xslt/opml.xslt +++ b/public/xslt/opml.xslt @@ -9,11 +9,26 @@ +

+
+ + + Open in the (requires JavaScript). +
+

- First - Prev - {page.currentPage} of {page.lastPage} - Next - Last -
- diff --git a/src/pages/pulse/browse/_page.tsx b/src/pages/pulse/browse/_page.tsx new file mode 100644 index 0000000..ed50cef --- /dev/null +++ b/src/pages/pulse/browse/_page.tsx @@ -0,0 +1,31 @@ +import { CardList } from "@/components/react/cardList"; +import { RenderPost as PostComponent } from "@/components/react/post"; +import { queryPosts } from "@/features/postsQuery/query"; + +export async function BrowsePage() { + const results = await queryPosts({ + filter: {}, + sort: { + mode: "date", + direction: "desc", + }, + paginate: { + maxPageSize: 100, + page: 1, + }, + }); + return ( + <> + + Showing posts {results.pagination.firstItemIndex + 1} -{" "} + {results.pagination.lastItemIndex + 1} of{" "} + {results.pagination.totalItems}. + + + {results.items.map((post) => ( + + ))} + + + ); +} diff --git a/src/pages/pulse/browse/index.astro b/src/pages/pulse/browse/index.astro new file mode 100644 index 0000000..c539a8d --- /dev/null +++ b/src/pages/pulse/browse/index.astro @@ -0,0 +1,8 @@ +--- +import UiLayout from "@/components/layout/UiLayout.astro"; +import { BrowsePage } from "@/pages/pulse/browse/_page"; +--- + + + + diff --git a/src/pages/pulse/sources/[slug]/index.astro b/src/pages/pulse/sources/[slug]/index.astro index 4b12d6c..6461857 100644 --- a/src/pages/pulse/sources/[slug]/index.astro +++ b/src/pages/pulse/sources/[slug]/index.astro @@ -21,15 +21,16 @@ if (!feed) throw new Error(`Feed ${slug} not found`);

{feed.description}

Homepage: {feed.homepageUrl}{feed.urls.homepage}

Indexed posts: {(await feed.posts()).length}

- Subcribe: rss + Subcribe: rss + atom

diff --git a/src/pages/pulse/sources/index.astro b/src/pages/pulse/sources/index.astro index fb9fecd..b422c0d 100644 --- a/src/pages/pulse/sources/index.astro +++ b/src/pages/pulse/sources/index.astro @@ -5,14 +5,14 @@ import { FeedManager } from "@/features/feeds/feedManager";
-

Scouting411 News Sources

+

News Sources

{ FeedManager.feeds.map(async (feed) => (
{feed.name} diff --git a/src/pages/pulse/stats/index.astro b/src/pages/pulse/stats/index.astro index a0e6af3..a1962a3 100644 --- a/src/pages/pulse/stats/index.astro +++ b/src/pages/pulse/stats/index.astro @@ -4,48 +4,53 @@ import { FeedManager } from "@/features/feeds/feedManager"; --- - - - - - - - - - - { - FeedManager.feeds.map(async (feed) => ( - - - - - - )) - } - - - - - - - - -
FeedPostsAdapter Type
- - {feed.name} - - - {Intl.NumberFormat().format((await feed.posts()).length)} - {feed.type.human}
Total: - { - Intl.NumberFormat().format((await FeedManager.allPosts()).length) - } - {}
+
+

Feed Statistics

+ + + + + + + + + + { + FeedManager.feeds.map(async (feed) => ( + + + + + + )) + } + + + + + + + + +
FeedPostsAdapter Type
+ + {feed.name} + + + {Intl.NumberFormat().format((await feed.posts()).length)} + {feed.type.human}
Total: + { + Intl.NumberFormat().format( + (await FeedManager.allPosts()).length, + ) + } + {}
+
diff --git a/src/pages/pulse/subscribe/index.astro b/src/pages/pulse/subscribe/index.astro index 0ca5ae4..f657cf8 100644 --- a/src/pages/pulse/subscribe/index.astro +++ b/src/pages/pulse/subscribe/index.astro @@ -8,10 +8,10 @@ import { FeedManager } from "@/features/feeds/feedManager";

Subscribe with your Feed Reader

- Although the upstream sources on Pulse are published in a variety of - different formats, we have gone to the trouble of re-exporting every - source as a non-paginated RSS feed for you. If you don't know what RSS is, - you can learn more at

- diff --git a/src/stores/postsQuery.ts b/src/stores/postsQuery.ts index a819ab4..03b1747 100644 --- a/src/stores/postsQuery.ts +++ b/src/stores/postsQuery.ts @@ -9,7 +9,7 @@ const defaultQueryOpts: QueryOpts = { }, paginate: { page: 1, - pageSize: 20, + maxPageSize: 20, }, }; diff --git a/src/util/paginateArray.ts b/src/util/paginateArray.ts index 1be235b..9b661a0 100644 --- a/src/util/paginateArray.ts +++ b/src/util/paginateArray.ts @@ -5,9 +5,11 @@ export function paginateArray( data: T[], opts: PaginateOpts, ): PaginatedResults { - const firstItemIndex = (opts.page - 1) * opts.pageSize; - const lastItemIndex = firstItemIndex + opts.pageSize - 1; - const totalPages = Math.ceil(data.length / opts.pageSize); + const firstItemIndex = (opts.page - 1) * opts.maxPageSize; + const lastItemIndex = firstItemIndex + opts.maxPageSize - 1; + /** if the last item would be greater than the length of the array, set it to the last index */ + const realLastItemIndex = Math.min(lastItemIndex, data.length - 1); + const totalPages = Math.ceil(data.length / opts.maxPageSize); const items = data.slice(firstItemIndex, lastItemIndex + 1); @@ -15,9 +17,10 @@ export function paginateArray( items, pagination: { page: opts.page, - pageSize: opts.pageSize, + maxPageSize: opts.maxPageSize, + pageSize: items.length, firstItemIndex, - lastItemIndex, + lastItemIndex: realLastItemIndex, totalItems: data.length, totalPages, }, @@ -26,8 +29,8 @@ export function paginateArray( type PaginateOpts = z.infer; export const paginateOptsSchema = z.object({ - /** the page size */ - pageSize: z.coerce.number().min(1), + /** the maximum page size */ + maxPageSize: z.coerce.number().min(1), /** the page number */ page: z.coerce.number().min(1), }); @@ -41,6 +44,8 @@ type PaginationResultsMetadata = { /** the current page number */ page: number; /** the maximum number of items per page */ + maxPageSize: number; + /** the number of items on the current page */ pageSize: number; /** the start index of the items on this page */ firstItemIndex: number;