Skip to content
Merged
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
8 changes: 5 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ KITHE_BRIDGE_VERIFY_SSL=true
# =============================================================================

APPLICATION_URL=http://localhost:8000/api/v1/
# Host port published by the Docker Compose frontend service.
FRONTEND_PORT=5173
# Base URL for the Geoportal (resource pages). Used for citation URLs.
# When unset, derived from APPLICATION_URL by stripping /api/v1.
# GEOPORTAL_BASE_URL=http://localhost:3000
# GEOPORTAL_BASE_URL=http://localhost:5173
LOG_PATH=logs
# Comma-separated CORS origins (use * for all). Local dev: 3000 (React Router) or 5173 (Vite)
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
# Comma-separated CORS origins (use * for all). Local dev: 5173 (Docker/Vite) or 3000 (direct npm)
CORS_ORIGINS=http://localhost:5173,http://localhost:3000
# Enable crawler indexing and advertise the sitemap in robots.txt.
# Keep this false for local/dev/staging environments.
SEARCH_ENGINE_INDEXING_ENABLED=false
Expand Down
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ workflows, and future agent-facing discovery tools.
## Local Proxy

The local browser-facing proxy is provided by the React Router frontend dev
server at `http://localhost:3000`. It is not a separate service: start the
server at `http://localhost:5173`. It is not a separate service: start the
normal Docker stack and open the frontend URL.

```bash
Expand All @@ -74,18 +74,18 @@ but the frontend uses same-origin proxy routes for request paths that should run
through server-side loaders, carry the server-side API key, or avoid browser
CORS/rate-limit surprises. Common local proxy routes include:

- `http://localhost:3000/search/results` -> `/api/v1/search`
- `http://localhost:3000/search/facets/:facetName` ->
- `http://localhost:5173/search/results` -> `/api/v1/search`
- `http://localhost:5173/search/facets/:facetName` ->
`/api/v1/search/facets/:facetName`
- `http://localhost:3000/map/h3` -> `/api/v1/map/h3`
- `http://localhost:3000/home/blog-posts` -> `/api/v1/home/blog-posts`
- `http://localhost:3000/places/suggest` -> `/api/v1/places/suggest`
- `http://localhost:3000/resources/:id/thumbnail` ->
- `http://localhost:5173/map/h3` -> `/api/v1/map/h3`
- `http://localhost:5173/home/blog-posts` -> `/api/v1/home/blog-posts`
- `http://localhost:5173/places/suggest` -> `/api/v1/places/suggest`
- `http://localhost:5173/resources/:id/thumbnail` ->
`/api/v1/resources/:id/thumbnail`
- `http://localhost:3000/resources/:id/static-map` ->
- `http://localhost:5173/resources/:id/static-map` ->
`/api/v1/resources/:id/static-map`
- `http://localhost:3000/static-maps/...` and
`http://localhost:3000/thumbnails/...` for generated map and thumbnail assets
- `http://localhost:5173/static-maps/...` and
`http://localhost:5173/thumbnails/...` for generated map and thumbnail assets

There are two API base URLs to keep straight in local development:

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ services:
- ./frontend/eslint.config.js:/app/eslint.config.js
- ./frontend/vitest.config.ts:/app/vitest.config.ts
ports:
- "3000:3000"
- "${FRONTEND_PORT:-5173}:3000"
depends_on:
api:
condition: service_started
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ docker compose down

Local URLs:

- Frontend: `http://localhost:3000`
- Frontend: `http://localhost:5173`
- API docs: `http://localhost:8000/api/docs`
- OpenAPI JSON: `http://localhost:8000/api/openapi.json`
- Flower: `http://localhost:5555`
Expand Down
4 changes: 2 additions & 2 deletions docs/backend/turnstile.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,15 @@ traffic can still use the server-side API key without receiving
`turnstile_required`.

The frontend stores the returned session token in `sessionStorage` so local
cross-origin dev traffic from `localhost:3000` to `localhost:8000` can carry
cross-origin Docker traffic from `localhost:5173` to `localhost:8000` can carry
`X-Turnstile-Session` without relying on cross-origin cookies. Production
same-origin traffic also receives an HttpOnly cookie.

To review the branded gate shell without Cloudflare keys or backend Turnstile
verification, run the Vite dev server and open:

```text
http://localhost:3000/turnstile-preview
http://localhost:5173/turnstile-preview
```

This route only works in Vite dev mode. It renders the same BTAA Geoportal gate
Expand Down
4 changes: 2 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ with one command.

4. Open the local services:

- Website: `http://localhost:3000`
- Website: `http://localhost:5173`
- API docs: `http://localhost:8000/api/docs`
- Flower: `http://localhost:5555`

Expand All @@ -43,7 +43,7 @@ The local stack is defined in `docker-compose.yml`.
| Service | Container name | Purpose |
| --- | --- | --- |
| `api` | `btaa-geospatial-api-app` | FastAPI app on port 8000 |
| `frontend` | `btaa-geospatial-api-frontend` | Vite/React dev server on port 3000 |
| `frontend` | `btaa-geospatial-api-frontend` | Vite/React dev server published on port 5173 |
| `elasticsearch` | `btaa-geospatial-api-elasticsearch` | Elasticsearch on port 9200 |
| `paradedb` | `btaa-geospatial-api-paradedb` | PostgreSQL/ParadeDB |
| `redis` | `btaa-geospatial-api-redis` | Redis cache and Celery broker |
Expand Down
4 changes: 2 additions & 2 deletions docs/frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Frontend Documentation

