From 428d4703c9ae6307f3de27524d4b998f53d8b4e2 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sat, 28 Jun 2025 20:28:06 -0400 Subject: [PATCH 01/19] Adds initial getAuthorWorks for draft PR start --- src/author/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/author/index.ts diff --git a/src/author/index.ts b/src/author/index.ts new file mode 100644 index 0000000..1daf20d --- /dev/null +++ b/src/author/index.ts @@ -0,0 +1,9 @@ +// I'll move this to the right spot, just a temporary thing to get a draft PR going +interface IgetAuthorWorks { + username: string; + page: number; +} + +export const getAuthorWorks = ({ username, page = 0}) => { + +} From ac800c345432a1238c217616ff5de7a87d7a56a5 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sat, 28 Jun 2025 20:38:02 -0400 Subject: [PATCH 02/19] Adds a url function for getting userWorksUrl --- src/urls.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/urls.ts b/src/urls.ts index 90c26ab..0d7843c 100644 --- a/src/urls.ts +++ b/src/urls.ts @@ -25,6 +25,9 @@ export const getWorkUrl = ({ export const getUserProfileUrl = ({ username }: { username: string }) => `https://archiveofourown.org/users/${encodeURI(username)}/profile`; +export const getUserWorksUrl = ({ username, page = 0 }: { username: string, page?: number }) => + `https://archiveofourown.org/users/${encodeURI(username)}/works?page=${page}` + export const getTagUrl = (tagName: string) => `https://archiveofourown.org/tags/${encodeURI(tagName) .replaceAll("/", "*s*") From 933067d51446800058e5dddded1641ce601dc8a9 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sat, 28 Jun 2025 20:38:58 -0400 Subject: [PATCH 03/19] Adds an initial pageLoader I think the ideal solution is to have a loadUserWorksByPage that can then be extended into a `loadUserWorks` that loops and handles the pagination to generate one big list --- src/page-loaders.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/page-loaders.ts b/src/page-loaders.ts index b4e6b00..ff337cb 100644 --- a/src/page-loaders.ts +++ b/src/page-loaders.ts @@ -3,6 +3,7 @@ import { getTagWorksFeedAtomUrl, getTagWorksFeedUrl, getUserProfileUrl, + getUserWorksUrl, getWorkUrl, } from "./urls"; @@ -101,6 +102,15 @@ export const loadUserProfilePage = async ({ }); }; +export interface UserWorksPage extends CheerioAPI { + kind: 'UserWorksPage' +} +export const loadUserWorksList = async ({ username }: { username: string }) => { + return await fetchPage({ + url: getUserWorksUrl({ username }), + }); +} + export interface ChapterIndexPage extends CheerioAPI { kind: "ChapterIndexPage"; } From bebec3eb4e9ad6fb61ef30740b07f375b6ca513d Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sat, 28 Jun 2025 20:58:16 -0400 Subject: [PATCH 04/19] Removes author file --- src/author/index.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/author/index.ts diff --git a/src/author/index.ts b/src/author/index.ts deleted file mode 100644 index 1daf20d..0000000 --- a/src/author/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// I'll move this to the right spot, just a temporary thing to get a draft PR going -interface IgetAuthorWorks { - username: string; - page: number; -} - -export const getAuthorWorks = ({ username, page = 0}) => { - -} From 931d0a2bf02209082f02d13961203374b194282d Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sat, 28 Jun 2025 20:58:36 -0400 Subject: [PATCH 05/19] Adds pseudo coded idea for the getUserWorks function --- src/users/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index f13c475..b52ea1b 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -15,9 +15,9 @@ import { getUserProfileWorks, } from "./getters"; -import { User } from "types/entities"; +import { User, WorkSummary } from "types/entities"; import { getUserProfileUrl } from "../urls"; -import { loadUserProfilePage } from "../page-loaders"; +import { loadUserProfilePage, loadUserWorksList, loadWorkPage } from "../page-loaders"; export const getUser = async ({ username, @@ -46,3 +46,13 @@ export const getUser = async ({ bioHtml: getUserProfileBio(profilePage), }; }; + +export const getUserWorks = async ({ username }: {username: string}): Promise => { + const worksPage = await loadUserWorksList({ username }); + // parse current works page + // check for next page + // if next page + // loop it + // else return data + return []; +} From 344dcbc1c25cbe30c1e0fecc95f527ea64cb843f Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:02:11 -0400 Subject: [PATCH 06/19] Reorganizes the data based on previous work I believe the updatedAt might be available on the works list, as there is a date in the top right corner but published at is not available --- types/entities.ts | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/types/entities.ts b/types/entities.ts index 0b3ee43..66f76ae 100644 --- a/types/entities.ts +++ b/types/entities.ts @@ -123,14 +123,10 @@ export interface Author { anonymous: boolean; } -export interface WorkSummary { +export interface WorkPreview { id: string; title: string; category: WorkCategory[] | null; - // Date in ISO format. See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString - // Note that AO3 doesn't publish the actual time of publish, just the date. - publishedAt: string; - updatedAt: string | null; // TODO: should this be in HTML? summary: string | null; rating: WorkRatings; @@ -144,30 +140,37 @@ export interface WorkSummary { relationships: string[]; additional: string[]; }; + language: string; + words: number; + complete: boolean; + series: BasicSeries[]; + stats: { + bookmarks: number; + comments: number; + kudos: number; + hits: number; + }; + locked: false; // If the author is anonymous this array will contain a single // entry whose "anonymous" property is "true". authors: Author[]; - language: string; - words: number; chapters: { published: number; total: number | null; }; +} + +export interface WorkSummary extends WorkPreview { + // Date in ISO format. See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + // Note that AO3 doesn't publish the actual time of publish, just the date. + publishedAt: string; + updatedAt: string | null; chapterInfo: { id: string; index: number; name: string | null; summary: string | null; } | null; - series: BasicSeries[]; - complete: boolean; - stats: { - bookmarks: number; - comments: number; - kudos: number; - hits: number; - }; - locked: false; } export interface LockedWorkSummary { From fa7d3f3c4de7b47e4798512c79a0d45d8901bcfe Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:07:59 -0400 Subject: [PATCH 07/19] Adds udpatedAt to WorkPreview since there is always a date in the top right corner of the work preview, it will always be a string. An actual work would have a publishedAt and *could* have an updatedAt --- types/entities.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/types/entities.ts b/types/entities.ts index 66f76ae..e3dc0ea 100644 --- a/types/entities.ts +++ b/types/entities.ts @@ -158,6 +158,9 @@ export interface WorkPreview { published: number; total: number | null; }; + // Date in ISO format. See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + // Note that AO3 doesn't publish the actual time of publish, just the date. + updatedAt: string; } export interface WorkSummary extends WorkPreview { From c0234e096cd861e1dc2e2b992eec4f4e8370bc59 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:10:05 -0400 Subject: [PATCH 08/19] Removes updatedAtt from WorkSummary Now updatedAt will reflect publishedAt if it has not been updated --- types/entities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/entities.ts b/types/entities.ts index e3dc0ea..43478e4 100644 --- a/types/entities.ts +++ b/types/entities.ts @@ -167,7 +167,6 @@ export interface WorkSummary extends WorkPreview { // Date in ISO format. See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString // Note that AO3 doesn't publish the actual time of publish, just the date. publishedAt: string; - updatedAt: string | null; chapterInfo: { id: string; index: number; @@ -189,3 +188,4 @@ export interface Chapter { publishedAt: string; url: string; } + From 615db7a6782d99c249d0bdc1f604f454127ec3b6 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:22:29 -0400 Subject: [PATCH 09/19] Adds initial implementation Data structure is still a little goofy and doesn't match what was discussed in the discord --- src/users/index.ts | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index b52ea1b..4bc2e77 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -15,9 +15,9 @@ import { getUserProfileWorks, } from "./getters"; -import { User, WorkSummary } from "types/entities"; +import { User, WorkPreview, WorkSummary } from "types/entities"; import { getUserProfileUrl } from "../urls"; -import { loadUserProfilePage, loadUserWorksList, loadWorkPage } from "../page-loaders"; +import { loadUserProfilePage, loadUserWorksList, loadWorkPage, UserWorksPage } from "../page-loaders"; export const getUser = async ({ username, @@ -47,12 +47,47 @@ export const getUser = async ({ }; }; -export const getUserWorks = async ({ username }: {username: string}): Promise => { +const parseUserWorksIntoObject = ($userWorks: UserWorksPage) => { + /** + * It's just easier for me to reason this way, + * I can move this into a more correct file later + */ + const itemSelector = '.index.work.group > li'; + const selectors = { + kudos: '.kudos + .kudos a', + comments: '.comments + .comments a', + chapters: '.chapters + .chapters a', + words: '.words + .words', + hits: '.hits + .hits', + bookmarks: '.bookmarks + .bookmarks a', + title: '.heading a:first-child', + fandom: '.fandoms a', + category: '.category .text', + rating: '.rating .text', + warnings: '.warnings .tag', + complete: '.iswip .text' + } + const numberKeys = ['kudos','comments','chapters','words','hits','bookmarks'] + const works = []; + // unfortunately $userWorks(selector).map doesn't return an Array, it returns a Cheerio + $userWorks(itemSelector).each((_i, el) => { + const data = {}; + const $item = $userWorks(el); + for (const [key, selector] of Object.entries(selectors)) { + data[key] = numberKeys.includes(key) ? parseInt($item.find(selector).text(), 10) : $item.find(selector).text(); + } + works.push(data); + }) + + return works +} + +export const getUserWorks = async ({ username }: { username: string }): Promise => { const worksPage = await loadUserWorksList({ username }); // parse current works page // check for next page // if next page - // loop it + // loop it // else return data return []; } From 566eab32698c9ed84520420c54fb6e16f2b0aa37 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sun, 29 Jun 2025 11:00:57 -0400 Subject: [PATCH 10/19] Adds missing page param to loadUserWorksList --- src/page-loaders.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/page-loaders.ts b/src/page-loaders.ts index ff337cb..074ca60 100644 --- a/src/page-loaders.ts +++ b/src/page-loaders.ts @@ -105,9 +105,9 @@ export const loadUserProfilePage = async ({ export interface UserWorksPage extends CheerioAPI { kind: 'UserWorksPage' } -export const loadUserWorksList = async ({ username }: { username: string }) => { +export const loadUserWorksList = async ({ username, page = 0 }: { username: string, page: number }) => { return await fetchPage({ - url: getUserWorksUrl({ username }), + url: getUserWorksUrl({ username, page }), }); } From 16448c30e8ca4589b7caa671f5e72a74db885e87 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sun, 29 Jun 2025 11:03:24 -0400 Subject: [PATCH 11/19] Addds initial set up for a `getUserWorks` function currently the function does not automatically paginate, might want to think about changing the name to reflect that Since that is the case, this could be a good time to add in the caching feature, will double check with boba first --- src/users/index.ts | 75 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index 4bc2e77..883c291 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -65,7 +65,8 @@ const parseUserWorksIntoObject = ($userWorks: UserWorksPage) => { category: '.category .text', rating: '.rating .text', warnings: '.warnings .tag', - complete: '.iswip .text' + complete: '.iswip .text', + datetime: '.header.module .datetime', } const numberKeys = ['kudos','comments','chapters','words','hits','bookmarks'] const works = []; @@ -73,6 +74,10 @@ const parseUserWorksIntoObject = ($userWorks: UserWorksPage) => { $userWorks(itemSelector).each((_i, el) => { const data = {}; const $item = $userWorks(el); + /** + * Parse into a number if it is a number data point + * otherwise pass in the text + */ for (const [key, selector] of Object.entries(selectors)) { data[key] = numberKeys.includes(key) ? parseInt($item.find(selector).text(), 10) : $item.find(selector).text(); } @@ -82,12 +87,74 @@ const parseUserWorksIntoObject = ($userWorks: UserWorksPage) => { return works } -export const getUserWorks = async ({ username }: { username: string }): Promise => { - const worksPage = await loadUserWorksList({ username }); +interface GetUserWorks { + username: string; + // very unsure about name + counts: { + works: number; + series: number; + bookmarks: number; + collections: number; + gifts: number; + } + pageInfo: { + currentPage: number; + totalPages: number; + } + worksInPage: WorkPreview[]; +} + +const getTotalPages = ($page: UserWorksPage) => { + const lastNumberPagination = $page('.pagination li:has(+ .next)'); + + return parseInt(lastNumberPagination.text(), 10); +} + +const getWorkCount = ($page: UserWorksPage) => { + const worksNavItem = $page('.navigation.actions:nth-child(2) li:first-child'); + return parseInt(worksNavItem.text().replaceAll(/\D/g, ''), 10); +} + +const getSeriesCount = ($page: UserWorksPage) => { + const seriesNavItem = $page('.navigation.actions:nth-child(2) li:nth-child(3)'); + return parseInt(seriesNavItem.text().replaceAll(/\D/g, ''), 10); +} + +const getBookmarksCount = ($page: UserWorksPage) => { + const bookmarksNavItem = $page('.navigation.actions:nth-child(2) li:nth-child(4)'); + return parseInt(bookmarksNavItem.text().replaceAll(/\D/g, ''), 10); +} + +const getCollectionsCount = ($page: UserWorksPage) => { + const collectionsNavItem = $page('.navigation.actions:nth-child(2) li:last-child'); + return parseInt(collectionsNavItem.text().replaceAll(/\D/g, ''), 10); +} + +const getGiftsCount = ($page: UserWorksPage) => { + const giftsNavItem = $page('.navigation.actions:last-child li:last-child'); + return parseInt(giftsNavItem.text().replaceAll(/\D/g, ''), 10); +} + +export const getUserWorks = async ({ username, page = 0 }: { username: string, page: number }): Promise => { + const worksPage = await loadUserWorksList({ username, page }); // parse current works page // check for next page // if next page // loop it // else return data - return []; + return { + username, + pageInfo: { + currentPage: page, + totalPages: getTotalPages(worksPage), + }, + counts: { + works: getWorkCount(worksPage), + series: getSeriesCount(worksPage), + bookmarks: getBookmarksCount(worksPage), + collections: getCollectionsCount(worksPage), + gifts: getGiftsCount(worksPage), + }, + worksInPage: parseUserWorksIntoObject(worksPage) + } } From 2f584bc92140fc7fcb5dfacc9a5a78f52e3eac28 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sun, 29 Jun 2025 11:09:22 -0400 Subject: [PATCH 12/19] Initial test set up still need to get the data parsing to look more like WorkPreview type --- tests/user.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/user.test.ts b/tests/user.test.ts index 4238a4e..0778c95 100644 --- a/tests/user.test.ts +++ b/tests/user.test.ts @@ -1,4 +1,4 @@ -import { getUser } from "src/index"; +import { getUser, getUserWorks } from "src/index"; import { User } from "types/entities"; //NOTE: Some of these tests may fail if the referenced user has updated their profile! @@ -44,4 +44,11 @@ describe("Fetches id data.", () => { header: "Yes, it's really spelled with a Z", } satisfies Partial); }); + + test('Fetches user works list', async () => { + const works = await getUserWorks({ + username: 'franzeska' + }); + console.log(works); + }) }); From 5677097fb5aa025010ca0bc09325a1e99933d3fe Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sun, 29 Jun 2025 11:12:47 -0400 Subject: [PATCH 13/19] Adds the UserWorks type --- types/entities.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/types/entities.ts b/types/entities.ts index 43478e4..bc56314 100644 --- a/types/entities.ts +++ b/types/entities.ts @@ -175,6 +175,24 @@ export interface WorkSummary extends WorkPreview { } | null; } + +export interface UserWorks { + username: string; + // very unsure about name + counts: { + works: number; + series: number; + bookmarks: number; + collections: number; + gifts: number; + } + pageInfo: { + currentPage: number; + totalPages: number; + } + worksInPage: WorkPreview[]; +} + export interface LockedWorkSummary { id: string; locked: true; From 4a11541f769fcd171548cce3f2e22ef98ce29053 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sun, 29 Jun 2025 11:14:51 -0400 Subject: [PATCH 14/19] Moves getters into getter file Tries to add proper typing to get tests running, but running into an index signature complaint :rolling_eyes: typical typescript hooha --- src/users/index.ts | 62 ++++++++-------------------------------------- 1 file changed, 10 insertions(+), 52 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index 883c291..fc66f56 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -1,4 +1,9 @@ import { + getBookmarksCount, + getCollectionsCount, + getGiftsCount, + getSeriesCount, + getTotalPages, getUserProfileBio, getUserProfileBirthday, getUserProfileBookmarks, @@ -13,6 +18,7 @@ import { getUserProfilePseuds, getUserProfileSeries, getUserProfileWorks, + getWorkCount, } from "./getters"; import { User, WorkPreview, WorkSummary } from "types/entities"; @@ -69,10 +75,10 @@ const parseUserWorksIntoObject = ($userWorks: UserWorksPage) => { datetime: '.header.module .datetime', } const numberKeys = ['kudos','comments','chapters','words','hits','bookmarks'] - const works = []; + const works: WorkPreview[] = []; // unfortunately $userWorks(selector).map doesn't return an Array, it returns a Cheerio $userWorks(itemSelector).each((_i, el) => { - const data = {}; + const data = {} as WorkPreview; const $item = $userWorks(el); /** * Parse into a number if it is a number data point @@ -81,61 +87,13 @@ const parseUserWorksIntoObject = ($userWorks: UserWorksPage) => { for (const [key, selector] of Object.entries(selectors)) { data[key] = numberKeys.includes(key) ? parseInt($item.find(selector).text(), 10) : $item.find(selector).text(); } - works.push(data); + works.push(data as WorkPreview); }) return works } -interface GetUserWorks { - username: string; - // very unsure about name - counts: { - works: number; - series: number; - bookmarks: number; - collections: number; - gifts: number; - } - pageInfo: { - currentPage: number; - totalPages: number; - } - worksInPage: WorkPreview[]; -} - -const getTotalPages = ($page: UserWorksPage) => { - const lastNumberPagination = $page('.pagination li:has(+ .next)'); - - return parseInt(lastNumberPagination.text(), 10); -} - -const getWorkCount = ($page: UserWorksPage) => { - const worksNavItem = $page('.navigation.actions:nth-child(2) li:first-child'); - return parseInt(worksNavItem.text().replaceAll(/\D/g, ''), 10); -} - -const getSeriesCount = ($page: UserWorksPage) => { - const seriesNavItem = $page('.navigation.actions:nth-child(2) li:nth-child(3)'); - return parseInt(seriesNavItem.text().replaceAll(/\D/g, ''), 10); -} - -const getBookmarksCount = ($page: UserWorksPage) => { - const bookmarksNavItem = $page('.navigation.actions:nth-child(2) li:nth-child(4)'); - return parseInt(bookmarksNavItem.text().replaceAll(/\D/g, ''), 10); -} - -const getCollectionsCount = ($page: UserWorksPage) => { - const collectionsNavItem = $page('.navigation.actions:nth-child(2) li:last-child'); - return parseInt(collectionsNavItem.text().replaceAll(/\D/g, ''), 10); -} - -const getGiftsCount = ($page: UserWorksPage) => { - const giftsNavItem = $page('.navigation.actions:last-child li:last-child'); - return parseInt(giftsNavItem.text().replaceAll(/\D/g, ''), 10); -} - -export const getUserWorks = async ({ username, page = 0 }: { username: string, page: number }): Promise => { +export const getUserWorks = async ({ username, page = 0 }: { username: string, page?: number }): Promise => { const worksPage = await loadUserWorksList({ username, page }); // parse current works page // check for next page From 8d69ad10e019cd7f838ce163e49bc26ba1ddde7f Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sun, 29 Jun 2025 11:15:01 -0400 Subject: [PATCH 15/19] Adds getters for UserWorksPage --- src/users/getters.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/users/getters.ts b/src/users/getters.ts index 1838f2b..06e830d 100644 --- a/src/users/getters.ts +++ b/src/users/getters.ts @@ -1,4 +1,4 @@ -import { UserProfile } from "../page-loaders"; +import { UserProfile, UserWorksPage } from "../page-loaders"; import { getUserProfileUrl } from "../urls"; //Dates are ten characters long in the following format: @@ -123,3 +123,35 @@ export const getUserProfileGifts = ($userProfile: UserProfile) => { .slice(GIFTS_PREFIX.length, -STAT_SUFFIX.length) || "0" ); }; + + +export const getTotalPages = ($page: UserWorksPage) => { + const lastNumberPagination = $page('.pagination li:has(+ .next)'); + + return parseInt(lastNumberPagination.text(), 10); +} + +export const getWorkCount = ($page: UserWorksPage) => { + const worksNavItem = $page('.navigation.actions:nth-child(2) li:first-child'); + return parseInt(worksNavItem.text().replaceAll(/\D/g, ''), 10); +} + +export const getSeriesCount = ($page: UserWorksPage) => { + const seriesNavItem = $page('.navigation.actions:nth-child(2) li:nth-child(3)'); + return parseInt(seriesNavItem.text().replaceAll(/\D/g, ''), 10); +} + +export const getBookmarksCount = ($page: UserWorksPage) => { + const bookmarksNavItem = $page('.navigation.actions:nth-child(2) li:nth-child(4)'); + return parseInt(bookmarksNavItem.text().replaceAll(/\D/g, ''), 10); +} + +export const getCollectionsCount = ($page: UserWorksPage) => { + const collectionsNavItem = $page('.navigation.actions:nth-child(2) li:last-child'); + return parseInt(collectionsNavItem.text().replaceAll(/\D/g, ''), 10); +} + +export const getGiftsCount = ($page: UserWorksPage) => { + const giftsNavItem = $page('.navigation.actions:last-child li:last-child'); + return parseInt(giftsNavItem.text().replaceAll(/\D/g, ''), 10); +} From 395a78e2b716a58cfaab3cde89a8967671b17d65 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sun, 29 Jun 2025 11:17:02 -0400 Subject: [PATCH 16/19] Adds lib option to tsconfig this stops errors in regards to string replaceAll not being supported --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 4089157..0cab153 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "src/*": ["./src/*"], "types/*": ["./types/*"] }, - "resolveJsonModule": true + "resolveJsonModule": true, + "lib": ["es2022"] }, "include": ["**/*.ts"], "exclude": ["node_modules/"] From 506ca6fbf908f6b8d1b6c0b6ec8e3959affc5dea Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Sun, 29 Jun 2025 11:21:54 -0400 Subject: [PATCH 17/19] Changes WorkPreview to extend Record This should solve the issues with not being able to do `data[key] = value` --- types/entities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/entities.ts b/types/entities.ts index bc56314..cba4602 100644 --- a/types/entities.ts +++ b/types/entities.ts @@ -123,7 +123,7 @@ export interface Author { anonymous: boolean; } -export interface WorkPreview { +export interface WorkPreview extends Record { id: string; title: string; category: WorkCategory[] | null; From 52ae3fae76e13eb0fc60c04da473545e9752bd52 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Tue, 15 Jul 2025 09:55:19 -0400 Subject: [PATCH 18/19] Fixes UserWorks type usage/naming --- src/users/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/users/index.ts b/src/users/index.ts index fc66f56..47b986a 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -21,7 +21,7 @@ import { getWorkCount, } from "./getters"; -import { User, WorkPreview, WorkSummary } from "types/entities"; +import { User, UserWorks, WorkPreview } from "types/entities"; import { getUserProfileUrl } from "../urls"; import { loadUserProfilePage, loadUserWorksList, loadWorkPage, UserWorksPage } from "../page-loaders"; @@ -93,7 +93,7 @@ const parseUserWorksIntoObject = ($userWorks: UserWorksPage) => { return works } -export const getUserWorks = async ({ username, page = 0 }: { username: string, page?: number }): Promise => { +export const getUserWorks = async ({ username, page = 0 }: { username: string, page?: number }): Promise => { const worksPage = await loadUserWorksList({ username, page }); // parse current works page // check for next page From d849b2d584e22f96ae134b9638cc1d176f0635c2 Mon Sep 17 00:00:00 2001 From: ginger <26461046+gingerchew@users.noreply.github.com> Date: Tue, 15 Jul 2025 09:56:07 -0400 Subject: [PATCH 19/19] Changes how updatedAt is used between WorkPreview and WorkSummary --- types/entities.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/types/entities.ts b/types/entities.ts index cba4602..85459f0 100644 --- a/types/entities.ts +++ b/types/entities.ts @@ -160,7 +160,7 @@ export interface WorkPreview extends Record { }; // Date in ISO format. See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString // Note that AO3 doesn't publish the actual time of publish, just the date. - updatedAt: string; + updatedAt: string|null; } export interface WorkSummary extends WorkPreview { @@ -175,6 +175,11 @@ export interface WorkSummary extends WorkPreview { } | null; } +export interface LockedWorkSummary { + id: string; + locked: true; +} + export interface UserWorks { username: string; @@ -193,10 +198,6 @@ export interface UserWorks { worksInPage: WorkPreview[]; } -export interface LockedWorkSummary { - id: string; - locked: true; -} export interface Chapter { id: string;