Skip to content

Commit 4d6c290

Browse files
committed
feat: Basic support Image and RGBA fields.
feat: ResultListing now supports a `image` property. fix: Addresses count issues with pagination. feat: Introduces authorization feature.
1 parent 616a8d3 commit 4d6c290

20 files changed

+746
-207
lines changed

next.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ const nextConfig = {
99
* as the `basePath` for the Next.js application.
1010
*/
1111
basePath: STATIC._static?.host?.base_path || undefined,
12+
images: {
13+
/**
14+
* @see https://nextjs.org/docs/pages/api-reference/components/image#unoptimized
15+
*/
16+
unoptimized: true,
17+
},
1218
};
1319

1420
export default nextConfig;

src/app/authenticate/page.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use client";
2+
import React, { useEffect } from "react";
3+
import { useRouter } from "next/navigation";
4+
import { useGlobusAuth } from "@/globus/globus-auth-context/useGlobusAuth";
5+
import { Spinner } from "@chakra-ui/react";
6+
import { isFeatureEnabled } from "../../../static";
7+
8+
function Authenticate() {
9+
const auth = useGlobusAuth();
10+
const router = useRouter();
11+
const instance = auth.authorization;
12+
13+
useEffect(() => {
14+
async function attempt() {
15+
if (auth.isAuthenticated) {
16+
return router.replace("/");
17+
} else {
18+
await instance?.handleCodeRedirect({
19+
shouldReplace: false,
20+
});
21+
}
22+
}
23+
attempt();
24+
}, [router, instance, instance?.handleCodeRedirect, auth.isAuthenticated]);
25+
return <Spinner />;
26+
}
27+
28+
export default isFeatureEnabled("authentication") ? Authenticate : () => null;

src/app/layout.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,58 @@
1+
"use client";
2+
13
import React from "react";
24
import { ThemeProvider } from "./theme-provider";
5+
import {
6+
getEnvironment,
7+
getRedirectUri,
8+
getAttribute,
9+
isFeatureEnabled,
10+
} from "../../static";
11+
12+
import { GlobusAuthorizationManagerProvider } from "@/globus/globus-auth-context/Provider";
13+
import Header from "@/components/Header";
14+
15+
const env = getEnvironment();
16+
if (env) {
17+
// @ts-ignore
18+
globalThis.GLOBUS_SDK_ENVIRONMENT = env;
19+
}
20+
21+
const redirect = getRedirectUri();
22+
const client = getAttribute("globus.application.client_id");
23+
const scopes = "urn:globus:auth:scope:search.api.globus.org:search";
324