The frontend is the React/TypeScript public Geoportal interface for this
repository. It runs locally as the `frontend` Docker Compose service on port
3000, or directly with npm from `frontend/`.
repository. It runs locally as the `frontend` Docker Compose service on host
port 5173, or directly with npm on port 3000 from `frontend/`.

## Available Documentation

Expand Down
155 changes: 155 additions & 0 deletions frontend/src/__tests__/geoblacklight/layer_index_map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
availabilityStyle,
normalizeIndexMapProperties,
updateInformation,
} from '../../geoblacklight/layer_index_map';

describe('OpenIndexMap v1 information', () => {
beforeEach(() => {
document.body.innerHTML = '<div class="viewer-information"></div>';
});

afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});

it('normalizes v1 link, thumbnail, and identifier property names', () => {
expect(
normalizeIndexMapProperties({
thumbUrl: 'https://example.com/thumb.jpg',
download: 'https://example.com/map.tif',
recId: 'map-27',
})
).toMatchObject({
thumbnailUrl: 'https://example.com/thumb.jpg',
downloadUrl: 'https://example.com/map.tif',
recordIdentifier: 'map-27',
});
});

it('renders v1 thumbnails and links for a selected feature', async () => {
await updateInformation({
title: 'Jaguaribe',
label: 'SB 24',
websiteUrl: 'https://example.com/item',
download: 'https://example.com/map.tif',
digHold: 'https://example.com/digital-holdings',
physHold: 'Held in the map library',
iiifUrl: 'https://example.com/manifest.json',
thumbUrl: 'https://example.com/thumb.jpg',
recId: 'am002175',
});

const information = document.querySelector('.viewer-information');
const image = information?.querySelector('img');
const links = Array.from(information?.querySelectorAll('a') || []);

expect(image).toHaveAttribute('src', 'https://example.com/thumb.jpg');
expect(image).toHaveAttribute('alt', 'Thumbnail for Jaguaribe');
expect(links.map((link) => link.getAttribute('href'))).toEqual(
expect.arrayContaining([
'https://example.com/item',
'https://example.com/map.tif',
'https://example.com/digital-holdings',
'https://example.com/manifest.json',
])
);
expect(information).toHaveTextContent('Record identifier');
expect(information).toHaveTextContent('am002175');
expect(information).toHaveTextContent('Held in the map library');
});

it('renders digHold when it is the feature’s only item link', async () => {
await updateInformation({
label: '19,20',
digHold: 'https://collections.example.edu/digital/item/25010',
recId: 'am002722',
});

const information = document.querySelector('.viewer-information');
const link = information?.querySelector('a');

expect(information).toHaveTextContent('Digital holdings');
expect(link).toHaveAttribute(
'href',
'https://collections.example.edu/digital/item/25010'
);
});

it('continues to support legacy OpenIndexMap property names', () => {
expect(
normalizeIndexMapProperties({
thumbnailUrl: 'https://example.com/legacy-thumb.jpg',
downloadUrl: 'https://example.com/legacy-map.tif',
recordIdentifier: 'legacy-1',
})
).toMatchObject({
thumbnailUrl: 'https://example.com/legacy-thumb.jpg',
downloadUrl: 'https://example.com/legacy-map.tif',
recordIdentifier: 'legacy-1',
});
});

it('uses a IIIF manifest thumbnail when thumbUrl is absent', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(
JSON.stringify({
thumbnail: [{ id: 'https://example.com/iiif-thumb.jpg' }],
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
);

await updateInformation({
label: '27',
iiifUrl: 'https://example.com/manifest.json',
});

expect(document.querySelector('.viewer-information img')).toHaveAttribute(
'src',
'https://example.com/iiif-thumb.jpg'
);
});

it('escapes remote text and does not link unsafe URL schemes', async () => {
await updateInformation({
title: '<img src=x onerror=alert(1)>',
websiteUrl: 'javascript:alert(1)',
});

const information = document.querySelector('.viewer-information');

expect(information?.querySelector('img')).not.toBeInTheDocument();
expect(information?.querySelector('a')).not.toBeInTheDocument();
expect(information).toHaveTextContent('<img src=x onerror=alert(1)>');
});

it('applies availability and opacity without mutating configured styles', () => {
const defaultStyle = { color: 'green' };
const unavailableStyle = { color: 'yellow' };
const options = {
opacity: 0.5,
LAYERS: {
INDEX: {
DEFAULT: defaultStyle,
UNAVAILABLE: unavailableStyle,
},
},
};

expect(availabilityStyle(true, options)).toEqual({
color: 'green',
fillOpacity: 0.5,
opacity: 0.5,
});
expect(availabilityStyle(false, options)).toEqual({
color: 'yellow',
fillOpacity: 0.5,
opacity: 0.5,
});
expect(defaultStyle).toEqual({ color: 'green' });
expect(unavailableStyle).toEqual({ color: 'yellow' });
});
});
2 changes: 1 addition & 1 deletion frontend/src/components/resource/ResourceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface ItemData {

// New component for index map
function IndexMap() {
return <div className="viewer-information"></div>;
return <div className="viewer-information" aria-live="polite"></div>;
}

// New component for the attribute table
Expand Down
Loading
Loading