web: start using graphql-codegen, switch to urql, use graphql data a few places

This commit is contained in:
2024-05-10 04:45:53 +02:00
parent 97cfb05710
commit 6f021e4842
16 changed files with 5855 additions and 374 deletions

1
web/.env Normal file
View File

@ -0,0 +1 @@
GRAPHQL_ENDPOINT=https://cms.neuf.kult.444.no/api/graphql/

View File

@ -1,36 +1,15 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started ## Development
First, run the development server: Run the development server:
```bash ```bash
npm run dev npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. Update GraphQL definitions from `http://127.0.0.1:8000/api/graphql/`:
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. ```bash
npm run codegen
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ```
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

14
web/codegen.ts Normal file
View File

@ -0,0 +1,14 @@
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "http://127.0.0.1:8000/api/graphql/",
documents: ["src/**/*.tsx"],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
"./src/gql/": {
preset: "client",
},
},
};
export default config;

4591
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,15 +6,20 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"codegen": "graphql-codegen"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.10.2", "@graphql-codegen/cli": "^5.0.2",
"@apollo/experimental-nextjs-app-support": "^0.10.0", "@graphql-codegen/client-preset": "^4.2.5",
"@parcel/watcher": "^2.4.1",
"@urql/next": "^1.1.1",
"graphql": "^16.8.1",
"next": "14.2.3", "next": "14.2.3",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"sass": "^1.77.0" "sass": "^1.77.0",
"urql": "^4.0.7"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",

View File

@ -1,64 +1,50 @@
import { gql } from "@apollo/client"; import { graphql } from "@/gql";
import { EventFragment } from "@/gql/graphql";
import { getClient } from "@/app/client"; import { getClient } from "@/app/client";
import { Blocks } from "@/components/blocks/Blocks";
export async function generateStaticParams() { export async function generateStaticParams() {
const query = gql(` const allEventSlugsQuery = graphql(`
{ query allEventSlugs {
pages(contentType: "events.EventPage") { pages(contentType: "events.EventPage") {
id id
slug slug
} }
} }
`); `);
const { data } = await getClient().query({ const { data } = await getClient().query(allEventSlugsQuery);
query: query,
});
return data.pages.map((page: any) => ({ return data?.pages.map((page: any) => ({
slug: page.slug, slug: page.slug,
})); }));
} }
export default async function Page({ params }: { params: { slug: string } }) { export default async function Page({ params }: { params: { slug: string } }) {
const query = gql(` const eventBySlugQuery = graphql(`
query ($slug: String!) { query eventBySlug($slug: String!) {
event: page(contentType: "events.EventPage", slug: $slug) { event: page(contentType: "events.EventPage", slug: $slug) {
id
slug
title
... on EventPage { ... on EventPage {
body { ...Event
id
blockType
}
} }
} }
} }
`); `);
// const response = await getClient() const { data } = await getClient().query(eventBySlugQuery, {
// .query({ slug: params.slug,
// query: query,
// variables: { slug: params.slug },
// })
// .then()
// .catch((e) => console.error(e.networkError.result.errors));
const { data } = await getClient().query({
query: query,
variables: { slug: params.slug },
}); });
const { event } = data; const event = (data?.event ?? {}) as EventFragment;
console.log("event", event);
return ( return (
<main className="site-main" id="main"> <main className="site-main" id="main">
<section className="page-header"> <section className="page-header">
<h1>Et enkeltarrangement</h1> <h1>{event.title}</h1>
<p>!!</p>
</section> </section>
<section className="page-content"> <section className="page-content">
<div key={event.id}>{event.title}</div> <Blocks blocks={event.body} />
</section> </section>
</main> </main>
); );

View File

@ -1,41 +1,53 @@
import { gql } from "@apollo/client"; import { graphql } from "@/gql";
import { EventFragment } from "@/gql/graphql";
import { getClient } from "@/app/client"; import { getClient } from "@/app/client";
import { EventList } from "@/components/events/EventList"; import { EventList } from "@/components/events/EventList";
export default async function Page() { const EventFragmentDefinition = graphql(`
const query = gql(` fragment Event on EventPage {
{ __typename
pages(contentType: "events.EventPage") {
id id
slug slug
title title
... on EventPage {
body { body {
id id
blockType blockType
field
... on RichTextBlock {
rawValue
value
} }
} }
featuredImage {
src
}
facebookUrl
ticketUrl
priceRegular
priceMember
priceRegular
}
`);
export default async function Page() {
const allEventsQuery = graphql(`
query allEvents {
events: pages(contentType: "events.EventPage") {
... on EventPage {
...Event
}
} }
} }
`); `);
const { data } = await getClient().query({ const { data, error } = await getClient().query(allEventsQuery, {});
query: query, const events = (data?.events ?? []) as EventFragment[]
});
return ( return (
<main className="site-main" id="main"> <main className="site-main" id="main">
<section className="page-header"> <section className="page-header">
<h1>Arrangementer</h1> <h1>Arrangementer</h1>
<p>woo</p>
</section> </section>
{data.pages && ( <EventList events={events} />
<section className="page-content">
{data.pages.map((event) => (
<div key={event.id}>{event.title}</div>
))}
</section>
)}
<EventList />
</main> </main>
); );
} }

