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
16 changes: 11 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- **Name:** UFOund it
- **What it is:** UFOund it, is a webapp that displays UFO sightings.
- **Dataset:** [NUFORC UFO Sightings (Kaggle)](https://www.kaggle.com/datasets/NUFORC/ufo-sightings), CSV, approx 80,000 reports, approx 15mb.
- **Current state:** The frontend runs on hand-written mock data in `frontend/src/data/`. The backend is an Apollo Server with only a placeholder `health` query and is not yet connected to the frontend. No database yet.
- **Current state:** The frontend runs on hand-written mock data in `frontend/src/data/`. The backend is an Apollo Server with only a placeholder `health` query. The frontend reaches it through Apollo Client, but only for a temporary backend status check in the footer. No database yet.
- **Course context:** NTNU IT2810 project. Must be reproducible, accessible and sustainable.

## Tech stack
Expand All @@ -28,13 +28,18 @@ Run from the repository root unless noted.
# Install dependencies
npm install

# Start frontend dev server
# Start frontend dev server (proxies /project2/graphql to the backend)
npm run dev

# Start backend dev server (GraphQL on http://localhost:3001, restarts on save)
# Use PORT=3002 npm run dev:backend if port 3001 is taken
npm run dev:backend

# If port 3001 is taken, move both to another port. The frontend's
# BACKEND_PORT must match the backend's PORT, or the footer shows
# "Backend: unavailable".
PORT=3002 npm run dev:backend
BACKEND_PORT=3002 npm run dev

# Build (type-check + production build of frontend and backend)
npm run build

Expand All @@ -59,13 +64,13 @@ npm run test
```
frontend/
src/
components/ Reusable UI (Banner, SearchBar, SightingList, SightingMap)
components/ Reusable UI (Banner, SearchBar, SightingList, SightingMap, BackendStatus)
pages/ Route-level components (SearchPage, SightingDetailPage)
hooks/ Custom hooks (debounce, sighting search, single sighting)
utils/ Pure helpers (filtering, formatting)
types/ Shared TypeScript types
data/ Mock sightings (to be replaced by GraphQL)
# graphql/ Planned: Apollo queries
graphql/ Apollo Client setup (client.ts) and GraphQL queries (queries.ts)
backend/ Node + TypeScript GraphQL server (Apollo Server 5)
src/
index.ts Starts the server on PORT (default 3001)
Expand Down Expand Up @@ -127,6 +132,7 @@ Never:

- **VM address:** TODO (`http://it2810-XX.idi.ntnu.no/project2`)
- **Backend port:** 3001
- **GraphQL endpoint:** The frontend calls `/project2/graphql` on its own origin. Apache must proxy it to `http://localhost:3001/graphql`, like the Vite dev proxy does locally. `VITE_GRAPHQL_URL` overrides the URL at build time if needed.
- **How to deploy:** TODO

## AI usage documentation
Expand Down
5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
"test": "vitest"
},
"dependencies": {
"@apollo/client": "^4.3.2",
"graphql": "^16.14.2",
"leaflet": "^1.9.4",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-leaflet": "^5.0.0",
"react-router": "^8.4.0"
"react-router": "^8.4.0",
"rxjs": "^7.8.2"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Route, Routes } from 'react-router'
import BackendStatus from './components/BackendStatus'
import Banner from './components/Banner'
import SearchPage from './pages/SearchPage'
import SightingDetailPage from './pages/SightingDetailPage'
Expand All @@ -16,6 +17,9 @@ function App() {
<Route path="/submit" element={<SubmitSightingPage />} />
</Routes>
</main>
<footer>
<BackendStatus />
</footer>
</>
)
}
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/components/BackendStatus.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.backend-status {
padding: 16px 32px;
border-top: 1px solid var(--border);
color: var(--text);
font-size: 15px;

@media (max-width: 1024px) {
padding: 16px;
}
}
25 changes: 25 additions & 0 deletions frontend/src/components/BackendStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useQuery } from '@apollo/client/react'
import { HEALTH_QUERY } from '../graphql/queries'
import './BackendStatus.css'

/**
* Temporary check that the frontend can reach the GraphQL backend.
* Remove once real queries show whether the backend is up.
*/
function BackendStatus() {
const { data, loading, error } = useQuery(HEALTH_QUERY)

let status = 'checking…'
if (error) status = 'unavailable'
else if (!loading && data?.health === 'ok') status = 'connected'

// role="status" makes screen readers announce the change from
// "checking…" to the result, without moving focus.
return (
<p className="backend-status" role="status">
Backend: {status}
</p>
)
}

