diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4efaa2f..6b6c774 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,7 +19,7 @@ "@types/react-dom": "^19.0.4", "@typescript-eslint/eslint-plugin": "^8.29.1", "@typescript-eslint/parser": "^8.29.1", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^4.4.1", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.6", @@ -30,7 +30,7 @@ "prettier": "^3.5.3", "typescript": "~5.7.2", "typescript-eslint": "^8.24.1", - "vite": "^6.2.0" + "vite": "^6.3.5" } }, "node_modules/@ampproject/remapping": { @@ -1413,12 +1413,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -1673,17 +1667,16 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", - "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/core": "^7.26.0", + "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "react-refresh": "^0.17.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2145,7 +2138,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", "engines": { "node": ">=18" } @@ -4389,22 +4381,19 @@ "license": "MIT" }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-router": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz", - "integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==", - "license": "MIT", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz", + "integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==", "dependencies": { - "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" @@ -4423,12 +4412,11 @@ } }, "node_modules/react-router-dom": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.0.tgz", - "integrity": "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==", - "license": "MIT", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.3.tgz", + "integrity": "sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A==", "dependencies": { - "react-router": "7.5.0" + "react-router": "7.5.3" }, "engines": { "node": ">=20.0.0" @@ -4659,8 +4647,7 @@ "node_modules/set-cookie-parser": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -4974,6 +4961,48 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5010,8 +5039,7 @@ "node_modules/turbo-stream": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", - "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", - "license": "ISC" + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" }, "node_modules/type-check": { "version": "0.4.0", @@ -5109,7 +5137,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5202,15 +5229,17 @@ } }, "node_modules/vite": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", - "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -5273,6 +5302,32 @@ } } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 757dfc6..3316f97 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,7 +21,7 @@ "@types/react-dom": "^19.0.4", "@typescript-eslint/eslint-plugin": "^8.29.1", "@typescript-eslint/parser": "^8.29.1", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^4.4.1", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.6", @@ -32,6 +32,6 @@ "prettier": "^3.5.3", "typescript": "~5.7.2", "typescript-eslint": "^8.24.1", - "vite": "^6.2.0" + "vite": "^6.3.5" } } diff --git a/frontend/src/components/InstitutionType.tsx b/frontend/src/components/InstitutionType.tsx new file mode 100644 index 0000000..3a31a0f --- /dev/null +++ b/frontend/src/components/InstitutionType.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { + getInstitutionTypeDisplayName, + getInstitutionTypeIcon, + INSTITUTION_TYPES +} from '../constants/institutionTypes'; + +interface InstitutionTypeProps { + type: string | null | undefined; + showIcon?: boolean; + className?: string; + showTooltip?: boolean; +} + +/** + * A component for displaying institution types with consistent formatting and styling + * based on the OpenAlex institution type vocabulary. + * + * @param props.type - The institution type (from OpenAlex) + * @param props.showIcon - Whether to show an icon (default: true) + * @param props.className - Additional CSS classes to apply + * @param props.showTooltip - Whether to show a tooltip with the description (default: true) + */ +const InstitutionType: React.FC = ({ + type, + showIcon = true, + className = '', + showTooltip = true +}) => { + if (!type) return N/A; + + // Get display name and icon from constants + const displayName = getInstitutionTypeDisplayName(type); + const icon = getInstitutionTypeIcon(type); + + // Apply specific styling based on institution type + const typeKey = type.toLowerCase(); + const baseClassName = `institution-type institution-type-${typeKey in INSTITUTION_TYPES ? typeKey : 'unknown'}`; + + // Get description for tooltip + const description = typeKey in INSTITUTION_TYPES ? + INSTITUTION_TYPES[typeKey].description : + 'Unknown institution type'; + + return ( + + {showIcon && {icon}} + {displayName} + + ); +}; + +export default InstitutionType; \ No newline at end of file diff --git a/frontend/src/components/InstitutionTypeFilter.css b/frontend/src/components/InstitutionTypeFilter.css new file mode 100644 index 0000000..6794b14 --- /dev/null +++ b/frontend/src/components/InstitutionTypeFilter.css @@ -0,0 +1,41 @@ +.institution-type-filter { + margin: 16px 0; +} + +.institution-type-filter h4 { + margin-bottom: 8px; +} + +.institution-type-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 16px; +} + +.institution-type-filter-item { + cursor: pointer; + padding: 6px 12px; + border-radius: 4px; + background-color: #f5f5f5; + transition: all 0.2s ease; + display: flex; + align-items: center; + user-select: none; +} + +.institution-type-filter-item:hover { + background-color: #e0e0e0; +} + +.institution-type-filter-item.selected { + background-color: #e3f2fd; + border: 1px solid #2196f3; +} + +/* Override the default institution type styling within filter items */ +.institution-type-filter-item .institution-type { + background-color: transparent !important; + padding: 0 !important; + margin: 0 !important; +} \ No newline at end of file diff --git a/frontend/src/components/InstitutionTypeFilter.tsx b/frontend/src/components/InstitutionTypeFilter.tsx new file mode 100644 index 0000000..f7ebdf9 --- /dev/null +++ b/frontend/src/components/InstitutionTypeFilter.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { INSTITUTION_TYPES_ARRAY } from '../constants/institutionTypes'; +import InstitutionType from './InstitutionType'; +import './InstitutionTypeFilter.css'; + +interface InstitutionTypeFilterProps { + onTypeSelect: (type: string | null) => void; + selectedType: string | null; + showAllOption?: boolean; + className?: string; +} + +/** + * A component for filtering institutions by their type + * + * @param props.onTypeSelect - Callback function when a type is selected + * @param props.selectedType - Currently selected type + * @param props.showAllOption - Whether to show an "All Types" option (default: true) + * @param props.className - Additional CSS classes to apply + */ +const InstitutionTypeFilter: React.FC = ({ + onTypeSelect, + selectedType, + showAllOption = true, + className = '' +}) => { + return ( +
+

