Skip to content

Commit

Permalink
Improve method for adding items to topic summary/landing page
Browse files Browse the repository at this point in the history
  • Loading branch information
nonprofittechy committed Nov 5, 2024
1 parent 29b8755 commit 7bc68e6
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 90 deletions.
8 changes: 2 additions & 6 deletions src/app/components/TopicCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const TopicCard = ({
const [isExpanded, setIsExpanded] = useState(false);
const visibilityClass = index > 8 ? 'hidden' : '';
const displayInterviews = isExpanded
? interviews.slice(0, Math.min(10, interviews.length))
? interviews.slice(0, Math.min(20, interviews.length))
: interviews.slice(0, 3);
const remainingCount = interviews.length > 10 ? interviews.length - 10 : 0;
const cardClassName = isSpot ? 'spot-topic-card-parent' : 'topic-card-parent';
Expand Down Expand Up @@ -90,11 +90,7 @@ const TopicCard = ({
if (interview.metadata && interview.metadata.title) {
return (
<Link
key={
toUrlFriendlyString(interview.metadata.title) +
'-' +
index
}
key={`${toUrlFriendlyString(interview.filename)}-${topic.name}-${index}`}
className={
styles.FormTag +
' form-tag btn btn-outline-secondary border-2 align-self-start text-start'
Expand Down
4 changes: 4 additions & 0 deletions src/app/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export const toUrlFriendlyString = (title: string) => {
// if title isn't a string, return a default
if (typeof title !== 'string') {
return 'Untitled';
}
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-') // Replace special characters with hyphens
Expand Down
14 changes: 1 addition & 13 deletions src/config/topics.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const legalTopics: Topic[] = [
priority: 100,
},
{
codes: ['FA-00-00-00-00'],
codes: ['FA-00-00-00-00', 'ES-03-00-00-00'],
name: 'family',
long_name: 'Family and safety',
icon: 'people-roof',
Expand Down Expand Up @@ -185,15 +185,3 @@ export const legalTopics: Topic[] = [
priority: 0,
},
];

export function findParentTopic(tag: string) {
// Remove trailing sections of "-00" from the tag
const cleanedTag = tag.replace(/(-00)+$/, '');

// Find the first topic that starts with the same characters as the cleaned tag
const parentTopic = legalTopics.find((topic) =>
cleanedTag.startsWith(topic.codes[0].replace(/(-00)+$/, ''))
);

return parentTopic; // This will be undefined if no matching topic is found
}
220 changes: 166 additions & 54 deletions src/data/fetchInterviewData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
import { formSources, pathToServerConfig } from '../config/formSources.config';
import { legalTopics } from '../config/topics.config';
import { excludedForms } from '../config/formSources.config';
import { findClosestTopic } from './helpers';
import { getTopicNames } from './helpers';

interface LocalizedString {
[key: string]: string;
}

interface RawInterview {
metadata?: {
unlisted?: boolean;
LIST_topics?: string[];
description?: string | LocalizedString;
can_I_use_this_form?: string | LocalizedString;
help_page_url?: string | LocalizedString;
help_page_title?: string | LocalizedString;
original_form?: string | LocalizedString;
before_you_start?: string | LocalizedString;
form_titles?: string[] | LocalizedString;
form_numbers?: string[] | LocalizedString;
jurisdiction?: string | LocalizedString;
maturity?: string | LocalizedString;
footer?: string | LocalizedString;
'short title'?: string | LocalizedString;
title?: string | LocalizedString;
[key: string]: any;
};
tags?: string[];
filename?: string;
title?: string | LocalizedString;
link?: string;
[key: string]: any;
}

interface Interview {
metadata: {
Expand All @@ -17,9 +47,13 @@ interface Interview {
form_numbers: string[];
jurisdiction: string;
maturity: string;
footer: string;
'short title': string;
title: string;
estimated_completion_minutes: number;
estimated_completion_delta: number;
languages: string[];
[key: string]: any;
};
tags: string[];
filename: string;
Expand All @@ -29,7 +63,41 @@ interface Interview {
}

interface Data {
interviews?: Interview[];
interviews?: RawInterview[];
}

// Helper function to extract localized strings
function extractLocalizedString(
value: string | LocalizedString | undefined,
locale: string
): string {
if (typeof value === 'string') {
return value;
} else if (value && typeof value === 'object') {
return value[locale] ?? value['en'] ?? Object.values(value)[0] ?? '';
} else {
return '';
}
}

// Helper function to extract localized arrays
function extractLocalizedArray(
value: string[] | LocalizedString | undefined,
locale: string
): string[] {
if (Array.isArray(value)) {
return value;
} else if (value && typeof value === 'object') {
const localizedValue =
value[locale] ?? value['en'] ?? Object.values(value)[0];
return Array.isArray(localizedValue)
? localizedValue
: localizedValue
? [localizedValue]
: [];
} else {
return [];
}
}

export const fetchInterviews = async (path: string) => {
Expand All @@ -41,6 +109,8 @@ export const fetchInterviews = async (path: string) => {
serverNames.includes(server.name)
);

const locale = 'en'; // Todo: make this dynamic

let allInterviews: Interview[] = [];
for (const server of servers) {
const url = new URL(`${server.url}/list`);
Expand All @@ -52,45 +122,87 @@ export const fetchInterviews = async (path: string) => {

if (data && data.interviews) {
const taggedInterviews = data.interviews
.filter((interview: Interview) => !interview.metadata?.unlisted) // exclude unlisted interviews safely
// exclude interviews with filenames that are in the excludedForms list relative to this server
.filter((interview: Interview) => {
// Check if an exclusion list exists for the server, and use it if available
.filter((interview: RawInterview) => !interview.metadata?.unlisted)
.filter((interview: RawInterview) => {
const exclusions = excludedForms[server.key];
const filename = interview.filename || ''; // safe fallback for filename
const filename = interview.filename ?? '';
if (exclusions) {
return !exclusions.includes(filename);
} else {
return true;
}
})
.map((interview: Interview) => ({
...interview,
serverUrl: server.url,
metadata: {
unlisted: interview.metadata?.unlisted ?? false,
LIST_topics: interview.metadata?.LIST_topics || [],
description: interview.metadata?.description || '',
can_I_use_this_form:
interview.metadata?.can_I_use_this_form || '',
help_page_url: interview.metadata?.help_page_url || '',
help_page_title: interview.metadata?.help_page_title || '',
original_form: interview.metadata?.original_form || '',
before_you_start: interview.metadata?.before_you_start || '',
form_titles: interview.metadata?.form_titles || [],
form_numbers: interview.metadata?.form_numbers || [],
jurisdiction: interview.metadata?.jurisdiction || '',
maturity: interview.metadata?.maturity || '',
estimated_completion_minutes:
interview.metadata?.estimated_completion_minutes ?? 0,
estimated_completion_delta:
interview.metadata?.estimated_completion_delta ?? 0,
languages: interview.metadata?.languages || [],
},
tags: interview.tags || [],
filename: interview.filename || '',
title: interview.title || '',
}));
.map(
(interview: RawInterview): Interview => ({
...interview,
serverUrl: server.url,
metadata: {
...interview.metadata,
unlisted: interview.metadata?.unlisted ?? false,
LIST_topics: interview.metadata?.LIST_topics ?? [],
description: extractLocalizedString(
interview.metadata?.description,
locale
),
can_I_use_this_form: extractLocalizedString(
interview.metadata?.can_I_use_this_form,
locale
),
help_page_url: extractLocalizedString(
interview.metadata?.help_page_url,
locale
),
help_page_title: extractLocalizedString(
interview.metadata?.help_page_title,
locale
),
original_form: extractLocalizedString(
interview.metadata?.original_form,
locale
),
before_you_start: extractLocalizedString(
interview.metadata?.before_you_start,
locale
),
form_titles: extractLocalizedArray(
interview.metadata?.form_titles,
locale
),
form_numbers: extractLocalizedArray(
interview.metadata?.form_numbers,
locale
),
jurisdiction: extractLocalizedString(
interview.metadata?.jurisdiction,
locale
),
maturity: extractLocalizedString(
interview.metadata?.maturity,
locale
),
footer: extractLocalizedString(
interview.metadata?.footer,
locale
),
'short title': extractLocalizedString(
interview.metadata?.['short title'],
locale
),
title: extractLocalizedString(
interview.metadata?.title,
locale
),
estimated_completion_minutes:
interview.metadata?.estimated_completion_minutes ?? 0,
estimated_completion_delta:
interview.metadata?.estimated_completion_delta ?? 0,
languages: interview.metadata?.languages ?? [],
},
tags: interview.tags ?? [],
filename: interview.filename ?? '',
title: extractLocalizedString(interview.title, locale),
})
);
allInterviews = allInterviews.concat(taggedInterviews);
}
} catch (error) {
Expand All @@ -102,6 +214,7 @@ export const fetchInterviews = async (path: string) => {
}
}

// Initialize interviewsByTopic and titlesInTopics with all topics
const interviewsByTopic: { [key: string]: Interview[] } = {};
const titlesInTopics: { [key: string]: Set<string> } = {};
legalTopics.forEach((topic) => {
Expand All @@ -112,29 +225,28 @@ export const fetchInterviews = async (path: string) => {

allInterviews.forEach((interview: Interview) => {
const uniqueTopics = new Set<string>();
const title = interview.title || ''; // safe fallback for title

// Match topics by metadata.LIST_topics and tags
(interview.metadata.LIST_topics || [])
.concat(interview.tags || [])
.forEach((code: string) => {
const topic = findClosestTopic(code, legalTopics);
if (topic) {
const topicName = topic.name.toLowerCase();
if (!uniqueTopics.has(topicName)) {
uniqueTopics.add(topicName);
if (!titlesInTopics[topicName].has(title)) {
interviewsByTopic[topicName].push(interview);
titlesInTopics[topicName].add(title);
}
}
}
});
const title = interview.title;

// Collect all codes from metadata.LIST_topics and tags
const codes = [...interview.metadata.LIST_topics, ...interview.tags];

// Get matching topic names
const topicNames = getTopicNames(codes);

topicNames.forEach((topicName) => {
topicName = topicName.toLowerCase();
//if (!uniqueTopics.has(topicName)) {
uniqueTopics.add(topicName);
if (!titlesInTopics[topicName].has(title)) {
interviewsByTopic[topicName].push(interview);
titlesInTopics[topicName].add(title);
}
//}
});

// If no topics matched, add to 'other'
if (uniqueTopics.size === 0) {
const otherTopic = 'other';
interviewsByTopic[otherTopic] = interviewsByTopic[otherTopic] || [];
titlesInTopics[otherTopic] = titlesInTopics[otherTopic] || new Set();
if (!titlesInTopics[otherTopic].has(title)) {
interviewsByTopic[otherTopic].push(interview);
titlesInTopics[otherTopic].add(title);
Expand Down
50 changes: 34 additions & 16 deletions src/data/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
export function findClosestTopic(topicCode, legalTopics) {
const prefix = topicCode.slice(0, 2);
const numericCode = parseInt(topicCode.slice(3).replace(/-/g, ''), 10);
let closestTopic;
let smallestDiff = Infinity;
import { legalTopics } from '../../config/topics.config';

// Find topics by closest, smaller topic code with matching prefix
legalTopics.forEach((topic) => {
topic.codes.forEach((code) => {
if (code.startsWith(prefix)) {
const topicNumeric = parseInt(code.slice(3).replace(/-/g, ''), 10);
const diff = numericCode - topicNumeric;
if (diff >= 0 && diff < smallestDiff) {
smallestDiff = diff;
closestTopic = topic;
function getTopicNames(
tags: string[],
defaultName: string = 'other'
): string[] {
const topicNames = new Set<string>();

// Preprocess topics' codes by cleaning them
const topicsWithCleanCodes = legalTopics.map((topic) => ({
...topic,
cleanedCodes: topic.codes.map((code) => code.replace(/(-00)+$/, '')),
}));

if (tags.length === 0) {
topicNames.add(defaultName);
} else {
tags.forEach((tag) => {
const cleanedTag = tag.replace(/(-00)+$/, '');
for (const topic of topicsWithCleanCodes) {
for (const cleanedCode of topic.cleanedCodes) {
if (cleanedTag.startsWith(cleanedCode)) {
topicNames.add(topic.name);
// We don't break here because multiple topics might match
}
}
}
});
});
return closestTopic;

// If none of the tags matched any topic, add the default topic
if (!(topicNames.size > 0)) {
topicNames.add(defaultName);
}
}

return Array.from(topicNames);
}

export { getTopicNames };
Loading

0 comments on commit 7bc68e6

Please sign in to comment.