export default BackendStatus
12 changes: 12 additions & 0 deletions frontend/src/graphql/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'

// BASE_URL is Vite's `base` ("/project2"), so the default is a same-origin
// path that the Vite dev proxy and Apache on the VM both forward to the
// backend. VITE_GRAPHQL_URL is only for pointing at a different backend.
const baseUrl = import.meta.env.BASE_URL.replace(/\/$/, '')
const graphqlUrl = import.meta.env.VITE_GRAPHQL_URL ?? `${baseUrl}/graphql`

export const apolloClient = new ApolloClient({
link: new HttpLink({ uri: graphqlUrl }),
cache: new InMemoryCache(),
})
15 changes: 15 additions & 0 deletions frontend/src/graphql/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { gql, type TypedDocumentNode } from '@apollo/client'

interface HealthQueryData {
health: string
}

/**
* Asks the backend's placeholder health field, to check that the frontend
* can reach it. The type parameters let useQuery know the shape of `data`.
*/
export const HEALTH_QUERY: TypedDocumentNode<HealthQueryData> = gql`
query Health {
health
}
`
10 changes: 7 additions & 3 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { ApolloProvider } from '@apollo/client/react'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { HashRouter } from 'react-router'
import './index.css'
import App from './App.tsx'
import { apolloClient } from './graphql/client.ts'

// Hash URLs (/project2/#/sightings/1) need no server rewrite rules, so
// refreshing or sharing a deep link works on the course VM as-is.
createRoot(document.getElementById('root')!).render(
<StrictMode>
<HashRouter>
<App />
</HashRouter>
<ApolloProvider client={apolloClient}>
<HashRouter>
<App />
</HashRouter>
</ApolloProvider>
</StrictMode>,
)
52 changes: 52 additions & 0 deletions frontend/src/tests/BackendStatus.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
MockedProvider,
type MockedProviderProps,
} from '@apollo/client/testing/react'
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import BackendStatus from '../components/BackendStatus'
import { HEALTH_QUERY } from '../graphql/queries'

// MockedProvider stands in for ApolloProvider and answers queries from the
// mocks below, so these tests never need a running backend.
function renderWithMocks(mocks: MockedProviderProps['mocks']) {
return render(
<MockedProvider mocks={mocks}>
<BackendStatus />
</MockedProvider>,
)
}

describe('BackendStatus', () => {
it('shows that it is checking while the query is in flight', () => {
renderWithMocks([
{ request: { query: HEALTH_QUERY }, result: { data: { health: 'ok' } } },
])

expect(screen.getByText('Backend: checking…')).toBeInTheDocument()
})

it('is a status region so screen readers announce changes', () => {
renderWithMocks([
{ request: { query: HEALTH_QUERY }, result: { data: { health: 'ok' } } },
])

expect(screen.getByRole('status')).toHaveTextContent('Backend: checking…')
})

it('shows connected when the backend answers ok', async () => {
renderWithMocks([
{ request: { query: HEALTH_QUERY }, result: { data: { health: 'ok' } } },
])

expect(await screen.findByText('Backend: connected')).toBeInTheDocument()
})

it('shows unavailable when the request fails', async () => {
renderWithMocks([
{ request: { query: HEALTH_QUERY }, error: new Error('Network error') },
])

expect(await screen.findByText('Backend: unavailable')).toBeInTheDocument()
})
})
8 changes: 8 additions & 0 deletions frontend/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Types for our own VITE_ environment variables. Vite inlines these into
* the bundle at build time, so they must never contain secrets.
*/
interface ImportMetaEnv {
/** Overrides the GraphQL endpoint. Defaults to /project2/graphql. */
readonly VITE_GRAPHQL_URL?: string
}
15 changes: 15 additions & 0 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vitest/config'

// Matches the backend's PORT, so `BACKEND_PORT=3002 npm run dev` works
// when the backend runs with `PORT=3002`.
const backendPort = process.env.BACKEND_PORT ?? '3001'

// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
base: '/project2',
server: {
// The frontend always calls /project2/graphql on its own origin. In
// development Vite forwards it to the backend, on the VM Apache does,
// so the browser never makes cross-origin (CORS) requests.
proxy: {
'/project2/graphql': {
target: `http://localhost:${backendPort}`,
rewrite: (path) => path.replace(/^\/project2/, ''),
},
},
},
test: {
globals: true,
environment: 'jsdom',
Expand Down
Loading