Filter by Institution Type

+
+ {showAllOption && ( +
onTypeSelect(null)} + > + 🔍 All Types +
+ )} + + {INSTITUTION_TYPES_ARRAY.map(type => ( +
onTypeSelect(type.value)} + title={type.description} + > + +
+ ))} +
+
+ ); +}; + +export default InstitutionTypeFilter; \ No newline at end of file diff --git a/frontend/src/constants/institutionTypes.ts b/frontend/src/constants/institutionTypes.ts new file mode 100644 index 0000000..55f2ab8 --- /dev/null +++ b/frontend/src/constants/institutionTypes.ts @@ -0,0 +1,95 @@ +/** + * Standard institution types from OpenAlex + * These are based on the ROR "type" controlled vocabulary + * @see https://docs.openalex.org/api-entities/institutions/institution-object#type + */ + +export interface InstitutionTypeDefinition { + value: string; + displayName: string; + description: string; + icon: string; +} + +export const INSTITUTION_TYPES: Record = { + education: { + value: 'education', + displayName: 'Education', + description: 'Universities, colleges, and other educational institutions', + icon: '🎓' + }, + healthcare: { + value: 'healthcare', + displayName: 'Healthcare', + description: 'Hospitals, medical centers, and other healthcare facilities', + icon: '🏥' + }, + company: { + value: 'company', + displayName: 'Company', + description: 'Commercial entities and businesses', + icon: '🏢' + }, + archive: { + value: 'archive', + displayName: 'Archive', + description: 'Libraries, museums, and other archives', + icon: '📚' + }, + nonprofit: { + value: 'nonprofit', + displayName: 'Nonprofit', + description: 'Non-profit organizations and NGOs', + icon: '🤝' + }, + government: { + value: 'government', + displayName: 'Government', + description: 'Government agencies and public bodies', + icon: '🏛️' + }, + facility: { + value: 'facility', + displayName: 'Facility', + description: 'Research facilities, laboratories, and specialized centers', + icon: '🔬' + }, + other: { + value: 'other', + displayName: 'Other', + description: 'Organizations that don\'t fit in other categories', + icon: '📋' + } +}; + +/** + * Array of institution types for dropdown menus, etc. + */ +export const INSTITUTION_TYPES_ARRAY = Object.values(INSTITUTION_TYPES); + +/** + * Get a formatted display name for an institution type + * @param type The raw institution type string + * @returns Properly formatted display name or the original string if not found + */ +export const getInstitutionTypeDisplayName = (type: string | null | undefined): string => { + if (!type) return 'Unknown'; + + const typeKey = type.toLowerCase(); + return INSTITUTION_TYPES[typeKey]?.displayName || + type.charAt(0).toUpperCase() + type.slice(1).toLowerCase(); +}; + +/** + * Get the icon for an institution type + * @param type The raw institution type string + * @returns The icon for the institution type, or a question mark for unknown types + */ +export const getInstitutionTypeIcon = (type: string | null | undefined): string => { + if (!type) return '❓'; + + const typeKey = type.toLowerCase(); + return INSTITUTION_TYPES[typeKey]?.icon || '❓'; +}; + +export default INSTITUTION_TYPES; \ No newline at end of file diff --git a/frontend/src/pages/DetailPage.css b/frontend/src/pages/DetailPage.css index 24dcbc4..8bca179 100644 --- a/frontend/src/pages/DetailPage.css +++ b/frontend/src/pages/DetailPage.css @@ -81,4 +81,58 @@ .results-category p { color: #666; font-style: italic; +} + +/* Institution type styling */ +.institution-type { + display: inline-flex; + align-items: center; + padding: 4px 8px; + border-radius: 4px; + font-weight: 500; + margin-left: 6px; +} + +.institution-type-icon { + margin-right: 5px; +} + +.institution-type-education { + background-color: #e3f2fd; + color: #0d47a1; +} + +.institution-type-healthcare { + background-color: #e8f5e9; + color: #1b5e20; +} + +.institution-type-company { + background-color: #fce4ec; + color: #880e4f; +} + +.institution-type-archive { + background-color: #fff3e0; + color: #e65100; +} + +.institution-type-nonprofit { + background-color: #f3e5f5; + color: #4a148c; +} + +.institution-type-government { + background-color: #e8eaf6; + color: #1a237e; +} + +.institution-type-facility { + background-color: #e0f7fa; + color: #006064; +} + +.institution-type-other, .institution-type-unknown { + background-color: #f5f5f5; + color: #616161; } \ No newline at end of file diff --git a/frontend/src/pages/InstitutionDetailPage.tsx b/frontend/src/pages/InstitutionDetailPage.tsx index a335cfb..dbc4d15 100644 --- a/frontend/src/pages/InstitutionDetailPage.tsx +++ b/frontend/src/pages/InstitutionDetailPage.tsx @@ -9,6 +9,7 @@ import { import LoadingSpinner, { LoadingSpinnerProps } from '../components/LoadingSpinner'; // Import props type for explicit prop passing import ErrorMessage, { ErrorMessageProps } from '../components/ErrorMessage'; // Import props type for explicit prop passing import SimpleTable, { SimpleTableProps } from '../components/SimpleTable'; // Import props type for explicit prop passing +import InstitutionType from '../components/InstitutionType'; // Import the new InstitutionType component import type { InstitutionResponse, RepositorySummary, @@ -26,8 +27,8 @@ interface LinkedReposState { error: string | null; } -/** Defines the structure for storing the state related to calculated affiliations, - * including the affiliation data array, loading status, and any potential error message. */ +/** Defines the structure for storing the state related to institution affiliations, + * including the affiliations data array, loading status, and any potential error message. */ interface AffiliationsState { affiliations: AffiliationResultResponse[]; loading: boolean; @@ -280,7 +281,7 @@ const InstitutionDetailPage: React.FC = () => {

OpenAlex ID: {institution.openalex_id}

{/* Link to external ROR page if ROR ID exists */}

ROR: {institution.ror ? {institution.ror} : 'N/A'}

-

Type: {institution.type || 'N/A'}

+

Type:

Country Code: {institution.country_code || 'N/A'}

{/* Display potential GitHub org logins if available */} {institution.github_organization_logins && institution.github_organization_logins.length > 0 && ( diff --git a/frontend/src/pages/SearchPage.tsx b/frontend/src/pages/SearchPage.tsx index 7b6c842..5456449 100644 --- a/frontend/src/pages/SearchPage.tsx +++ b/frontend/src/pages/SearchPage.tsx @@ -16,6 +16,9 @@ import ErrorMessage from '../components/ErrorMessage'; // Import page-specific styles import './SearchPage.css'; +// Import the InstitutionType component +import InstitutionType from '../components/InstitutionType'; + /** * Interface defining the structure for storing search results across different entity types. * Each property holds an array of summary objects for that entity type. @@ -261,6 +264,7 @@ function SearchPage() { {inst.display_name} {/* Optionally display ROR ID */} {inst.ror && ` (ROR: ${inst.ror})`} + {inst.type && } ))}