425
export default function RootLayout({
526
children,
627
}: {
728
children: React.ReactNode;
829
}) {
30+
if (!isFeatureEnabled("authentication")) {
31+
return (
32+
<html lang="en">
33+
<body>
34+
<ThemeProvider>
35+
<Header />
36+
{children}
37+
</ThemeProvider>
38+
</body>
39+
</html>
40+
);
41+
}
42+
943
return (
1044
<html lang="en">
1145
<body>
12-
<ThemeProvider>{children}</ThemeProvider>
46+
<ThemeProvider>
47+
<GlobusAuthorizationManagerProvider
48+
redirect={redirect}
49+
client={client}
50+
scopes={scopes}
51+
>
52+
<Header />
53+
{children}
54+
</GlobusAuthorizationManagerProvider>
55+
</ThemeProvider>
1356
</body>
1457
</html>
1558
);

src/app/page.tsx

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,14 @@
11
"use client";
22
import React from "react";
33

4-
import { Container, Box, Heading, HStack, Image } from "@chakra-ui/react";
5-
6-
import { getAttribute } from "../../static";
4+
import { Container } from "@chakra-ui/react";
75

86
import SearchProvider from "./search-provider";
97
import { Search } from "@/components/Search";
108

11-
const SEARCH_INDEX = getAttribute("globus.search.index");
12-
const LOGO = getAttribute("content.logo");
13-
const HEADLINE = getAttribute(
14-
"content.headline",
15-
`Search Index ${SEARCH_INDEX}`,
16-
);
17-
189
export default function Index() {
1910
return (
2011
<>
21-
<Box bg="brand.800">
22-
<HStack p={4} spacing="24px">
23-
{LOGO && (
24-
<Image
25-
src={LOGO.src}
26-
alt={LOGO.alt}
27-
boxSize="100px"
28-
objectFit="contain"
29-
/>
30-
)}
31-
<Heading size="md" color="white">
32-
{HEADLINE}
33-
</Heading>
34-
</HStack>
35-
</Box>
3612
<Container maxW="container.xl">
3713
<main>
3814
<SearchProvider>

src/components/Field.tsx

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React from "react";
2+
import { get } from "lodash";
3+
import { Heading, Box, HStack } from "@chakra-ui/react";
4+
5+
import RgbaField from "./Fields/RgbaField";
6+
import ImageField from "./Fields/ImageField";
7+
import FallbackField from "./Fields/FallbackField";
8+
9+
import type { GMetaResult } from "../globus/search";
10+
11+
export type FieldDefinition =
12+
| string
13+
| {
14+
label: string;
15+
property: string;
16+
type?: string;
17+
}
18+
| {
19+
label: string;
20+
value: unknown;
21+
type?: string;
22+
};
23+
24+
type ProcessedField = {
25+
label: string | undefined;
26+
value: unknown;
27+
type: string | undefined;
28+
};
29+
30+
export function getProcessedField(
31+
field: FieldDefinition,
32+
data: GMetaResult,
33+
): ProcessedField {
34+
/**
35+
* Ensure we're working with a FieldDefinition object.
36+
*/
37+
const def = typeof field === "string" ? { property: field } : field;
38+
let value;
39+
if ("value" in def) {
40+
value = def.value;
41+
} else {
42+
value = get(data, def.property);
43+
}
44+
return {
45+
label: undefined,
46+
type: undefined,
47+
value,
48+
...def,
49+
};
50+
}
51+
52+
export const FieldValue = ({
53+
value,
54+
type,
55+
}: {
56+
value: unknown;
57+
type?: string;
58+
}) => {
59+
if (type === "rgba") {
60+
return <RgbaField value={value} />;
61+
}
62+
if (type === "image") {
63+
return <ImageField value={value} />;
64+
}
65+
return <FallbackField value={value} />; // fallback
66+
};
67+
68+
export const Field = ({
69+
field,
70+
gmeta,
71+
condensed,
72+
}: {
73+
field: FieldDefinition;
74+
gmeta: GMetaResult;
75+
condensed?: boolean;
76+
}) => {
77+
const processedField = getProcessedField(field, gmeta);
78+
if (condensed) {
79+
return (
80+
<HStack>
81+
{processedField.label && (
82+
<Heading as="h2" size="sm" my={2}>
83+
{processedField.label}
84+
</Heading>
85+
)}
86+
<FieldValue value={processedField.value} type={processedField.type} />
87+
</HStack>
88+
);
89+
}
90+
91+
return (
92+
<Box my="2">
93+
{processedField.label && (
94+
<Heading as="h2" size="sm" my={2}>
95+
{processedField.label}
96+
</Heading>
97+
)}
98+
<FieldValue value={processedField.value} type={processedField.type} />
99+
</Box>
100+
);
101+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from "react";
2+
import { Box, Code, Text } from "@chakra-ui/react";
3+
4+
type Value = unknown;
5+
6+
// function isValidValue(value: unknown): value is Value {
7+
// return true;
8+
// }
9+
10+
/**
11+
* A fallback field that will introspect the value and render it as best as it can.
12+
*/
13+
export default function FallbackField({ value }: { value: Value }) {
14+
if (
15+
typeof value === "string" ||
16+
typeof value === "number" ||
17+
typeof value === "boolean"
18+
) {
19+
return <Text as="p">{value}</Text>;
20+
}
21+
if (Array.isArray(value)) {
22+
return value.map((v, i) => (
23+
<Box key={i}>
24+
<FallbackField value={v} />
25+
</Box>
26+
));
27+
}
28+
return (
29+
<Code as="pre" display="block" borderRadius={2} my={1}>
30+
{JSON.stringify(value, null, 2)}
31+
</Code>
32+
);
33+
}

src/components/Fields/ImageField.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { useState } from "react";
2+
import Image from "next/image";
3+
import { Box, Code, HStack, Spinner, Text } from "@chakra-ui/react";
4+
5+
type Value =
6+
| string
7+
| {
8+
src: string;
9+
alt?: string;
10+
};
11+
12+
function isValidValue(value: unknown): value is Value {
13+
return (
14+
typeof value === "string" ||
15+
(typeof value === "object" && value !== null && "src" in value)
16+
);
17+
}
18+
19+
/**
20+
* Render a field as an image.
21+
*/
22+
export default function ImageField({ value }: { value: unknown }) {
23+
const [loading, setLoading] = useState(true);
24+
const [error, setError] = useState(false);
25+
if (!isValidValue(value)) {
26+
return;
27+
}
28+
29+
const config = typeof value === "string" ? { src: value } : value;
30+
31+
return (
32+
<Box>
33+
{loading && (
34+
<HStack>
35+
<Spinner emptyColor="gray.200" color="blue.500" />
36+
<Text>Loading image...</Text>
37+
</HStack>
38+
)}
39+
{error && (
40+
<Code fontSize="xs" variant="outline" colorScheme="orange">
41+
Unable to load image.
42+
</Code>
43+
)}
44+
{!error && (
45+
<Image
46+
width={200}
47+
height={200}
48+
src={config.src}
49+
alt={config.alt ?? ""}
50+
onLoad={() => setLoading(false)}
51+
onError={() => {
52+
setLoading(false);
53+
setError(true);
54+
}}
55+
/>
56+
)}
57+
</Box>
58+
);
59+
}

src/components/Fields/RgbaField.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from "react";
2+
import { Box, Code, HStack } from "@chakra-ui/react";
3+
4+
type Value = (string | number)[];
5+
6+
function isValidValue(value: unknown): value is Value {
7+
return Array.isArray(value) && value.length >= 3;
8+
}
9+
10+
/**
11+
* Render a field as an RGBA color.
12+
*/
13+
export default function RgbaField({ value }: { value: unknown }) {
14+
if (!isValidValue(value)) {
15+
return;
16+
}
17+
return (
18+
<HStack>
19+
<Box w="100px" h="100px" bg={`rgba(${value.join(",")})`} />
20+
<Code>{value.join(",")}</Code>
21+
</HStack>
22+
);
23+
}

0 commit comments

Comments
 (0)