* feat: add structured filters to backlinks tables Add per-tab filter panels (Backlinks, Referring Domains, Top Pages) with include/exclude text search, numeric range filters, link type selector, and visibility toggles. Filter values persist in localStorage across searches and page reloads. * fix: improve backlinks filter layout and hide domain overview filters on pages tab - Add w-full to text filter inputs so they fill their grid columns - Restructure backlinks results card to use border-separated sections matching the domain overview pattern - Hide filter button and panel on domain overview's Top Pages tab since keyword filters don't apply there * fix: move backlinks filter button to its own toolbar row below tabs Match the domain overview layout: tabs in their own header row, then a separate toolbar row with the Filters button underneath. * style: fix prettier formatting in backlinks filter files * fix: resolve all ci:check issues - Remove unused BacklinksResultsHeader.tsx and useBacklinksSpamPreferences.ts - Un-export DEFAULT_BACKLINKS_SPAM_THRESHOLD and normalizeBacklinksSpamThreshold (only used internally) - Remove unused BacklinksAllFilterValues type - Fix oxlint unsafe type assertions in useBacklinksFilters.ts * fix: wire spam filter options through to server and remove spam filters from non-backlinks tabs The client was not passing hideSpam/spamThreshold to the server, so the server always defaulted to hideSpam:true with threshold 40. This made the new client-side spam score filters on the backlinks tab silently filter an already-truncated dataset. Now the client sends hideSpam:false so the server returns all rows and client-side filtering works correctly. Removed spam score filters from the Referring Domains tab since spam filtering is not needed there. * fix: restore backlinks spam filtering and safe filter hydration * simplify: remove server-side spam filter wiring, keep client-side only All backlinks filtering (including spam score) is now done client-side. Server-side spam filtering adds complexity without meaningful benefit at current data volumes. This removes the bridging code added in the last two commits and simplifies filter hydration.
93 lines
2.7 KiB
TypeScript
93 lines
2.7 KiB
TypeScript
import { createServerFn } from "@tanstack/react-start";
|
|
import {
|
|
buildBacklinksDisabledAccessStatus,
|
|
setBacklinksAccessStatus,
|
|
} from "@/server/features/backlinks/backlinksAccess";
|
|
import { BacklinksService } from "@/server/features/backlinks/services/BacklinksService";
|
|
import { AppError } from "@/server/lib/errors";
|
|
import { requireProjectContext } from "@/serverFunctions/middleware";
|
|
import { backlinksOverviewInputSchema } from "@/types/schemas/backlinks";
|
|
|
|
export const getBacklinksOverview = createServerFn({
|
|
method: "POST",
|
|
})
|
|
.middleware(requireProjectContext)
|
|
.inputValidator((data: unknown) => backlinksOverviewInputSchema.parse(data))
|
|
.handler(async ({ data, context }) => {
|
|
try {
|
|
const input = {
|
|
target: data.target,
|
|
scope: data.scope,
|
|
};
|
|
const spamOptions = {
|
|
hideSpam: data.hideSpam,
|
|
spamThreshold: data.spamThreshold,
|
|
};
|
|
const profile = await BacklinksService.profileOverview(
|
|
input,
|
|
context,
|
|
spamOptions,
|
|
);
|
|
return profile.overview;
|
|
} catch (error) {
|
|
if (error instanceof AppError && error.code === "BACKLINKS_NOT_ENABLED") {
|
|
const checkedAt = new Date().toISOString();
|
|
await setBacklinksAccessStatus(
|
|
buildBacklinksDisabledAccessStatus(checkedAt, error.code),
|
|
);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
export const getBacklinksReferringDomains = createServerFn({
|
|
method: "POST",
|
|
})
|
|
.middleware(requireProjectContext)
|
|
.inputValidator((data: unknown) => backlinksOverviewInputSchema.parse(data))
|
|
.handler(async ({ data, context }) => {
|
|
try {
|
|
const input = {
|
|
target: data.target,
|
|
scope: data.scope,
|
|
};
|
|
const profile = await BacklinksService.profileReferringDomains(
|
|
input,
|
|
context,
|
|
);
|
|
return profile.rows;
|
|
} catch (error) {
|
|
await updateBacklinksAccessStatusOnError(error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
export const getBacklinksTopPages = createServerFn({
|
|
method: "POST",
|
|
})
|
|
.middleware(requireProjectContext)
|
|
.inputValidator((data: unknown) => backlinksOverviewInputSchema.parse(data))
|
|
.handler(async ({ data, context }) => {
|
|
try {
|
|
const input = {
|
|
target: data.target,
|
|
scope: data.scope,
|
|
};
|
|
const profile = await BacklinksService.profileTopPages(input, context);
|
|
return profile.rows;
|
|
} catch (error) {
|
|
await updateBacklinksAccessStatusOnError(error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
async function updateBacklinksAccessStatusOnError(error: unknown) {
|
|
if (error instanceof AppError && error.code === "BACKLINKS_NOT_ENABLED") {
|
|
const checkedAt = new Date().toISOString();
|
|
await setBacklinksAccessStatus(
|
|
buildBacklinksDisabledAccessStatus(checkedAt, error.code),
|
|
);
|
|
}
|
|
}
|