diff --git a/frontend/app/routes.ts b/frontend/app/routes.ts index c9cacb20..0ac903b4 100644 --- a/frontend/app/routes.ts +++ b/frontend/app/routes.ts @@ -3,6 +3,8 @@ import { index, route } from "@react-router/dev/routes"; export default [ index("routes/_index.tsx"), + route("about", "routes/about.tsx"), + route("help", "routes/help.tsx"), route("search", "routes/search.tsx"), route("search/results", "routes/search.results.ts"), route("resources/:id", "routes/resources.$id.tsx"), diff --git a/frontend/app/routes/about.tsx b/frontend/app/routes/about.tsx new file mode 100644 index 00000000..ed4bae81 --- /dev/null +++ b/frontend/app/routes/about.tsx @@ -0,0 +1,19 @@ +import type { LoaderFunctionArgs, MetaFunction } from 'react-router'; +import { AboutPage } from '../../src/pages/AboutPage'; +import { buildSeoMeta } from '../../src/config/seo'; + +export function loader({ request }: LoaderFunctionArgs) { + return { currentUrl: new URL(request.url).href }; +} + +export const meta: MetaFunction = ({ data }) => + buildSeoMeta({ + title: 'About', + description: + 'Learn about the Big Ten Academic Alliance Geoportal and the collections it helps users discover.', + url: data?.currentUrl, + }); + +export default function About() { + return ; +} diff --git a/frontend/app/routes/help.tsx b/frontend/app/routes/help.tsx new file mode 100644 index 00000000..947da5ef --- /dev/null +++ b/frontend/app/routes/help.tsx @@ -0,0 +1,19 @@ +import type { LoaderFunctionArgs, MetaFunction } from 'react-router'; +import { HelpPage } from '../../src/pages/HelpPage'; +import { buildSeoMeta } from '../../src/config/seo'; + +export function loader({ request }: LoaderFunctionArgs) { + return { currentUrl: new URL(request.url).href }; +} + +export const meta: MetaFunction = ({ data }) => + buildSeoMeta({ + title: 'Help', + description: + 'Learn how to search, filter, view resources, and use bookmarks in the Big Ten Academic Alliance Geoportal.', + url: data?.currentUrl, + }); + +export default function Help() { + return ; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 88c08fb1..21c09e80 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,8 @@ import { ProviderPillsTestPage } from './pages/ProviderPillsTestPage'; import { MapPage } from './pages/MapPage'; import { TestPage } from './pages/TestPage'; import { NotFoundPage } from './pages/NotFoundPage'; +import { AboutPage } from './pages/AboutPage'; +import { HelpPage } from './pages/HelpPage'; // Import Leaflet CSS import 'leaflet/dist/leaflet.css'; @@ -32,6 +34,8 @@ function App() { {/* More specific paths first so /search matches before / */} + } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/content/MarkdownContent.tsx b/frontend/src/components/content/MarkdownContent.tsx new file mode 100644 index 00000000..c93c6ef9 --- /dev/null +++ b/frontend/src/components/content/MarkdownContent.tsx @@ -0,0 +1,175 @@ +import type { ReactNode } from 'react'; +import { Link } from 'react-router'; + +type MarkdownBlock = + | { type: 'heading'; level: 1 | 2 | 3; text: string } + | { type: 'paragraph'; text: string } + | { type: 'list'; items: string[] }; + +const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g; + +function isSafeHref(href: string) { + return ( + href.startsWith('/') || + href.startsWith('https://') || + href.startsWith('http://') || + href.startsWith('mailto:') + ); +} + +function renderInlineMarkdown(text: string, keyPrefix: string) { + const nodes: ReactNode[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + + linkPattern.lastIndex = 0; + while ((match = linkPattern.exec(text)) !== null) { + const [raw, label, href] = match; + const index = match.index; + + if (index > lastIndex) { + nodes.push(text.slice(lastIndex, index)); + } + + if (isSafeHref(href)) { + const className = + 'font-semibold text-brand underline underline-offset-4 hover:text-brand-active'; + + nodes.push( + href.startsWith('/') ? ( + + {label} + + ) : ( + + {label} + + ) + ); + } else { + nodes.push(label); + } + + lastIndex = index + raw.length; + } + + if (lastIndex < text.length) { + nodes.push(text.slice(lastIndex)); + } + + return nodes.length > 0 ? nodes : text; +} + +function parseMarkdown(markdown: string): MarkdownBlock[] { + const blocks: MarkdownBlock[] = []; + const lines = markdown.replace(/\r\n/g, '\n').split('\n'); + let index = 0; + + while (index < lines.length) { + const line = lines[index].trim(); + + if (!line) { + index += 1; + continue; + } + + const heading = /^(#{1,3})\s+(.+)$/.exec(line); + if (heading) { + blocks.push({ + type: 'heading', + level: heading[1].length as 1 | 2 | 3, + text: heading[2], + }); + index += 1; + continue; + } + + if (line.startsWith('- ')) { + const items: string[] = []; + while (index < lines.length && lines[index].trim().startsWith('- ')) { + items.push(lines[index].trim().slice(2)); + index += 1; + } + blocks.push({ type: 'list', items }); + continue; + } + + const paragraphLines: string[] = []; + while ( + index < lines.length && + lines[index].trim() && + !/^(#{1,3})\s+/.test(lines[index].trim()) && + !lines[index].trim().startsWith('- ') + ) { + paragraphLines.push(lines[index].trim()); + index += 1; + } + blocks.push({ type: 'paragraph', text: paragraphLines.join(' ') }); + } + + return blocks; +} + +export function MarkdownContent({ markdown }: { markdown: string }) { + const blocks = parseMarkdown(markdown); + + return ( +
+ {blocks.map((block, index) => { + if (block.type === 'heading') { + if (block.level === 1) { + return ( +

+ {renderInlineMarkdown(block.text, `h-${index}`)} +

+ ); + } + + const HeadingTag = block.level === 2 ? 'h2' : 'h3'; + return ( + + {renderInlineMarkdown(block.text, `h-${index}`)} + + ); + } + + if (block.type === 'list') { + return ( +
    + {block.items.map((item, itemIndex) => ( +
  • +
  • + ))} +
+ ); + } + + return ( +

+ {renderInlineMarkdown(block.text, `p-${index}`)} +

+ ); + })} +
+ ); +} diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index a0b96a07..ade39918 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -7,12 +7,16 @@ import { useTheme } from '../../hooks/useTheme'; const NAV_LINKS = [ { - href: 'https://gin.btaa.org/about/about-us/', + href: '/about', label: 'About', - external: true, + external: false, + }, + { + href: '/help', + label: 'Help', + external: false, }, { href: 'https://geo.btaa.org/feedback', label: 'Feedback', external: true }, - { href: '/bookmarks', label: 'Bookmarks', external: false }, ]; export function Header() { diff --git a/frontend/src/content/pages/about.md b/frontend/src/content/pages/about.md new file mode 100644 index 00000000..ecf9b0b6 --- /dev/null +++ b/frontend/src/content/pages/about.md @@ -0,0 +1,32 @@ +# About the BTAA Geoportal + +The Big Ten Academic Alliance (BTAA) Geoportal helps users find geospatial resources from BTAA member libraries and public data sources. + +The Geoportal brings together maps, geospatial datasets, aerial imagery, scanned historical maps, web services, and related documentation so users can search across institutions without leaving the geoportal. + +Most resources in the Geoportal link to data stored by libraries, government agencies, and other trusted partners. Some resources are also stored and shared directly through the Geoportal as part of a growing BTAA effort to collect and preserve geospatial data. + + +## What You Can Find + +In the Geoportal, you can find: + +- GIS datasets +- Scanned maps +- Historical and public domain maps +- Aerial photos +- Web mapping services +- Interactive maps and websites + + +## How The Portal Works + +The geoportal indexes descriptive metadata from participating institutions and presents it through a shared search interface. When a resource is hosted by a partner institution, the record links users to the original download, viewer, service endpoint, or catalog page. + +## Who Maintains It + +The BTAA Geoportal is maintained by the [Big Ten Academic Alliance Geospatial Information Network](https://gin.btaa.org), a collaborative program focused on improving discovery, access, and preservation for geospatial information. + +## Start Exploring + +[Browse all resources](/search?q=) \ No newline at end of file diff --git a/frontend/src/content/pages/help.md b/frontend/src/content/pages/help.md new file mode 100644 index 00000000..763059d7 --- /dev/null +++ b/frontend/src/content/pages/help.md @@ -0,0 +1,25 @@ +# Help + +Use the BTAA Geoportal to search for maps, geospatial datasets, imagery, and related resources from participating institutions. + +## Search + +Enter keywords in the search box to find resources by title, subject, place, institution, publisher, or other metadata. Leave the search box blank and search to browse all resources. + +## Filter Results + +Use the filters on search results to narrow by resource type, institution, subject, place, format, time period, and other available facets. + +## View A Resource + +Open a result to see the resource description, access links, download options, map previews, citation information, and full metadata. Some resources link out to partner repositories for authoritative access. + +## Bookmarks + +Use bookmarks to keep a temporary list of resources while you browse. Bookmarks are stored in your browser and are not synced across devices. + +## Need More Help? + +[Contact us](https://geo.btaa.org/feedback) + +[Browse all resources](/search?q=) diff --git a/frontend/src/pages/AboutPage.tsx b/frontend/src/pages/AboutPage.tsx new file mode 100644 index 00000000..52544834 --- /dev/null +++ b/frontend/src/pages/AboutPage.tsx @@ -0,0 +1,25 @@ +import { Header } from '../components/layout/Header'; +import { Footer } from '../components/layout/Footer'; +import { MarkdownContent } from '../components/content/MarkdownContent'; +import { Seo } from '../components/Seo'; +import aboutMarkdown from '../content/pages/about.md?raw'; + +export function AboutPage() { + return ( +
+ +
+
+
+
+ +
+
+
+
+
+ ); +} diff --git a/frontend/src/pages/HelpPage.tsx b/frontend/src/pages/HelpPage.tsx new file mode 100644 index 00000000..3dd1c777 --- /dev/null +++ b/frontend/src/pages/HelpPage.tsx @@ -0,0 +1,25 @@ +import { Header } from '../components/layout/Header'; +import { Footer } from '../components/layout/Footer'; +import { MarkdownContent } from '../components/content/MarkdownContent'; +import { Seo } from '../components/Seo'; +import helpMarkdown from '../content/pages/help.md?raw'; + +export function HelpPage() { + return ( +
+ +
+
+
+
+ +
+
+
+
+ ); +}