View File

@ -1,11 +1,11 @@
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; import { cacheExchange, createClient, fetchExchange } from "@urql/core";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc"; import { registerUrql } from "@urql/next/rsc";
export const { getClient } = registerApolloClient(() => { const makeClient = () => {
return new ApolloClient({ return createClient({
cache: new InMemoryCache(), url: process.env.GRAPHQL_ENDPOINT ?? "",
link: new HttpLink({ exchanges: [cacheExchange, fetchExchange],
uri: "https://cms.neuf.kult.444.no/api/graphql/",
}),
});
}); });
};
export const { getClient } = registerUrql(makeClient);

View File

@ -1,12 +1,25 @@
import { graphql } from "@/gql";
import {EventFragment} from "@/gql/graphql"
import { getClient } from "@/app/client";
import { EventList } from "@/components/events/EventList"; import { EventList } from "@/components/events/EventList";
import { Body } from "@/components/general/Body"; import { Body } from "@/components/general/Body";
import Image from "next/image"; import Image from "next/image";
export default function Home() { export default async function Home() {
const homeQuery = graphql(`
query home {
events: pages(contentType: "events.EventPage") {
...Event
}
}
`);
const { data, error } = await getClient().query(homeQuery, {});
const events = (data?.events ?? []) as EventFragment[]
return ( return (
<main className="site-main" id="main"> <main className="site-main" id="main">
<div> <div>
<EventList /> <EventList events={events} />
<blockquote>«Hvor Glæden hersker, er alltid Fest»</blockquote> <blockquote>«Hvor Glæden hersker, er alltid Fest»</blockquote>
<p className="lead"> <p className="lead">
Sed sodales nunc quis sapien malesuada, a faucibus turpis blandit. Sed sodales nunc quis sapien malesuada, a faucibus turpis blandit.

View File

@ -0,0 +1,49 @@
export const RichTextBlock = ({ block }: any) => {
return (
<div
className="rich-text-block"
dangerouslySetInnerHTML={{ __html: block.value }}
></div>
);
};
export const Blocks = ({ blocks }: any) => {
return blocks.map((block: any) => {
switch (block.blockType) {
case "RichTextBlock":
return <RichTextBlock block={block} />;
break;
default:
return <div>Unsupported block type {block.blockType}</div>;
console.log("unsupported block", block);
}
});
};
/*
StreamFieldBlock
CharBlock
TextBlock
EmailBlock
IntegerBlock
FloatBlock
DecimalBlock
RegexBlock
URLBlock
BooleanBlock
DateBlock
TimeBlock
DateTimeBlock
RichTextBlock
RawHTMLBlock
BlockQuoteBlock
ChoiceBlock
StreamBlock
StructBlock
StaticBlock
ListBlock
EmbedBlock
PageChooserBlock
DocumentChooserBlock
ImageChooserBlock
*/

View File

@ -1,13 +1,17 @@
import { EventFragment } from "@/gql/graphql";
import styles from "./eventItem.module.scss"; import styles from "./eventItem.module.scss";
import Link from "next/link";
export const EventItem = () => { export const EventItem = ({ event }: { event: EventFragment }) => {
return ( return (
<Link href={`/arrangementer/${event.slug}`}>
<li className={`${styles.eventItem} linkItem`}> <li className={`${styles.eventItem} linkItem`}>
<div className={styles.image}></div> <div className={styles.image}></div>
<div className={styles.text}> <div className={styles.text}>
<h1 className={styles.title}>Arrangementstittel</h1> <h1 className={styles.title}>{event.title}</h1>
<p className={styles.details}>Detaljer og tidspunkt</p> <p className={styles.details}>Detaljer og tidspunkt</p>
</div> </div>
</li> </li>
</Link>
); );
}; };

View File

@ -1,13 +1,13 @@
import { EventFragment } from "@/gql/graphql";
import { EventItem } from "./EventItem"; import { EventItem } from "./EventItem";
import styles from "./eventList.module.scss"; import styles from "./eventList.module.scss";
export const EventList = () => { export const EventList = ({ events }: { events: EventFragment[] }) => {
return ( return (
<ul className={styles.eventList}> <ul className={styles.eventList}>
<EventItem /> {events.map((event) => (
<EventItem /> <EventItem key={event.id} event={event} />
<EventItem /> ))}
<EventItem />
</ul> </ul>
); );
}; };

View File

@ -0,0 +1,67 @@
/* eslint-disable */
import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import { FragmentDefinitionNode } from 'graphql';
import { Incremental } from './graphql';
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
infer TType,
any
>
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never
: never
: never;
// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}
export function makeFragmentData<
F extends DocumentTypeDecoration<any, any>,
FT extends ResultOf<F>
>(data: FT, _fragment: F): FragmentType<F> {
return data as FragmentType<F>;
}
export function isFragmentReady<TQuery, TFrag>(
queryNode: DocumentTypeDecoration<TQuery, any>,
fragmentNode: TypedDocumentNode<TFrag>,
data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
): data is FragmentType<typeof fragmentNode> {
const deferredFields = (queryNode as { __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> } }).__meta__
?.deferredFields;
if (!deferredFields) return true;
const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
const fragName = fragDef?.name?.value;
const fields = (fragName && deferredFields[fragName]) || [];
return fields.length > 0 && fields.every(field => data && field in data);
}

62
web/src/gql/gql.ts Normal file
View File

@ -0,0 +1,62 @@
/* eslint-disable */
import * as types from './graphql';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
"\n query allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n ": types.AllEventSlugsDocument,
"\n query eventBySlug($slug: String!) {\n event: page(contentType: \"events.EventPage\", slug: $slug) {\n ... on EventPage {\n ...Event\n }\n }\n }\n ": types.EventBySlugDocument,
"\n fragment Event on EventPage {\n __typename\n id\n slug\n title\n body {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n }\n featuredImage {\n src\n }\n facebookUrl\n ticketUrl\n priceRegular\n priceMember\n priceRegular\n }\n": types.EventFragmentDoc,
"\n query allEvents {\n events: pages(contentType: \"events.EventPage\") {\n ... on EventPage {\n ...Event\n }\n }\n }\n ": types.AllEventsDocument,
"\n query home {\n events: pages(contentType: \"events.EventPage\") {\n ...Event\n }\n }\n ": types.HomeDocument,
};
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*
*
* @example
* ```ts
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
* ```
*
* The query argument is unknown!
* Please regenerate the types.
*/
export function graphql(source: string): unknown;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n "): (typeof documents)["\n query allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n "];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query eventBySlug($slug: String!) {\n event: page(contentType: \"events.EventPage\", slug: $slug) {\n ... on EventPage {\n ...Event\n }\n }\n }\n "): (typeof documents)["\n query eventBySlug($slug: String!) {\n event: page(contentType: \"events.EventPage\", slug: $slug) {\n ... on EventPage {\n ...Event\n }\n }\n }\n "];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment Event on EventPage {\n __typename\n id\n slug\n title\n body {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n }\n featuredImage {\n src\n }\n facebookUrl\n ticketUrl\n priceRegular\n priceMember\n priceRegular\n }\n"): (typeof documents)["\n fragment Event on EventPage {\n __typename\n id\n slug\n title\n body {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n }\n featuredImage {\n src\n }\n facebookUrl\n ticketUrl\n priceRegular\n priceMember\n priceRegular\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query allEvents {\n events: pages(contentType: \"events.EventPage\") {\n ... on EventPage {\n ...Event\n }\n }\n }\n "): (typeof documents)["\n query allEvents {\n events: pages(contentType: \"events.EventPage\") {\n ... on EventPage {\n ...Event\n }\n }\n }\n "];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query home {\n events: pages(contentType: \"events.EventPage\") {\n ...Event\n }\n }\n "): (typeof documents)["\n query home {\n events: pages(contentType: \"events.EventPage\") {\n ...Event\n }\n }\n "];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
}
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;

1202
web/src/gql/graphql.ts Normal file

File diff suppressed because it is too large Load Diff

2
web/src/gql/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./fragment-masking";
export * from "./gql";