Skip to content

Commit 53a5970

Browse files
committed
feat: implement rich, highlighted search results
- **Rich Results UI:** Search results are now displayed in a structured, boxed layout. Each result is presented as a card showing its category (e.g., Experience, Project), a title, and the relevant content. - **Term Highlighting:** Matching keywords are now visually highlighted within the search results, making them easy to spot and scan. - **Granular Search Logic:** The search functionality is significantly improved to be more precise, checking individual fields across all sections of the resume data. - **Enhanced Usage & Empty State:** - Running `search` without arguments now shows a helpful usage guide. - A user-friendly message is displayed when no results are found. - **Consistent Design:** The new search UI aligns with the modern, boxed layout of other commands, ensuring a cohesive user experience. This commit completely overhauls the `search` command, replacing the previous plain-text output with a rich, interactive, and more powerful user interface.
1 parent 3d56b4f commit 53a5970

1 file changed

Lines changed: 215 additions & 30 deletions

File tree

client/src/hooks/useTerminal.ts

Lines changed: 215 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -795,62 +795,243 @@ export function useTerminal({ portfolioData }: UseTerminalProps) {
795795

796796
const searchTerm = args.join(' ').toLowerCase();
797797
if (!searchTerm) {
798-
addLine('<span class="text-terminal-yellow">Usage: search [term]</span>');
799-
addLine('<span class="text-white">Example: search python</span>');
798+
const usageBox = `
799+
<div class="border border-terminal-green/50 rounded-sm mb-4 terminal-glow">
800+
<div class="border-b border-terminal-green/30 px-3 py-1 text-center">
801+
<span class="text-terminal-bright-green text-sm font-bold">SEARCH USAGE</span>
802+
</div>
803+
<div class="p-3 space-y-3 text-xs sm:text-sm">
804+
<div>
805+
<div class="text-terminal-yellow font-bold mb-2">📋 USAGE</div>
806+
<div class="ml-2 space-y-1">
807+
<div class="text-white bg-terminal-green/5 p-2 rounded">
808+
<span class="text-terminal-bright-green font-semibold">search</span> <span class="text-terminal-yellow">[term]</span>
809+
</div>
810+
</div>
811+
</div>
812+
<div>
813+
<div class="text-terminal-yellow font-bold mb-2">💡 EXAMPLES</div>
814+
<div class="ml-2 space-y-1">
815+
<div class="text-white bg-terminal-green/5 p-2 rounded">
816+
<span class="text-terminal-bright-green">search</span> python
817+
</div>
818+
<div class="text-white bg-terminal-green/5 p-2 rounded">
819+
<span class="text-terminal-bright-green">search</span> react
820+
</div>
821+
<div class="text-white bg-terminal-green/5 p-2 rounded">
822+
<span class="text-terminal-bright-green">search</span> machine learning
823+
</div>
824+
</div>
825+
</div>
826+
</div>
827+
</div>
828+
`.trim();
829+
830+
addLine(usageBox, 'w-full');
800831
return;
801832
}
802833

803-
addLine(`<span class="text-terminal-bright-green">Searching for: "${searchTerm}"</span>`);
804-
addLine('');
805-
806-
const results: string[] = [];
834+
const results: Array<{category: string, title: string, content: string}> = [];
807835
const { cv } = portfolioData;
808836

837+
// Helper function to highlight search terms
838+
const highlightMatch = (text: string, term: string): string => {
839+
const regex = new RegExp(`(${term})`, 'gi');
840+
return text.replace(regex, '<span class="bg-terminal-yellow/30 text-terminal-bright-green font-semibold">$1</span>');
841+
};
842+
843+
// Search in intro/about section
809844
cv.sections.intro.forEach((intro, i) => {
810845
if (intro.toLowerCase().includes(searchTerm)) {
811-
results.push(`About (intro ${i + 1}): ${intro.substring(0, 100)}...`);
846+
results.push({
847+
category: 'About',
848+
title: `Introduction ${i + 1}`,
849+
content: highlightMatch(intro, searchTerm)
850+
});
812851
}
813852
});
814853

854+
// Search in technologies/skills
815855
cv.sections.technologies.forEach(tech => {
816856
if (tech.label.toLowerCase().includes(searchTerm) || tech.details.toLowerCase().includes(searchTerm)) {
817-
results.push(`Skills: ${tech.label} - ${tech.details}`);
857+
const matchedContent = [];
858+
if (tech.label.toLowerCase().includes(searchTerm)) {
859+
matchedContent.push(`Technology: ${highlightMatch(tech.label, searchTerm)}`);
860+
}
861+
if (tech.details.toLowerCase().includes(searchTerm)) {
862+
matchedContent.push(`Details: ${highlightMatch(tech.details, searchTerm)}`);
863+
}
864+
results.push({
865+
category: 'Skills',
866+
title: tech.label,
867+
content: matchedContent.join('<br>')
868+
});
818869
}
819870
});
820871

872+
// Search in experience
821873
cv.sections.experience.forEach(exp => {
822-
const searchableText = `${exp.company} ${exp.position} ${exp.location} ${exp.highlights.join(' ')}`.toLowerCase();
823-
if (searchableText.includes(searchTerm)) {
824-
results.push(`Experience: ${exp.position} at ${exp.company}`);
874+
const matchedContent = [];
875+
876+
if (exp.company.toLowerCase().includes(searchTerm)) {
877+
matchedContent.push(`Company: ${highlightMatch(exp.company, searchTerm)}`);
878+
}
879+
if (exp.position.toLowerCase().includes(searchTerm)) {
880+
matchedContent.push(`Position: ${highlightMatch(exp.position, searchTerm)}`);
881+
}
882+
if (exp.location.toLowerCase().includes(searchTerm)) {
883+
matchedContent.push(`Location: ${highlightMatch(exp.location, searchTerm)}`);
884+
}
885+
886+
// Check highlights for matches
887+
exp.highlights.forEach((highlight, index) => {
888+
if (highlight.toLowerCase().includes(searchTerm)) {
889+
matchedContent.push(`Highlight ${index + 1}: ${highlightMatch(highlight, searchTerm)}`);
890+
}
891+
});
892+
893+
if (matchedContent.length > 0) {
894+
results.push({
895+
category: 'Experience',
896+
title: `${exp.position} at ${exp.company}`,
897+
content: matchedContent.join('<br>')
898+
});
899+
}
900+
});
901+
902+
// Search in education
903+
cv.sections.education.forEach(edu => {
904+
const matchedContent = [];
905+
906+
if (edu.institution.toLowerCase().includes(searchTerm)) {
907+
matchedContent.push(`Institution: ${highlightMatch(edu.institution, searchTerm)}`);
908+
}
909+
if (edu.degree.toLowerCase().includes(searchTerm)) {
910+
matchedContent.push(`Degree: ${highlightMatch(edu.degree, searchTerm)}`);
911+
}
912+
if (edu.area.toLowerCase().includes(searchTerm)) {
913+
matchedContent.push(`Major: ${highlightMatch(edu.area, searchTerm)}`);
914+
}
915+
if (edu.location.toLowerCase().includes(searchTerm)) {
916+
matchedContent.push(`Location: ${highlightMatch(edu.location, searchTerm)}`);
917+
}
918+
919+
// Check highlights for matches
920+
edu.highlights.forEach((highlight, index) => {
921+
if (highlight.toLowerCase().includes(searchTerm)) {
922+
matchedContent.push(`Highlight ${index + 1}: ${highlightMatch(highlight, searchTerm)}`);
923+
}
924+
});
925+
926+
if (matchedContent.length > 0) {
927+
results.push({
928+
category: 'Education',
929+
title: `${edu.degree} in ${edu.area} from ${edu.institution}`,
930+
content: matchedContent.join('<br>')
931+
});
825932
}
826933
});
827934

935+
// Search in professional projects
828936
cv.sections.selected_projects.forEach(proj => {
829-
const searchableText = `${proj.name} ${proj.highlights.join(' ')}`.toLowerCase();
830-
if (searchableText.includes(searchTerm)) {
831-
results.push(`Project: ${proj.name}`);
937+
const matchedContent = [];
938+
939+
if (proj.name.toLowerCase().includes(searchTerm)) {
940+
matchedContent.push(`Project: ${highlightMatch(proj.name, searchTerm)}`);
941+
}
942+
943+
// Check highlights for matches
944+
proj.highlights.forEach((highlight, index) => {
945+
if (highlight.toLowerCase().includes(searchTerm)) {
946+
matchedContent.push(`Detail ${index + 1}: ${highlightMatch(highlight, searchTerm)}`);
947+
}
948+
});
949+
950+
if (matchedContent.length > 0) {
951+
results.push({
952+
category: 'Professional Projects',
953+
title: proj.name,
954+
content: matchedContent.join('<br>')
955+
});
832956
}
833957
});
834958

959+
// Search in personal projects
835960
cv.sections.personal_projects.forEach(proj => {
836-
const searchableText = `${proj.name} ${proj.highlights.join(' ')}`.toLowerCase();
837-
if (searchableText.includes(searchTerm)) {
838-
results.push(`Personal Project: ${proj.name}`);
961+
const matchedContent = [];
962+
963+
if (proj.name.toLowerCase().includes(searchTerm)) {
964+
matchedContent.push(`Project: ${highlightMatch(proj.name, searchTerm)}`);
965+
}
966+
967+
// Check highlights for matches
968+
proj.highlights.forEach((highlight, index) => {
969+
if (highlight.toLowerCase().includes(searchTerm)) {
970+
matchedContent.push(`Detail ${index + 1}: ${highlightMatch(highlight, searchTerm)}`);
971+
}
972+
});
973+
974+
if (matchedContent.length > 0) {
975+
results.push({
976+
category: 'Personal Projects',
977+
title: proj.name,
978+
content: matchedContent.join('<br>')
979+
});
839980
}
840981
});
841982

842-
if (results.length === 0) {
843-
addLine('<span class="text-terminal-yellow">No results found</span>');
844-
} else {
845-
addLine(`<span class="text-terminal-green">Found ${results.length} result(s):</span>`);
846-
addLine('');
847-
results.forEach((result, index) => {
848-
setTimeout(() => {
849-
addLine(`<span class="text-white">• ${result}</span>`);
850-
}, index * 100);
851-
});
852-
}
853-
}, [addLine, portfolioData]);
983+
// Create the search results box
984+
const searchBox = `
985+
<div class="border border-terminal-green/50 rounded-sm mb-4 terminal-glow max-w-4xl">
986+
<div class="border-b border-terminal-green/30 px-3 py-1 text-center">
987+
<span class="text-terminal-bright-green text-sm font-bold">SEARCH RESULTS</span>
988+
</div>
989+
<div class="p-3 space-y-3 text-xs sm:text-sm">
990+
<div class="bg-terminal-green/5 p-2 rounded">
991+
<span class="text-terminal-yellow font-semibold">Search term:</span>
992+
<span class="text-white"> "${searchTerm}"</span>
993+
</div>
994+
<div class="bg-terminal-green/5 p-2 rounded">
995+
<span class="text-terminal-bright-green font-semibold">Found ${results.length} result(s)</span>
996+
</div>
997+
${results.length === 0 ? `
998+
<div class="border border-terminal-yellow/30 rounded p-3 text-center">
999+
<div class="text-terminal-yellow font-semibold mb-2">No results found</div>
1000+
<div class="text-white opacity-80 text-xs">
1001+
Try searching for technologies, company names, or project keywords
1002+
</div>
1003+
</div>
1004+
` : `
1005+
<div class="space-y-3">
1006+
${results.map((result, index) => `
1007+
<div class="border border-terminal-green/20 rounded p-3 ${index < results.length - 1 ? 'border-b border-terminal-green/30' : ''}">
1008+
<div class="mb-2">
1009+
<span class="text-terminal-bright-green font-semibold text-xs">[${result.category.toUpperCase()}]</span>
1010+
<span class="text-terminal-yellow font-semibold ml-2">${result.title}</span>
1011+
</div>
1012+
<div class="text-white text-xs opacity-80 bg-terminal-green/5 p-2 rounded leading-relaxed">
1013+
${result.content}
1014+
</div>
1015+
</div>
1016+
`).join('')}
1017+
</div>
1018+
`}
1019+
<div class="border-t border-terminal-green/30 pt-3">
1020+
<div class="text-terminal-yellow font-bold mb-2">💡 EXPLORE MORE</div>
1021+
<div class="space-y-1 ml-2 text-xs">
1022+
<div><span class="text-white">•</span> Try <span class="text-terminal-bright-green font-semibold"><a href="?cmd=skills">skills</a></span> to see all technologies</div>
1023+
<div><span class="text-white">•</span> Try <span class="text-terminal-bright-green font-semibold"><a href="?cmd=experience">experience</a></span> to view work history</div>
1024+
<div><span class="text-white">•</span> Try <span class="text-terminal-bright-green font-semibold"><a href="?cmd=projects">projects</a></span> to explore all projects</div>
1025+
<div><span class="text-white">•</span> Try <span class="text-terminal-bright-green font-semibold"><a href="?cmd=help">help</a></span> for all available commands</div>
1026+
</div>
1027+
</div>
1028+
</div>
1029+
</div>
1030+
`.trim();
1031+
1032+
// Add the entire search box as a single line
1033+
addLine(searchBox, 'w-full');
1034+
}, [addLine, portfolioData, formatExperiencePeriod]);
8541035

8551036
const showTheme = useCallback((args: string[]) => {
8561037
const theme = args[0];
@@ -936,7 +1117,11 @@ export function useTerminal({ portfolioData }: UseTerminalProps) {
9361117
<span class="text-terminal-yellow font-semibold">Phone</span>
9371118
</div>
9381119
<div class="col-span-9 bg-terminal-green/5">
939-
<span class="text-white"><a href="${cv.phone}" class="text-terminal-bright-green underline hover:text-terminal-yellow cursor-pointer">${cv.phone.replace(/[^\d\+]/g, '')}</a></span>
1120+
<span class="text-white">
1121+
<a href="${cv.phone}" class="text-terminal-bright-green underline hover:text-terminal-yellow cursor-pointer">
1122+
${cv.phone.replace(/[^\d\+]/g, '')}
1123+
</a>
1124+
</span>
9401125
</div>
9411126
</div>
9421127
</div>

0 commit comments

Comments
 (0)