From f386c00dd970419b518e20b13d9483dd85cfcac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20M=C3=BCller-Grud?= Date: Mon, 19 Oct 2026 12:35:03 +0200 Subject: [PATCH 1/3] Refactor: Share sighting fileds in on fragment The sightings query, the sighting query and the create mutations now use a SightingFileds fragment, so the fields can't drift apart between them. Refs #66 --- frontend/src/graphql/mutations.ts | 14 ++-------- frontend/src/graphql/queries.ts | 46 ++++++++++++++++--------------- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/frontend/src/graphql/mutations.ts b/frontend/src/graphql/mutations.ts index ee4676f..5436662 100644 --- a/frontend/src/graphql/mutations.ts +++ b/frontend/src/graphql/mutations.ts @@ -1,4 +1,5 @@ import { gql, type TypedDocumentNode } from '@apollo/client' +import { SIGHTING_FIELDS } from './queries' /** Matches `CreateSightingInput` in backend/src/schema.ts. */ export interface CreateSightingInput { @@ -52,17 +53,8 @@ export const CREATE_SIGHTING_MUTATION: TypedDocumentNode< > = gql` mutation CreateSighting($input: CreateSightingInput!) { createSighting(input: $input) { - id - datetime - city - state - country - shape - durationSeconds - comments - datePosted - latitude - longitude + ...SightingFields } } + ${SIGHTING_FIELDS} ` diff --git a/frontend/src/graphql/queries.ts b/frontend/src/graphql/queries.ts index 3d39a4a..ee89407 100644 --- a/frontend/src/graphql/queries.ts +++ b/frontend/src/graphql/queries.ts @@ -15,6 +15,26 @@ export const HEALTH_QUERY: TypedDocumentNode = gql` } ` +/** + * The fields of a `UfoSighting`. Every query and mutation that returns a + * sighting spreads this, so they all fetch, and cache, the same fields. + */ +export const SIGHTING_FIELDS = gql` + fragment SightingFields on Sighting { + id + datetime + city + state + country + shape + durationSeconds + comments + datePosted + latitude + longitude + } +` + export interface SightingsQueryData { sightings: { edges: { cursor: string; node: UfoSighting }[] @@ -50,17 +70,7 @@ export const SIGHTINGS_QUERY: TypedDocumentNode< edges { cursor node { - id - datetime - city - state - country - shape - durationSeconds - comments - datePosted - latitude - longitude + ...SightingFields } } pageInfo { @@ -70,6 +80,7 @@ export const SIGHTINGS_QUERY: TypedDocumentNode< totalCount } } + ${SIGHTING_FIELDS} ` interface SightingQueryData { @@ -84,17 +95,8 @@ export const SIGHTING_QUERY: TypedDocumentNode< > = gql` query Sighting($id: ID!) { sighting(id: $id) { - id - datetime - city - state - country - shape - durationSeconds - comments - datePosted - latitude - longitude + ...SightingFields } } + ${SIGHTING_FIELDS} ` From a4ab64fd3695c9cf0e3aa979ac629790567db6c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20M=C3=BCller-Grud?= Date: Mon, 19 Oct 2026 12:37:40 +0200 Subject: [PATCH 2/3] Refactor: Remove duplicate CreatedSighting type Refs #66 --- frontend/src/graphql/mutations.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/frontend/src/graphql/mutations.ts b/frontend/src/graphql/mutations.ts index 5436662..ff2b108 100644 --- a/frontend/src/graphql/mutations.ts +++ b/frontend/src/graphql/mutations.ts @@ -1,4 +1,5 @@ import { gql, type TypedDocumentNode } from '@apollo/client' +import type { UfoSighting } from '../types/ufo-sighting' import { SIGHTING_FIELDS } from './queries' /** Matches `CreateSightingInput` in backend/src/schema.ts. */ @@ -15,22 +16,8 @@ export interface CreateSightingInput { longitude: number } -export interface CreatedSighting { - id: string - datetime: string | null - city: string | null - state: string | null - country: string | null - shape: string | null - durationSeconds: number | null - comments: string | null - datePosted: string | null - latitude: number | null - longitude: number | null -} - interface CreateSightingData { - createSighting: CreatedSighting + createSighting: UfoSighting } interface CreateSightingVariables { From 6ad1f0b80b2293b48f2dcbd1971a20207165bbae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20M=C3=BCller-Grud?= Date: Mon, 19 Oct 2026 12:42:09 +0200 Subject: [PATCH 3/3] Refactor: Move retry code into one helper useSighting and useSightingSearch had the same retry callback. Both now use createREtry, so it is defined in one place. Refs #66 --- frontend/src/hooks/use-sighting-search.ts | 9 ++------- frontend/src/hooks/use-sighting.ts | 12 ++++++------ frontend/src/utils/create-retry.ts | 10 ++++++++++ 3 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 frontend/src/utils/create-retry.ts diff --git a/frontend/src/hooks/use-sighting-search.ts b/frontend/src/hooks/use-sighting-search.ts index e55a62d..19a308d 100644 --- a/frontend/src/hooks/use-sighting-search.ts +++ b/frontend/src/hooks/use-sighting-search.ts @@ -8,6 +8,7 @@ import { sightingSortEnum, type SightingSortOption, } from '../types/ufo-sighting' +import { createRetry } from '../utils/create-retry' import { useDebouncedValue } from './use-debounced-value' const SEARCH_DEBOUNCE_MS = 300 @@ -73,12 +74,6 @@ export function useSightingSearch() { [shownData], ) - // A failed refetch is shown through `error`, so the rejected promise - // needs no handling of its own. - const retry = () => { - refetch().catch(() => {}) - } - // `loading` is also true while "load more" runs. Telling the two apart // keeps the list from dimming when only a page is being added. const loadingMore = networkStatus === NetworkStatus.fetchMore @@ -107,7 +102,7 @@ export function useSightingSearch() { totalCount: shownData?.sightings.totalCount ?? 0, loading: loading && !loadingMore, error, - retry, + retry: createRetry(refetch), hasNextPage: pageInfo?.hasNextPage ?? false, loadMore, loadingMore, diff --git a/frontend/src/hooks/use-sighting.ts b/frontend/src/hooks/use-sighting.ts index 6f83ab8..2000cee 100644 --- a/frontend/src/hooks/use-sighting.ts +++ b/frontend/src/hooks/use-sighting.ts @@ -1,5 +1,6 @@ import { useQuery } from '@apollo/client/react' import { SIGHTING_QUERY } from '../graphql/queries' +import { createRetry } from '../utils/create-retry' /** * Fetches one sighting. `sighting` is null once the load has finished and @@ -10,11 +11,10 @@ export function useSighting(id: string) { variables: { id }, }) - // A failed refetch is shown through `error`, so the rejected promise - // needs no handling of its own. - const retry = () => { - refetch().catch(() => {}) + return { + sighting: data?.sighting ?? null, + loading, + error, + retry: createRetry(refetch), } - - return { sighting: data?.sighting ?? null, loading, error, retry } } diff --git a/frontend/src/utils/create-retry.ts b/frontend/src/utils/create-retry.ts new file mode 100644 index 0000000..5de690b --- /dev/null +++ b/frontend/src/utils/create-retry.ts @@ -0,0 +1,10 @@ +/** + * Turns a query's `refetch` into a retry callback for an error message's + * button. A failed refetch is shown through the query's `error`, so the + * rejected promise needs no handling of its own. + */ +export function createRetry(refetch: () => Promise) { + return () => { + refetch().catch(() => {}) + } +}