Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
19 changes: 19 additions & 0 deletions frontend/app/routes/about.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof loader> = ({ 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 <AboutPage />;
}
19 changes: 19 additions & 0 deletions frontend/app/routes/help.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof loader> = ({ 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 <HelpPage />;
}
4 changes: 4 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,6 +34,8 @@ function App() {
<DebugProvider>
<Routes>
{/* More specific paths first so /search matches before / */}
<Route path="/about" element={<AboutPage />} />
<Route path="/help" element={<HelpPage />} />
<Route path="/search" element={<SearchPage />} />
<Route path="/bookmarks" element={<BookmarksPage />} />
<Route path="/resources/:id" element={<ResourceView />} />
Expand Down
175 changes: 175 additions & 0 deletions frontend/src/components/content/MarkdownContent.tsx
Original file line number Diff line number Diff line change
@@ -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('/') ? (
<Link key={`${keyPrefix}-${index}`} to={href} className={className}>
{label}
</Link>
) : (
<a
key={`${keyPrefix}-${index}`}
href={href}
className={className}
target={href.startsWith('http') ? '_blank' : undefined}
rel={href.startsWith('http') ? 'noopener noreferrer' : undefined}
>
{label}
</a>
)
);
} 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 (
<div className="space-y-7">
{blocks.map((block, index) => {
if (block.type === 'heading') {
if (block.level === 1) {
return (
<h1
key={index}
className="text-4xl sm:text-5xl font-bold text-gray-950"
>
{renderInlineMarkdown(block.text, `h-${index}`)}
</h1>
);
}

const HeadingTag = block.level === 2 ? 'h2' : 'h3';
return (
<HeadingTag
key={index}
className="text-2xl font-semibold text-gray-950"
>
{renderInlineMarkdown(block.text, `h-${index}`)}
</HeadingTag>
);
}

if (block.type === 'list') {
return (
<ul
key={index}
className="grid gap-3 text-lg leading-8 text-gray-700 sm:grid-cols-2"
>
{block.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex gap-3">
<span
className="mt-3 h-2 w-2 rounded-full bg-brand shrink-0"
aria-hidden="true"
/>
<span>{renderInlineMarkdown(item, `li-${index}`)}</span>
</li>
))}
</ul>
);
}

return (
<p key={index} className="text-lg leading-8 text-gray-700">
{renderInlineMarkdown(block.text, `p-${index}`)}
</p>
);
})}
</div>
);
}
10 changes: 7 additions & 3 deletions frontend/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/content/pages/about.md
Original file line number Diff line number Diff line change
@@ -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=)
25 changes: 25 additions & 0 deletions frontend/src/content/pages/help.md
Original file line number Diff line number Diff line change
@@ -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=)
25 changes: 25 additions & 0 deletions frontend/src/pages/AboutPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen flex flex-col bg-gray-50">
<Seo
title="About"
description="Learn about the Big Ten Academic Alliance Geoportal and the collections it helps users discover."
/>
<Header />
<main className="flex-1">
<section className="bg-white border-b border-gray-200">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
<MarkdownContent markdown={aboutMarkdown} />
</div>
</section>
</main>
<Footer />
</div>
);
}
25 changes: 25 additions & 0 deletions frontend/src/pages/HelpPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen flex flex-col bg-gray-50">
<Seo
title="Help"
description="Learn how to search, filter, view resources, and use bookmarks in the Big Ten Academic Alliance Geoportal."
/>
<Header />
<main className="flex-1">
<section className="bg-white border-b border-gray-200">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
<MarkdownContent markdown={helpMarkdown} />
</div>
</section>
</main>
<Footer />
</div>
);
}
Loading