├── .env.local.example
├── .gitignore
├── .vscode
└── settings.json
├── Dockerfile
├── README.md
├── bsconfig.json
├── k8s
└── deploy.yaml
├── next.config.js
├── package.json
├── public
├── favicon.ico
└── vercel.svg
├── relay.config.js
├── schema.graphql
├── src
├── __generated__
│ └── .gitignore
├── bindings
│ └── Next.res
├── features
│ └── pokemon
│ │ ├── PokemonDetail.module.css
│ │ ├── PokemonDetail.res
│ │ ├── PokemonFeed.module.css
│ │ ├── PokemonFeed.res
│ │ ├── PokemonItem.module.css
│ │ ├── PokemonItem.res
│ │ └── PokemonUtils.res
├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── graphql.ts
│ ├── index.res
│ └── pokemon
│ │ └── [pokemonId].res
└── relay
│ ├── RelayEnv.res
│ ├── environment.ts
│ └── hydrateRelayStore.ts
├── styles
└── globals.css
├── tsconfig.json
└── yarn.lock
/.env.local.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_API_URL=http://localhost:3000/api
2 | NODE_TLS_REJECT_UNAUTHORIZED=0
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | lib
39 | .bsb.lock
40 | *.bs.js
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | #
2 | # Dockerfile
3 | #
4 |
5 | # Install dependencies only when needed
6 | FROM node:18-alpine AS deps
7 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
8 | RUN apk add --no-cache libc6-compat
9 | WORKDIR /app
10 |
11 | # Install dependencies based on the preferred package manager
12 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
13 | RUN \
14 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
15 | elif [ -f package-lock.json ]; then npm ci; \
16 | elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
17 | else echo "Lockfile not found." && exit 1; \
18 | fi
19 |
20 |
21 | # Rebuild the source code only when needed
22 | FROM node:18-alpine AS builder
23 | WORKDIR /app
24 | COPY --from=deps /app/node_modules ./node_modules
25 | COPY . .
26 |
27 | # Next.js collects completely anonymous telemetry data about general usage.
28 | # Learn more here: https://nextjs.org/telemetry
29 | # Uncomment the following line in case you want to disable telemetry during the build.
30 | # ENV NEXT_TELEMETRY_DISABLED 1
31 |
32 | RUN yarn build
33 |
34 | # If using npm comment out above and use below instead
35 | # RUN npm run build
36 |
37 | # Production image, copy all the files and run next
38 | FROM node:18-alpine AS runner
39 | WORKDIR /app
40 |
41 | ENV NODE_ENV production
42 | # Uncomment the following line in case you want to disable telemetry during runtime.
43 | # ENV NEXT_TELEMETRY_DISABLED 1
44 |
45 | RUN addgroup --system --gid 1001 nodejs
46 | RUN adduser --system --uid 1001 nextjs
47 |
48 | COPY --from=builder /app/public ./public
49 |
50 | # Automatically leverage output traces to reduce image size
51 | # https://nextjs.org/docs/advanced-features/output-file-tracing
52 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
53 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
54 |
55 | ###
56 | ### aws-lambda-adapter
57 | ###
58 | # COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.6.0 /lambda-adapter /opt/extensions/lambda-adapter
59 | ###
60 | ###
61 | ###
62 |
63 | USER nextjs
64 |
65 | EXPOSE 3000
66 |
67 | ENV PORT 3000
68 |
69 | CMD ["node", "server.js"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rescript + Relay + Next.js + SSR example!
2 |
3 | * using graphql example by [GraphQL-Pokemon](https://graphql-pokemon.vercel.app/)
4 |
5 | ## Concept
6 |
7 | 1. Make feature component by using relay fragment (like [PokemonDetail.res](./src/features/pokemon/PokemonDetail.res))
8 | 2. Write page by rescript. (Warning; rescript file name should be unique by your project) ([/pages/pokemon/[pokemonEnum].res](./src/pages/pokemon/[pokemonEnum].res))
9 | 3. In page, make query and connect using feature fragments
10 | 4. Make environment and execute query and return dehydrate store data at `getServerSideProps` (This process is configure by [RelayEnv.SSR.make](./src/relay/RelayEnv.res) function and [hydrateRelayStore.ts](./src/relay/hydrateRelayStore.ts))
11 | 5. Execute query at page renderer, pass the `fragmentRefs` to feature component
12 | 6. complete!
13 |
--------------------------------------------------------------------------------
/bsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rescript-relay-next-ssr-template",
3 | "uncurried": true,
4 | "jsx": {
5 | "version": 4,
6 | "mode": "automatic"
7 | },
8 | "sources": {
9 | "dir": "src",
10 | "subdirs": true
11 | },
12 | "package-specs": [
13 | {
14 | "module": "es6",
15 | "in-source": true
16 | }
17 | ],
18 | "suffix": ".bs.js",
19 | "ppx-flags": ["rescript-relay/ppx"],
20 | "namespace": true,
21 | "bs-dependencies": [
22 | "@rescript/react",
23 | "rescript-relay"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/k8s/deploy.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | labels:
5 | app: rescript-relay-next-ssr
6 | name: rescript-relay-next-ssr
7 | spec:
8 | selector:
9 | matchLabels:
10 | app: rescript-relay-next-ssr
11 | template:
12 | metadata:
13 | labels:
14 | app: rescript-relay-next-ssr
15 | spec:
16 | containers:
17 | - name: app
18 | image: docker.io/minukang/rescript-relay-next-ssr
19 | imagePullPolicy: Always
20 | ports:
21 | - containerPort: 3000
22 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const bsconfig = require("./bsconfig.json");
2 |
3 | /** @type {import('next').NextConfig} */
4 | const nextConfig = {
5 | output: 'standalone',
6 | reactStrictMode: true,
7 | swcMinify: true,
8 | pageExtensions: ["tsx", "ts", "bs.js"],
9 | experimental: {
10 | scrollRestoration: true,
11 | transpilePackages: ["rescript"]
12 | .concat(
13 | bsconfig["bs-dependencies"]?.filter(
14 | (dep) => !bsconfig["pinned-dependencies"]?.includes(dep)
15 | )
16 | )
17 | .concat(["react-relay-network-modern"]),
18 | },
19 | };
20 |
21 | module.exports = nextConfig;
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rescript-relay-next-ssr-template",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "yarn relay && yarn res && concurrently 'next dev' 'yarn relay:watch' 'yarn res:watch'",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "res": "rescript build -with-deps",
11 | "res:watch": "yarn res -w",
12 | "relay": "rescript-relay-compiler",
13 | "relay:watch": "rescript-relay-compiler --watch"
14 | },
15 | "dependencies": {
16 | "@rescript/react": "0.12.0-alpha.3",
17 | "@types/node": "18.11.10",
18 | "@types/react": "18.0.26",
19 | "@types/react-dom": "18.0.9",
20 | "concurrently": "^7.6.0",
21 | "graphql": "^16.6.0",
22 | "next": "13.0.6",
23 | "react": "18.2.0",
24 | "react-dom": "18.2.0",
25 | "react-relay": "^15.0.0",
26 | "react-relay-network-modern": "^6.2.1",
27 | "relay-runtime": "^15.0.0",
28 | "rescript-relay": "2.0.2",
29 | "typescript": "4.9.3"
30 | },
31 | "devDependencies": {
32 | "@types/react-relay": "^14.1.2",
33 | "@types/relay-runtime": "^14.1.5",
34 | "rescript": "11.0.0-beta.4"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minuukang/rescript-relay-next-ssr-template/84b96dcde2f9e40f2914e07f5ee22afbc5007670/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/relay.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | src: "./",
3 | schema: "./schema.graphql",
4 | artifactDirectory: "./src/__generated__",
5 | customScalars: {
6 | DateTime: "string"
7 | }
8 | };
--------------------------------------------------------------------------------
/schema.graphql:
--------------------------------------------------------------------------------
1 | type Query {
2 | pokemons(limit: Int, skip: Int): [Pokemon!]
3 | pokemon(id: ID!): Pokemon
4 | }
5 |
6 | type Attack {
7 | name: String
8 | type: PokemonType
9 | damage: Int
10 | }
11 |
12 | enum PokemonType {
13 | Grass
14 | Poison
15 | Fire
16 | Flying
17 | Water
18 | Bug
19 | Normal
20 | Electric
21 | Ground
22 | Fairy
23 | Fighting
24 | Psychic
25 | Rock
26 | Steel
27 | Ice
28 | Ghost
29 | Dragon
30 | Dark
31 | }
32 |
33 | type EvolutionRequirement {
34 | amount: Int
35 | name: String
36 | }
37 |
38 | type PokemonDimension {
39 | minimum: String!
40 | maximum: String!
41 | }
42 |
43 | type AttacksConnection {
44 | fast: [Attack]
45 | special: [Attack]
46 | }
47 |
48 | type Pokemon {
49 | id: ID!
50 | name: String!
51 | classification: String
52 | types: [PokemonType!]!
53 | resistant: [PokemonType!]!
54 | weaknesses: [PokemonType!]!
55 | evolutionRequirements: [EvolutionRequirement]
56 | weight: PokemonDimension!
57 | height: PokemonDimension!
58 | attacks: AttacksConnection
59 | fleeRate: Float
60 | # Likelihood of an attempt to catch a Pokémon to fail.
61 |
62 | maxCP: Int
63 | # Maximum combat power a Pokémon may achieve at max level.
64 |
65 | maxHP: Int
66 | # Maximum health points a Pokémon may achieve at max level.
67 |
68 | evolutions: [Pokemon!]
69 | }
70 |
--------------------------------------------------------------------------------
/src/__generated__/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/src/bindings/Next.res:
--------------------------------------------------------------------------------
1 | module Req = {
2 | type t
3 |
4 | @get
5 | external cookies: t => Js.Dict.t = "cookies"
6 |
7 | @get external method: t => string = "method"
8 | @get external url: t => string = "url"
9 | @get external port: t => int = "port"
10 | @get external headers: t => Js.Dict.t = "headers"
11 | @get
12 | external rawHeaders: t => array = "rawHeaders"
13 | @get
14 | external rawTrailers: t => array = "rawTrailers"
15 | @get external aborted: t => bool = "aborted"
16 | @get external complete: t => bool = "complete"
17 | @send external destroy: t => unit = "destroy"
18 | @send
19 | external destroyWithError: (t, Js.Exn.t) => bool = "destroy"
20 | @get external statusCode: t => int = "statusCode"
21 | @get
22 | external statusMessage: t => string = "statusMessage"
23 | @get
24 | external trailers: t => Js.Dict.t = "trailers"
25 | }
26 |
27 | module Res = {
28 | type t
29 |
30 | @get external statusCode: t => int = "statusCode"
31 | @get external statusMessage: t => string = "statusMessage"
32 | @set
33 | external setStatusCode: (t, int) => unit = "statusCode"
34 | @send
35 | external getHeader: (t, string) => option = "getHeader"
36 | @send
37 | external setHeader: (t, string, string) => unit = "setHeader"
38 | @send external end: (t, 'a) => unit = "end"
39 | }
40 |
41 | module GetServerSideProps = {
42 | type result =
43 | | Props({.})
44 | | NotFound
45 | | Redirect(string)
46 | | RedirectPermanent(string)
47 | | RedirectStatusCode(string, int)
48 |
49 | type context<'params> = {
50 | req: Req.t,
51 | res: Res.t,
52 | params: 'params,
53 | query: Js.Dict.t,
54 | resolvedUrl: string,
55 | locale: string,
56 | locales: array,
57 | defaultLocale: string,
58 | }
59 |
60 | type t<'a, 'b> = context<'b> => Js.Promise.t
61 |
62 | let parseResult = result =>
63 | switch result {
64 | | Props(result) => {"props": result}->Obj.magic
65 | | NotFound => {"notFound": true}->Obj.magic
66 | | Redirect(url) => {"redirect": {"destination": url, "permanent": false}}->Obj.magic
67 | | RedirectPermanent(url) => {"redirect": {"destination": url, "permanent": true}}->Obj.magic
68 | | RedirectStatusCode(url, statusCode) =>
69 | {
70 | "redirect": {"destination": url, "statusCode": statusCode},
71 | }->Obj.magic
72 | }
73 |
74 | let make = async (callback: t<'a, 'b>, context: context<'b>) => {
75 | (await callback(context))->parseResult
76 | }
77 | }
78 |
79 | module Link = {
80 | @module("next/link") @react.component
81 | external make: (
82 | ~href: string,
83 | @as("as") ~_as: string=?,
84 | ~prefetch: bool=?,
85 | ~scroll: bool=?,
86 | ~replace: option=?,
87 | ~shallow: option=?,
88 | ~passHref: option=?,
89 | ~children: React.element,
90 | ~locale: string=?,
91 | ~ref: ReactDOM.domRef=?,
92 | ) => React.element = "default"
93 | }
94 |
95 | module Router = {
96 | module Events = {
97 | type t
98 |
99 | @send
100 | external on: (
101 | t,
102 | @string
103 | [
104 | | #routeChangeStart(string => unit)
105 | | #routeChangeComplete(string => unit)
106 | | #hashChangeComplete(string => unit)
107 | ],
108 | ) => unit = "on"
109 |
110 | @send
111 | external off: (
112 | t,
113 | @string
114 | [
115 | | #routeChangeStart(string => unit)
116 | | #routeChangeComplete(string => unit)
117 | | #hashChangeComplete(string => unit)
118 | ],
119 | ) => unit = "off"
120 | }
121 |
122 | type router = {
123 | route: string,
124 | asPath: string,
125 | events: Events.t,
126 | pathname: string,
127 | query: Js.Dict.t,
128 | }
129 |
130 | type pathObj = {
131 | pathname: string,
132 | query: Js.Dict.t,
133 | }
134 |
135 | @deriving(abstract)
136 | type options = {
137 | @optional
138 | scroll: bool,
139 | @optional
140 | prefetch: bool,
141 | @optional
142 | shallow: bool,
143 | }
144 |
145 | type state = {
146 | url: string,
147 | @as("as") _as: string,
148 | options: options,
149 | }
150 |
151 | @send external push: (router, string) => unit = "push"
152 | @send external pushObj: (router, pathObj) => unit = "push"
153 | @send external pushWithAs: (router, string, option, ~options: options=?) => unit = "push"
154 |
155 | @module("next/router") external useRouter: unit => router = "useRouter"
156 |
157 | @send external replace: (router, string) => unit = "replace"
158 | @send external replaceObj: (router, pathObj) => unit = "replace"
159 | @send
160 | external replaceWithAs: (router, string, option, ~options: options=?) => unit = "replace"
161 |
162 | @scope("options") @set external setScrollOption: (state, bool) => unit = "scroll"
163 |
164 | @send
165 | external beforePopState: (router, state => bool) => unit = "beforePopState"
166 |
167 | @send
168 | external clearBeforePopState: router => unit = "beforePopState"
169 | }
170 |
--------------------------------------------------------------------------------
/src/features/pokemon/PokemonDetail.module.css:
--------------------------------------------------------------------------------
1 | .list {
2 | display: grid;
3 | grid-template-columns: repeat(10, 1fr);
4 | gap: 10px;
5 | list-style: none;
6 | }
7 |
--------------------------------------------------------------------------------
/src/features/pokemon/PokemonDetail.res:
--------------------------------------------------------------------------------
1 | module Fragment = %relay(`
2 | fragment PokemonDetail_Fragment on Pokemon {
3 | id
4 | name
5 | types
6 | weight {
7 | minimum
8 | maximum
9 | }
10 | height {
11 | minimum
12 | maximum
13 | }
14 | evolutions {
15 | id
16 | ...PokemonItem_Fragment
17 | }
18 | }
19 | `)
20 |
21 | @module("./PokemonDetail.module.css")
22 | external styles: {..} = "default"
23 |
24 | @react.component
25 | let make = (~fragmentRefs) => {
26 | let pokemon = Fragment.use(fragmentRefs)
27 |
28 | {pokemon.name->React.string}
29 |
30 |
31 | - {`weight: ${pokemon.weight.minimum} ~ ${pokemon.weight.maximum}`->React.string}
32 | - {`height: ${pokemon.height.minimum} ~ ${pokemon.height.maximum}`->React.string}
33 |
34 | {`Types`->React.string}
35 |
36 | {pokemon.types
37 | ->Belt.Array.map(type_ =>
38 | - Fragment.pokemonType_toString}>
39 |
`풀`
44 | | #Poison => `독`
45 | | #Fire => `불꽃`
46 | | #Flying => `비행`
47 | | #Water => `물`
48 | | #Bug => `벌레`
49 | | #Normal => `노멀`
50 | | #Electric => `전기`
51 | | #Ground => `땅`
52 | | #Fairy => `페어리`
53 | | #Fighting => `격투`
54 | | #Psychic => `에스퍼`
55 | | #Rock => `바위`
56 | | #Steel => `강철`
57 | | #Ice => `얼음`
58 | | #Ghost => `고스트`
59 | | #Dragon => `드래곤`
60 | | #Dark => `악`
61 | }}
62 | />
63 |
64 | )
65 | ->React.array}
66 |
67 | {switch pokemon.evolutions {
68 | | Some([]) => React.null
69 | | Some(evolutions) =>
70 | <>
71 | {`Evolutions`->React.string}
72 |
73 | {evolutions
74 | ->Belt.Array.map(pokemon =>
75 | -
76 |
77 |
78 | )
79 | ->React.array}
80 |
81 | >
82 | | None => React.null
83 | }}
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/src/features/pokemon/PokemonFeed.module.css:
--------------------------------------------------------------------------------
1 | .list {
2 | display: grid;
3 | grid-template-columns: repeat(10, 1fr);
4 | gap: 10px;
5 | list-style: none;
6 | }
7 |
--------------------------------------------------------------------------------
/src/features/pokemon/PokemonFeed.res:
--------------------------------------------------------------------------------
1 | module Fragment = %relay(`
2 | fragment PokemonFeed_Fragment on Query {
3 | pokemons(limit: 1000) {
4 | id
5 | ...PokemonItem_Fragment
6 | }
7 | }
8 | `)
9 |
10 | @module("./PokemonFeed.module.css")
11 | external styles: {..} = "default"
12 |
13 | @react.component
14 | let make = (~fragmentRefs) => {
15 | let {pokemons} = Fragment.use(fragmentRefs)
16 |
17 |
{`Pokemons`->React.string}
18 |
19 | {pokemons
20 | ->Belt.Option.getWithDefault([])
21 | ->Belt.Array.map(pokemon =>
22 | -
23 |
24 |
25 | )
26 | ->React.array}
27 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/features/pokemon/PokemonItem.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 10px;
3 | }
4 |
5 | .wrapper img {
6 | display: block;
7 | width: 100%;
8 | margin: 0 auto;
9 | aspect-ratio: 1/1;
10 | object-fit: contain;
11 | }
12 |
--------------------------------------------------------------------------------
/src/features/pokemon/PokemonItem.res:
--------------------------------------------------------------------------------
1 | module Fragment = %relay(`
2 | fragment PokemonItem_Fragment on Pokemon {
3 | id
4 | name
5 | }
6 | `)
7 |
8 | @module("./PokemonItem.module.css")
9 | external styles: {..} = "default"
10 |
11 | @react.component
12 | let make = (~fragmentRefs) => {
13 | let pokemon = Fragment.use(fragmentRefs)
14 |
15 |
16 |
17 | {`No.${pokemon.id}: ${pokemon.name}`->React.string}
18 |
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/features/pokemon/PokemonUtils.res:
--------------------------------------------------------------------------------
1 | let makeSpriteImage = name => {
2 | let name = name->Js.String2.toLowerCase
3 | let name = name->Js.String2.replaceByRe(%re("/[\s\']/g"), "")
4 | let name = name->Js.String2.replaceByRe(%re("/-/g"), "_")
5 | `https://projectpokemon.org/images/normal-sprite/${name}.gif`
6 | }
7 |
8 | let makeTypeImage = (type_: RelaySchemaAssets_graphql.enum_PokemonType) => {
9 | switch type_ {
10 | | #Grass => `https://archives.bulbagarden.net/media/upload/a/a8/Grass_icon_SwSh.png`
11 | | #Poison => `https://archives.bulbagarden.net/media/upload/8/8d/Poison_icon_SwSh.png`
12 | | #Fire => `https://archives.bulbagarden.net/media/upload/a/ab/Fire_icon_SwSh.png`
13 | | #Flying => `https://archives.bulbagarden.net/media/upload/b/b5/Flying_icon_SwSh.png`
14 | | #Water => `https://archives.bulbagarden.net/media/upload/8/80/Water_icon_SwSh.png`
15 | | #Bug => `https://archives.bulbagarden.net/media/upload/9/9c/Bug_icon_SwSh.png`
16 | | #Normal => `https://archives.bulbagarden.net/media/upload/9/95/Normal_icon_SwSh.png`
17 | | #Electric => `https://archives.bulbagarden.net/media/upload/7/7b/Electric_icon_SwSh.png`
18 | | #Ground => `https://archives.bulbagarden.net/media/upload/2/27/Ground_icon_SwSh.png`
19 | | #Fairy => `https://archives.bulbagarden.net/media/upload/c/c6/Fairy_icon_SwSh.png`
20 | | #Fighting => `https://archives.bulbagarden.net/media/upload/3/3b/Fighting_icon_SwSh.png`
21 | | #Psychic => `https://archives.bulbagarden.net/media/upload/7/73/Psychic_icon_SwSh.png`
22 | | #Rock => `https://archives.bulbagarden.net/media/upload/1/11/Rock_icon_SwSh.png`
23 | | #Steel => `https://archives.bulbagarden.net/media/upload/0/09/Steel_icon_SwSh.png`
24 | | #Ice => `https://archives.bulbagarden.net/media/upload/1/15/Ice_icon_SwSh.png`
25 | | #Ghost => `https://archives.bulbagarden.net/media/upload/0/01/Ghost_icon_SwSh.png`
26 | | #Dragon => `https://archives.bulbagarden.net/media/upload/7/70/Dragon_icon_SwSh.png`
27 | | #Dark => `https://archives.bulbagarden.net/media/upload/d/d5/Dark_icon_SwSh.png`
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useRef } from "react";
2 | import type { AppProps } from "next/app";
3 | import { RelayEnvironmentProvider } from "react-relay";
4 | import { commitLocalUpdate, RecordSource } from "relay-runtime";
5 | import { makeEnvironment } from "../relay/environment";
6 |
7 | import "../../styles/globals.css";
8 |
9 | interface PageProps {
10 | __relayStore__?: Record>;
11 | }
12 |
13 | function App({ Component, pageProps, router }: AppProps) {
14 | const { __relayStore__ } = pageProps as PageProps;
15 | const environment = useMemo(() => makeEnvironment(), []);
16 | const relayStoreRef = useRef>({});
17 | useMemo(() => {
18 | const key = (router as unknown as { _key: string })._key;
19 | // NOTE: This flag expected that the store will not be reapplied in the same history
20 | // example) pagination of infinite loading connection
21 | if (!relayStoreRef.current[key]) {
22 | relayStoreRef.current[key] = __relayStore__;
23 | if (__relayStore__) {
24 | const recordStore = new RecordSource(__relayStore__);
25 | // NOTE: Bug of relay-runtime merging client connection and server connection
26 | // so, i will delete connection edges by updating from recordStore
27 | commitLocalUpdate(environment, (store) => {
28 | recordStore.getRecordIDs().forEach((dataID) => {
29 | store
30 | .get(dataID)
31 | ?.getLinkedRecords("edges")
32 | ?.forEach((edge) => {
33 | store.delete(edge.getDataID());
34 | });
35 | });
36 | });
37 | environment.getStore().publish(recordStore);
38 | }
39 | }
40 | }, [__relayStore__, environment, router]);
41 |
42 | return (
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | export default App;
50 |
--------------------------------------------------------------------------------
/src/pages/api/graphql.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from "next";
3 |
4 | export default async function handler(
5 | req: NextApiRequest,
6 | res: NextApiResponse
7 | ) {
8 | const response = await fetch(
9 | `https://trygql.formidable.dev/graphql/basic-pokedex`,
10 | {
11 | method: "POST",
12 | headers: {
13 | "Content-Type": "application/json",
14 | },
15 | body: JSON.stringify(req.body),
16 | }
17 | );
18 | res.status(response.status);
19 | if (response.ok) {
20 | const json = await response.json();
21 | res.send(json);
22 | } else {
23 | console.error(await response.text());
24 | res.send(await response.text());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/pages/index.res:
--------------------------------------------------------------------------------
1 | module Query = %relay(`
2 | query pages_Index_Query {
3 | ...PokemonFeed_Fragment
4 | }
5 | `)
6 |
7 | type params
8 |
9 | let getServerSideProps = RelayEnv.SSR.make(async (
10 | ~context as _: Next.GetServerSideProps.context,
11 | ~environment,
12 | ) => {
13 | let _ = await Query.fetchPromised(~environment, ~variables=(), ())
14 | None
15 | })
16 |
17 | let default = () => {
18 | let {fragmentRefs} = Query.use(~variables=(), ())
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/pages/pokemon/[pokemonId].res:
--------------------------------------------------------------------------------
1 | module Query = %relay(`
2 | query PokemonId_Page_Query($pokemonId: ID!) {
3 | pokemon(id: $pokemonId) {
4 | ...PokemonDetail_Fragment
5 | }
6 | }
7 | `)
8 |
9 | type params = {pokemonId: string}
10 |
11 | let getServerSideProps = RelayEnv.SSR.make(async (
12 | ~context: Next.GetServerSideProps.context,
13 | ~environment,
14 | ) => {
15 | let _ = await Query.fetchPromised(
16 | ~environment,
17 | ~variables={
18 | pokemonId: context.params.pokemonId,
19 | },
20 | (),
21 | )
22 | None
23 | })
24 |
25 | let default = () => {
26 | let router = Next.Router.useRouter()
27 | let pokemonId = router.query->Js.Dict.get("pokemonId")->Belt.Option.getExn
28 | let {pokemon} = Query.use(~variables={pokemonId: pokemonId}, ())
29 | Belt.Option.getExn).fragmentRefs} />
30 | }
31 |
--------------------------------------------------------------------------------
/src/relay/RelayEnv.res:
--------------------------------------------------------------------------------
1 | @module("./environment.ts")
2 | external makeEnvironment: unit => RescriptRelay.Environment.t = "makeEnvironment"
3 |
4 | @module("./hydrateRelayStore.ts")
5 | external hydrateRelayStore: (
6 | Next.GetServerSideProps.context<'a>,
7 | RescriptRelay.Environment.t => promise