diff --git a/.gitignore b/.gitignore index cc73063..d7273da 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,9 @@ yarn-error.log* # vercel .vercel + +# graphql generated files +**/*/*.graphql.d.ts +**/*/*.graphqls.d.ts +**/*/*.graphql.tsx +**/*/*.graphqls.tsx \ No newline at end of file diff --git a/.graphql-let.yml b/.graphql-let.yml new file mode 100644 index 0000000..3d7835a --- /dev/null +++ b/.graphql-let.yml @@ -0,0 +1,19 @@ +# https://graphql-code-generator.com/ +# https://github.com/piglovesyou/graphql-let#experimental-feature-resolver-types +# https://the-guild.dev/blog/better-type-safety-for-resolvers-with-graphql-codegen +# https://graphql-code-generator.com/docs/plugins/typescript-resolvers#use-your-model-types-mappers + +schema: 'src/**/*.graphqls' +schemaEntrypoint: 'src/apollo/type-defs.graphqls' +documents: 'src/**/*.graphql' +plugins: + - typescript + - typescript-operations + - typescript-react-apollo + # - typescript-resolvers TODO: not sure why this isn't working--see links above +config: + contextType: .apollo/context.models#ContextModel + mappers: + # Show: ./apollo/shows/shows.models#ShowModel + # Cast: ./apollo/shows/shows.models#CastModel +cacheDir: __generated__ diff --git a/next.config.js b/next.config.js index ee939d4..ac9fa85 100644 --- a/next.config.js +++ b/next.config.js @@ -10,7 +10,7 @@ module.exports = withPlugins( }), ], { - webpack(config) { + webpack(config, options) { config.resolve.alias = { ...config.resolve.alias, // https://blog.usejournal.com/my-awesome-custom-react-environment-variables-setup-8ebb0797d8ac @@ -23,6 +23,24 @@ module.exports = withPlugins( process.env.NODE_ENV === 'production' ? new DuplicatePackageCheckerPlugin() : null, ].filter(Boolean); + config.module.rules.push({ + test: /\.graphql$/, + exclude: /node_modules/, + use: [options.defaultLoaders.babel, { loader: 'graphql-let/loader' }], + }); + + config.module.rules.push({ + test: /\.graphqls$/, + exclude: /node_modules/, + use: ['graphql-let/schema/loader'], + }); + + config.module.rules.push({ + test: /\.ya?ml$/, + type: 'json', + use: 'yaml-loader', + }); + return config; }, } diff --git a/package.json b/package.json index b30c5a3..1f9f994 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ } }, "scripts": { - "dev": "CLIENT_ENV=development next", - "build": "next build", + "codegen": "graphql-let", + "dev": "yarn codegen && CLIENT_ENV=development next", + "build": "yarn codegen && next build", "start": "next start", "---------- Linting ----------------------------------------------------": "", "eslint": "eslint --quiet \"*/**/*.{ts,tsx}\"", @@ -28,14 +29,21 @@ "-----------------------------------------------------------------------": "" }, "dependencies": { + "@apollo/client": "3.3.7", + "@graphql-tools/load-files": "6.2.5", + "@graphql-tools/merge": "6.2.7", + "@graphql-tools/schema": "7.1.2", "@material-ui/core": "4.11.3", + "apollo-datasource-rest": "0.9.7", + "apollo-server-micro": "2.19.2", "axios": "0.21.1", "clsx": "1.1.1", "dayjs": "1.10.4", + "graphql": "15.4.0", + "graphql-let": "0.16.3", + "graphql-tag": "2.11.0", "lodash.groupby": "4.6.0", "lodash.orderby": "4.6.0", - "mobx": "6.1.1", - "mobx-react": "7.1.0", "next": "10.0.5", "notistack": "1.0.3", "nprogress": "0.2.0", @@ -45,6 +53,12 @@ "semantic-ui-react": "2.0.3" }, "devDependencies": { + "@graphql-codegen/cli": "1.20.0", + "@graphql-codegen/plugin-helpers": "1.18.2", + "@graphql-codegen/typescript": "1.20.0", + "@graphql-codegen/typescript-operations": "1.17.13", + "@graphql-codegen/typescript-react-apollo": "2.2.1", + "@graphql-codegen/typescript-resolvers": "1.18.1", "@next/bundle-analyzer": "10.0.5", "@types/lodash.groupby": "4.6.6", "@types/lodash.orderby": "4.6.6", @@ -69,7 +83,8 @@ "prettier": "2.2.1", "pretty-quick": "3.1.0", "sass": "1.32.5", - "typescript": "4.1.3" + "typescript": "4.1.3", + "yaml-loader": "0.6.0" }, "license": "MIT" } diff --git a/src/apollo/apolloClient.ts b/src/apollo/apolloClient.ts new file mode 100644 index 0000000..54d8449 --- /dev/null +++ b/src/apollo/apolloClient.ts @@ -0,0 +1,71 @@ +import { IncomingMessage, ServerResponse } from 'http'; +import { useMemo } from 'react'; +import { ApolloClient, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; +import { toastItemsVar } from '../domains/toasts/toasts.state'; + +let apolloClient: ApolloClient | undefined; + +export type ResolverContext = { + req?: IncomingMessage; + res?: ServerResponse; +}; + +const createIsomorphLink = (context: ResolverContext = {}) => { + if (typeof window === 'undefined') { + const { SchemaLink } = require('@apollo/client/link/schema'); + const { schema } = require('./schema'); + return new SchemaLink({ schema, context }); + } else { + const { HttpLink } = require('@apollo/client'); + return new HttpLink({ + uri: '/api/graphql', + credentials: 'same-origin', + }); + } +}; + +const createApolloClient = (context?: ResolverContext) => { + return new ApolloClient({ + ssrMode: typeof window === 'undefined', + link: createIsomorphLink(context), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + toastItems: { + read() { + return toastItemsVar(); + }, + }, + }, + }, + }, + }), + }); +}; + +export const initializeApollo = ( + initialState: any = null, + // Pages with Next.js data fetching methods, like `getStaticProps`, can send + // a custom context which will be used by `SchemaLink` to server render pages + context?: ResolverContext +) => { + const _apolloClient = apolloClient ?? createApolloClient(context); + + // If your page has Next.js data fetching methods that use Apollo Client, the initial state + // get hydrated here + if (initialState) { + _apolloClient.cache.restore(initialState); + } + // For SSG and SSR always create a new Apollo Client + if (typeof window === 'undefined') return _apolloClient; + // Create the Apollo Client once in the client + if (!apolloClient) apolloClient = _apolloClient; + + return _apolloClient; +}; + +export const useApollo = (initialState: any) => { + const store = useMemo(() => initializeApollo(initialState), [initialState]); + return store; +}; diff --git a/src/apollo/apolloServer.ts b/src/apollo/apolloServer.ts new file mode 100644 index 0000000..6bc2f55 --- /dev/null +++ b/src/apollo/apolloServer.ts @@ -0,0 +1,19 @@ +import { ApolloServer } from 'apollo-server-micro'; +import { ShowsAPI } from './shows/shows.datasource'; +import { AuthenticationAPI } from './auth/auth.datasource'; +import { schema } from './schema'; + +export const apolloServer = new ApolloServer({ + schema, + dataSources: () => { + return { + showsAPI: new ShowsAPI(), + authAPI: new AuthenticationAPI(), + }; + }, + context: () => { + return { + token: 'foo', + }; + }, +}); diff --git a/src/apollo/auth/auth.datasource.ts b/src/apollo/auth/auth.datasource.ts new file mode 100644 index 0000000..0f5ab51 --- /dev/null +++ b/src/apollo/auth/auth.datasource.ts @@ -0,0 +1,19 @@ +import { RESTDataSource } from 'apollo-datasource-rest'; +import { Auth } from 'apollo/type-defs.graphqls'; +import environment from 'environment'; +import { authReducer } from './auth.reducers'; + +export class AuthenticationAPI extends RESTDataSource { + constructor() { + super(); + this.baseURL = environment.api.userBase; + } + + async authenticateUser(showId: string): Promise { + const randomUser = await this.get( + `?inc=gender,name` // path + ); + + return authReducer(randomUser); + } +} diff --git a/src/apollo/auth/auth.graphqls b/src/apollo/auth/auth.graphqls new file mode 100644 index 0000000..ab096cf --- /dev/null +++ b/src/apollo/auth/auth.graphqls @@ -0,0 +1,4 @@ +type Auth { + isAuthenticated: Boolean! + userFullName: String! +} diff --git a/src/apollo/auth/auth.models.ts b/src/apollo/auth/auth.models.ts new file mode 100644 index 0000000..f5710ec --- /dev/null +++ b/src/apollo/auth/auth.models.ts @@ -0,0 +1,22 @@ +export interface AuthModel { + results: UserModel[]; + info: UserResponseInfoModel; +} + +export interface UserModel { + gender: string; + name: UserNameModel; +} + +export interface UserNameModel { + title: string; + first: string; + last: string; +} + +export interface UserResponseInfoModel { + seed: string; + results: number; + page: number; + version: string; +} diff --git a/src/apollo/auth/auth.reducers.ts b/src/apollo/auth/auth.reducers.ts new file mode 100644 index 0000000..709e750 --- /dev/null +++ b/src/apollo/auth/auth.reducers.ts @@ -0,0 +1,9 @@ +import { Auth } from 'apollo/type-defs.graphqls'; +import { AuthModel } from './auth.models'; + +export const authReducer = (user: AuthModel): Auth => { + return { + isAuthenticated: Boolean(user), + userFullName: `${user?.results[0]?.name?.first} ${user?.results[0]?.name?.last}`, + }; +}; diff --git a/src/apollo/context.models.ts b/src/apollo/context.models.ts new file mode 100644 index 0000000..8de5f85 --- /dev/null +++ b/src/apollo/context.models.ts @@ -0,0 +1,9 @@ +import { AuthenticationAPI } from './auth/auth.datasource'; +import { ShowsAPI } from './shows/shows.datasource'; + +export type ContextModel = { + dataSources: { + showsAPI: ShowsAPI; + authAPI: AuthenticationAPI; + }; +}; diff --git a/src/apollo/resolvers.ts b/src/apollo/resolvers.ts new file mode 100644 index 0000000..ee00af2 --- /dev/null +++ b/src/apollo/resolvers.ts @@ -0,0 +1,20 @@ +import { Resolvers } from './type-defs.graphqls'; + +export const resolvers: Resolvers = { + Query: { + cast: async (_parent, _args, _context, _info) => { + return _context.dataSources.showsAPI.getCast(_args.showId); + }, + show: async (_parent, _args, _context, _info) => { + return _context.dataSources.showsAPI.getShowDetails(_args.showId); + }, + episodes: async (_parent, _args, _context, _info) => { + return _context.dataSources.showsAPI.getEpisodes(_args.showId); + }, + }, + Mutation: { + signIn: async (_parent, _args, _context, _info) => { + return _context.dataSources.authAPI.authenticateUser(); + }, + }, +}; diff --git a/src/apollo/schema.ts b/src/apollo/schema.ts new file mode 100644 index 0000000..27f7bd0 --- /dev/null +++ b/src/apollo/schema.ts @@ -0,0 +1,14 @@ +import { join } from 'path'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { loadFilesSync } from '@graphql-tools/load-files'; +import { mergeTypeDefs } from '@graphql-tools/merge'; +import graphQLLetConfig from '../../.graphql-let.yml'; +import { resolvers } from './resolvers'; + +const loadedFiles = loadFilesSync(join(process.cwd(), graphQLLetConfig.schema)); +const typeDefs = mergeTypeDefs(loadedFiles); + +export const schema = makeExecutableSchema({ + typeDefs, + resolvers, +}); diff --git a/src/apollo/shows/shows.datasource.ts b/src/apollo/shows/shows.datasource.ts new file mode 100644 index 0000000..6a248ae --- /dev/null +++ b/src/apollo/shows/shows.datasource.ts @@ -0,0 +1,35 @@ +import { RESTDataSource } from 'apollo-datasource-rest'; +import environment from 'environment'; +import { Cast, Episode, Show } from 'lib/type-defs.graphqls'; +import { castReducer, episodeReducer, showReducer } from './shows.reducers'; + +export class ShowsAPI extends RESTDataSource { + constructor() { + super(); + this.baseURL = environment.api.showsBase; + } + + async getShowDetails(showId: string): Promise { + const show = await this.get( + `${showId}` // path + ); + + return showReducer(show); + } + + async getCast(showId: string): Promise { + const cast = await this.get( + `${showId}/cast` // path + ); + + return castReducer(cast); + } + + async getEpisodes(showId: string): Promise { + const episodes = await this.get( + `${showId}/episodes` // path + ); + + return episodeReducer(episodes); + } +} diff --git a/src/apollo/shows/shows.graphqls b/src/apollo/shows/shows.graphqls new file mode 100644 index 0000000..779a52d --- /dev/null +++ b/src/apollo/shows/shows.graphqls @@ -0,0 +1,67 @@ +type Image { + medium: String + original: String +} + +type Episode { + id: Int! + season: Int! + number: Int! + name: String! + airdate: String! + image: Image! + summary: String! +} + +type EpisodeTableRow { + episode: Int + name: String + date: String + image: String +} + +type EpisodeTable { + title: String + rows: [EpisodeTableRow] +} + +type Person { + id: Int! + name: String! + birthday: String + image: Image +} + +type Character { + id: Int + name: String! + image: Image +} + +type Cast { + person: Person! + character: Character! + self: Boolean! + voice: Boolean! +} + +type Show { + id: Int! + name: String! + summary: String! + genres: [String!] + network: Network! + image: String! +} + +type Network { + id: Int + name: String + country: Country +} + +type Country { + name: String + code: String + timezone: String +} diff --git a/src/apollo/shows/shows.models.ts b/src/apollo/shows/shows.models.ts new file mode 100644 index 0000000..842e282 --- /dev/null +++ b/src/apollo/shows/shows.models.ts @@ -0,0 +1,70 @@ +export type CountryModel = { + name: string; + code: string; + timezone: string; +}; + +export type ImageModel = { + medium: string; + original: string; +}; + +export type NetworkModel = { + id: number; + name: string; + country: CountryModel; +}; + +export type ShowModel = { + id: number; + url: string; + name: string; + type: string; + language: string; + summary: string; + genres: string[]; + network: NetworkModel; + image: ImageModel; +}; + +export type PersonModel = { + id: number; + name: string; + birthday: string; + image: ImageModel; +}; + +export type CharacterModel = { + id: number; + name: string; + image: ImageModel; +}; + +export type CastModel = { + person: PersonModel; + character: CharacterModel; + self: boolean; + voice: boolean; +}; + +export type EpisodeModel = { + id: number; + season: number; + number: number; + name: string; + airdate: string; + image: ImageModel; + summary: string; +}; + +export type EpisodeTableRowModel = { + episode: number; + name: string; + date: string; + image: string; +}; + +export type EpisodeTableModel = { + title: string; + rows: EpisodeTableRowModel[]; +}; diff --git a/src/apollo/shows/shows.reducers.ts b/src/apollo/shows/shows.reducers.ts new file mode 100644 index 0000000..3ea9390 --- /dev/null +++ b/src/apollo/shows/shows.reducers.ts @@ -0,0 +1,59 @@ +import { Cast, Episode, Show } from 'apollo/type-defs.graphqls'; +import { CastModel, EpisodeModel, ShowModel } from './shows.models'; + +export const showReducer = (show: ShowModel): Show => { + return { + id: show?.id ?? 0, + name: show?.name ?? '', + summary: show?.summary ?? '', + genres: show?.genres ?? [], + image: show?.image?.medium ?? '', + network: { + id: show?.network?.id ?? null, + name: show?.network?.name ?? '', + country: { + name: show?.network?.country?.name ?? '', + code: show?.network?.country?.code ?? '', + timezone: show?.network?.country?.timezone ?? '', + }, + }, + }; +}; + +export const castReducer = (cast: CastModel[]): Cast[] => { + return cast.map((castMember) => ({ + self: castMember.self, + voice: castMember.voice, + person: { + name: castMember.person.name, + id: castMember.person.id, + birthday: castMember?.person?.birthday ?? '', + image: { + medium: castMember.person.image.medium, + original: castMember?.person?.image?.original, + }, + }, + character: { + id: castMember.character.id, + name: castMember.character.name, + image: { + original: castMember.character?.image?.original, + medium: castMember?.character?.image?.medium, + }, + }, + })); +}; + +export const episodeReducer = (episodes: EpisodeModel[]): Episode[] => { + return episodes.map((episode) => ({ + id: episode.id, + season: episode.season, + name: episode.name, + number: episode.number, + airdate: episode.airdate, + image: { + medium: episode.image.medium ?? '', + }, + summary: episode.summary, + })); +}; diff --git a/src/apollo/type-defs.graphqls b/src/apollo/type-defs.graphqls new file mode 100644 index 0000000..99e36bf --- /dev/null +++ b/src/apollo/type-defs.graphqls @@ -0,0 +1,11 @@ +type Query { + auth: Auth! + toastItems: [ToastNotification] + cast(showId: String!): [Cast!] + show(showId: String!): Show + episodes(showId: String!): [Episode] +} + +type Mutation { + signIn(name: String): Auth! +} diff --git a/src/components/pages/about-page/AboutPage.store.ts b/src/components/pages/about-page/AboutPage.store.ts deleted file mode 100644 index 9729f4b..0000000 --- a/src/components/pages/about-page/AboutPage.store.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { makeAutoObservable } from 'mobx'; -import { getErrorRequest } from '../../../domains/shows/shows.services'; -import { initialResponseStatus } from '../../../utils/mobx.utils'; -import { ApiResponse } from '../../../utils/http/http.types'; -import { getGlobalStore } from '../../shared/global-store-provider/GlobalStoreProvider'; - -export class AboutPageStore { - globalStore = getGlobalStore(); - errorExampleResults = initialResponseStatus(null); - - constructor() { - makeAutoObservable(this); - } - - /** - * Store initializer. Should only be called once. - */ - *init() { - yield Promise.all([this.loadSomething()]); - } - - *loadSomething() { - const response: ApiResponse = yield getErrorRequest(); - - this.errorExampleResults = { - data: this.errorExampleResults.data, - isRequesting: false, - ...response, // Overwrites the default data prop or adds an error. Also adds the statusCode. - }; - - if (response.error) { - const message = `${response.statusCode}: ${response.error.message}`; - - this.globalStore.toastStore.enqueueToast(message, 'error'); - } - } -} diff --git a/src/components/pages/about-page/AboutPage.tsx b/src/components/pages/about-page/AboutPage.tsx index e923db3..fea2e82 100644 --- a/src/components/pages/about-page/AboutPage.tsx +++ b/src/components/pages/about-page/AboutPage.tsx @@ -1,31 +1,38 @@ import React from 'react'; import { LoadingIndicator } from '../../ui/loading-indicator/LoadingIndicator'; import { Container, Header, Message } from 'semantic-ui-react'; -import { AboutPageStore } from './AboutPage.store'; -import { observer } from 'mobx-react-lite'; -import { useLocalStore } from '../../shared/local-store-provider/LocalStoreProvider'; +import { useGetEpisodesByShowIdQuery } from 'domains/shows/getEpisodesByShowId.graphql'; +import { toastErrorMessage } from 'domains/toasts/toasts.utils'; interface IProps {} -export const AboutPage: React.FC = observer((props) => { - const localStore = useLocalStore(); +export const AboutPage: React.FC = (props) => { + const { data, error, loading } = useGetEpisodesByShowIdQuery({ + variables: { + halp: 'me -- I caused an error', + }, + }); + + if (error) { + toastErrorMessage(error?.message); + } return (
About
- +

This page is only to show how to handle API errors on the page.

You will also notice a popup indicator with the actual error text.

Below we create a custom error message.

- {Boolean(localStore.errorExampleResults.error) && ( + {Boolean(error) && ( )}
); -}); +}; AboutPage.displayName = 'AboutPage'; AboutPage.defaultProps = {}; diff --git a/src/components/pages/episodes-page/EpisodesPage.state.ts b/src/components/pages/episodes-page/EpisodesPage.state.ts new file mode 100644 index 0000000..9cc524b --- /dev/null +++ b/src/components/pages/episodes-page/EpisodesPage.state.ts @@ -0,0 +1,4 @@ +import { makeVar } from '@apollo/client'; +import { SortDirection } from 'constants/common.types'; + +export const episodesSortByVar = makeVar(SortDirection.ASCENDING); diff --git a/src/components/pages/episodes-page/EpisodesPage.store.ts b/src/components/pages/episodes-page/EpisodesPage.store.ts deleted file mode 100644 index 58c2cca..0000000 --- a/src/components/pages/episodes-page/EpisodesPage.store.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { observable } from 'mobx'; -import groupBy from 'lodash.groupby'; -import orderBy from 'lodash.orderby'; -import { IEpisode, IEpisodeTable } from '../../../domains/shows/shows.types'; -import dayjs from 'dayjs'; -import { ApiResponse } from '../../../utils/http/http.types'; -import { getGlobalStore } from '../../shared/global-store-provider/GlobalStoreProvider'; -import { EpisodesToggleOption } from './episodes-toggle/EpisodesToggle.constants'; - -export const EpisodesPageStore = (episodesResults: ApiResponse) => - observable({ - globalStore: getGlobalStore(), - sortType: EpisodesToggleOption.ASC, - episodesResults: episodesResults, - - get sortedTableData(): IEpisodeTable[] { - return orderBy(this.generateTableData, 'title', this.sortType); - }, - - get generateTableData(): IEpisodeTable[] { - if (this.episodesResults.error) { - return []; - } - - const seasons: { [season: string]: IEpisode[] } = groupBy(this.episodesResults.data, 'season'); - - return Object.entries(seasons).map(([season, models]) => { - return { - title: `Season ${season}`, - rows: models.map((model) => ({ - episode: model.number, - name: model.name, - date: dayjs(model.airdate).format('MMM D, YYYY'), - image: model.image?.medium ?? '', - })), - }; - }); - }, - - setSortType(sortType: EpisodesToggleOption): void { - this.sortType = sortType; - - this.globalStore.toastStore.enqueueToast('Nice! You just sorted Server-Side Rendered Content.', 'info'); - }, - }); - -export type EpisodesPageStore = ReturnType; diff --git a/src/components/pages/episodes-page/EpisodesPage.tsx b/src/components/pages/episodes-page/EpisodesPage.tsx index d18ecf4..70b213a 100644 --- a/src/components/pages/episodes-page/EpisodesPage.tsx +++ b/src/components/pages/episodes-page/EpisodesPage.tsx @@ -1,27 +1,37 @@ -import React from 'react'; -import { EpisodesPageStore } from './EpisodesPage.store'; +import React, { useMemo } from 'react'; import { EpisodesTable } from './episodes-table/EpisodesTable'; -import { useLocalStore } from '../../shared/local-store-provider/LocalStoreProvider'; -import { observer } from 'mobx-react-lite'; import { Container } from 'semantic-ui-react'; import { EpisodesToggle } from './episodes-toggle/EpisodesToggle'; +import { Episode, useGetEpisodesByShowIdQuery } from 'domains/shows/getEpisodesByShowId.graphql'; +import { generateAndSortTableData } from './EpisodesPage.utils'; +import { episodesSortByVar } from './EpisodesPage.state'; +import { useReactiveVar } from '@apollo/client'; -export interface IProps {} +export interface IProps { + episodeId: string; +} -export const EpisodesPage: React.FC = observer((props) => { - const localStore = useLocalStore(); +export const EpisodesPage: React.FC = (props) => { + const { data, error, loading } = useGetEpisodesByShowIdQuery({ variables: { showId: props.episodeId } }); + + const sortDirection = useReactiveVar(episodesSortByVar); + + const tableData = useMemo( + () => generateAndSortTableData(data?.episodes as Episode[], sortDirection, error?.message), + [loading, sortDirection] + ); return ( <> - {localStore.sortedTableData.map((model) => ( + {tableData.map((model) => ( ))} ); -}); +}; EpisodesPage.displayName = 'EpisodesPage'; EpisodesPage.defaultProps = {}; diff --git a/src/components/pages/episodes-page/EpisodesPage.utils.ts b/src/components/pages/episodes-page/EpisodesPage.utils.ts new file mode 100644 index 0000000..b3d14fd --- /dev/null +++ b/src/components/pages/episodes-page/EpisodesPage.utils.ts @@ -0,0 +1,30 @@ +import { toastErrorMessage } from 'domains/toasts/toasts.utils'; +import dayjs from 'dayjs'; +import groupBy from 'lodash.groupby'; +import { Episode } from '../../../domains/shows/getEpisodesByShowId.graphql'; +import { SortDirection } from 'constants/common.types'; +import orderBy from 'lodash.orderby'; + +export const generateTableData = (episodes: Episode[] = [], error?: string) => { + if (error) { + toastErrorMessage(error); + return []; + } + + const seasons: { [season: string]: Episode[] } = groupBy(episodes, 'season'); + + return Object.entries(seasons).map(([season, models]) => { + return { + title: `Season ${season}`, + rows: models.map((model) => ({ + episode: model.number, + name: model.name, + date: dayjs(model.airdate).format('MMM D, YYYY'), + image: model.image?.medium ?? '', + })), + }; + }); +}; + +export const generateAndSortTableData = (episodes: Episode[], sortType: SortDirection, error?: string) => + orderBy(generateTableData(episodes, error), 'title', sortType); diff --git a/src/components/pages/episodes-page/episodes-toggle/EpisodesToggle.tsx b/src/components/pages/episodes-page/episodes-toggle/EpisodesToggle.tsx index 893157b..3616dd3 100644 --- a/src/components/pages/episodes-page/episodes-toggle/EpisodesToggle.tsx +++ b/src/components/pages/episodes-page/episodes-toggle/EpisodesToggle.tsx @@ -1,33 +1,39 @@ import React from 'react'; import { Button } from 'semantic-ui-react'; -import { useLocalStore } from '../../../shared/local-store-provider/LocalStoreProvider'; -import { EpisodesPageStore } from '../EpisodesPage.store'; -import { EpisodesToggleOption } from './EpisodesToggle.constants'; -import { observer } from 'mobx-react-lite'; +import { episodesSortByVar } from '../EpisodesPage.state'; +import { useReactiveVar } from '@apollo/client'; +import { SortDirection } from 'constants/common.types'; +import { toastSuccessMessage } from 'domains/toasts/toasts.utils'; export interface IProps {} -export const EpisodesToggle: React.FC = observer((props) => { - const localStore = useLocalStore(); +export const EpisodesToggle: React.FC = (props) => { + const sortBy = useReactiveVar(episodesSortByVar); return ( ); -}); +}; EpisodesToggle.displayName = 'EpisodesToggle'; EpisodesToggle.defaultProps = {}; diff --git a/src/components/pages/index-page/IndexPage.state.ts b/src/components/pages/index-page/IndexPage.state.ts new file mode 100644 index 0000000..c19f627 --- /dev/null +++ b/src/components/pages/index-page/IndexPage.state.ts @@ -0,0 +1,4 @@ +import { makeVar } from '@apollo/client'; +import { ActorSortBy } from './actors/actors-sort-option/ActorsSortOption.constants'; + +export const actorSortByVar = makeVar(ActorSortBy.NAME); diff --git a/src/components/pages/index-page/IndexPage.store.ts b/src/components/pages/index-page/IndexPage.store.ts deleted file mode 100644 index 360261e..0000000 --- a/src/components/pages/index-page/IndexPage.store.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { observable } from 'mobx'; -import { getCastsRequest, getShowRequest } from '../../../domains/shows/shows.services'; -import { initialResponseStatus } from '../../../utils/mobx.utils'; -import { ICast, IShow } from '../../../domains/shows/shows.types'; -import { ApiResponse } from '../../../utils/http/http.types'; -import { defaultShowId } from '../../../domains/shows/shows.constants'; -import orderBy from 'lodash.orderby'; - -export const IndexPageStore = () => - observable({ - // globalStore: getGlobalStore(), - sortValue: '', - showResults: initialResponseStatus(null), - castsResults: initialResponseStatus([]), - - get isRequesting(): boolean { - return [this.showResults.isRequesting, this.castsResults.isRequesting].some(Boolean); - }, - - get actors(): ICast[] { - return orderBy(this.castsResults.data, (cast) => cast.person[this.sortValue], 'asc'); - }, - - setSortOption(sortValue: string): void { - this.sortValue = sortValue; - }, - - /** - * Store initializer. Should only be called once. - */ - *init() { - yield Promise.all([this.loadShow(), this.loadCasts()]); - }, - - *loadShow() { - const response: ApiResponse = yield getShowRequest(defaultShowId); - - this.showResults = { - data: this.showResults.data, - isRequesting: false, - ...response, // Overwrites the default data prop or adds an error. Also adds the statusCode. - }; - }, - - *loadCasts() { - const response: ApiResponse = yield getCastsRequest(defaultShowId); - - this.castsResults = { - data: this.castsResults.data, - isRequesting: false, - ...response, // Overwrites the default data prop or adds an error. Also adds the statusCode. - }; - }, - }); - -export type IndexPageStore = ReturnType; diff --git a/src/components/pages/index-page/IndexPage.tsx b/src/components/pages/index-page/IndexPage.tsx index 791629c..dc2ffde 100644 --- a/src/components/pages/index-page/IndexPage.tsx +++ b/src/components/pages/index-page/IndexPage.tsx @@ -3,17 +3,20 @@ import { Divider, Header, Icon } from 'semantic-ui-react'; import { LoadingIndicator } from '../../ui/loading-indicator/LoadingIndicator'; import { MainOverview } from './main-overview/MainOverview'; import { Actors } from './actors/Actors'; -import { observer } from 'mobx-react-lite'; -import { IndexPageStore } from './IndexPage.store'; -import { useLocalStore } from '../../shared/local-store-provider/LocalStoreProvider'; +import { defaultShowId } from 'domains/shows/shows.constants'; +import { useGetShowDetailsAndCastByShowIdQuery } from 'domains/shows/getShowDetailsAndCastByShowId.graphql'; interface IProps {} -export const IndexPage: React.FC = observer((props) => { - const localStore = useLocalStore(); +export const IndexPage: React.FC = (props) => { + const { loading } = useGetShowDetailsAndCastByShowIdQuery({ + variables: { + showId: defaultShowId, + }, + }); return ( - +
@@ -23,7 +26,7 @@ export const IndexPage: React.FC = observer((props) => { ); -}); +}; IndexPage.displayName = 'IndexPage'; IndexPage.defaultProps = {}; diff --git a/src/components/pages/index-page/actors/Actors.tsx b/src/components/pages/index-page/actors/Actors.tsx index 9c23e77..8d06f57 100644 --- a/src/components/pages/index-page/actors/Actors.tsx +++ b/src/components/pages/index-page/actors/Actors.tsx @@ -1,15 +1,28 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Card } from 'semantic-ui-react'; -import { observer } from 'mobx-react-lite'; -import { IndexPageStore } from '../IndexPage.store'; import { ActorCard } from './actor-card/ActorCard'; import { ActorsSortOption } from './actors-sort-option/ActorsSortOption'; -import { useLocalStore } from '../../../shared/local-store-provider/LocalStoreProvider'; +import { defaultShowId } from 'domains/shows/shows.constants'; +import { actorSortByVar } from '../IndexPage.state'; +import orderBy from 'lodash.orderby'; +import { useReactiveVar } from '@apollo/client'; +import { useGetShowDetailsAndCastByShowIdQuery } from 'domains/shows/getShowDetailsAndCastByShowId.graphql'; interface IProps {} -export const Actors: React.FC = observer((props) => { - const localStore = useLocalStore(); +export const Actors: React.FC = (props) => { + const actorSortBy = useReactiveVar(actorSortByVar); + + const { data } = useGetShowDetailsAndCastByShowIdQuery({ + variables: { + showId: defaultShowId, + }, + }); + + const cast = useMemo(() => orderBy(data?.cast ?? [], (castMember) => castMember.person[actorSortBy], 'asc'), [ + actorSortBy, + data?.cast, + ]); return ( <> @@ -17,13 +30,13 @@ export const Actors: React.FC = observer((props) => { - {localStore.actors.map((model) => ( - + {cast.map((model) => ( + ))} ); -}); +}; Actors.displayName = 'Actors'; Actors.defaultProps = {}; diff --git a/src/components/pages/index-page/actors/actor-card/ActorCard.tsx b/src/components/pages/index-page/actors/actor-card/ActorCard.tsx index 0eb1fce..57c924c 100644 --- a/src/components/pages/index-page/actors/actor-card/ActorCard.tsx +++ b/src/components/pages/index-page/actors/actor-card/ActorCard.tsx @@ -1,29 +1,28 @@ import React from 'react'; import { Card, Image } from 'semantic-ui-react'; -import { observer } from 'mobx-react-lite'; -import { ICast } from '../../../../../domains/shows/shows.types'; +import { Cast } from 'domains/shows/shows.graphql'; interface IProps { - readonly cardData: ICast; + readonly cardData: Cast; } -export const ActorCard: React.FC = observer((props) => { - const image: string = props.cardData?.character?.image?.medium; +export const ActorCard: React.FC = (props) => { + const image: string = props.cardData?.character?.image?.medium ?? ''; const missingImage = 'https://react.semantic-ui.com/images/wireframe/image.png'; return ( - + - {props.cardData.person.name} - as {props.cardData.character.name} + {props.cardData.person?.name} + as {props.cardData.character?.name} - Birth date: {props.cardData.person.birthday} + Birth date: {props.cardData.person?.birthday} ); -}); +}; ActorCard.displayName = 'ActorCard'; ActorCard.defaultProps = {}; diff --git a/src/components/pages/index-page/actors/actors-sort-option/ActorsSortOption.constants.ts b/src/components/pages/index-page/actors/actors-sort-option/ActorsSortOption.constants.ts index 72d556f..88bd049 100644 --- a/src/components/pages/index-page/actors/actors-sort-option/ActorsSortOption.constants.ts +++ b/src/components/pages/index-page/actors/actors-sort-option/ActorsSortOption.constants.ts @@ -1,4 +1,9 @@ +export enum ActorSortBy { + NAME = 'name', + BIRTHDAY = 'birthday', +} + export const actorSortOptions = [ - { value: 'name', text: 'Actor Name' }, - { value: 'birthday', text: 'Birth Date' }, + { value: ActorSortBy.NAME, text: 'Actor Name' }, + { value: ActorSortBy.BIRTHDAY, text: 'Birth Date' }, ]; diff --git a/src/components/pages/index-page/actors/actors-sort-option/ActorsSortOption.tsx b/src/components/pages/index-page/actors/actors-sort-option/ActorsSortOption.tsx index 991e298..7848943 100644 --- a/src/components/pages/index-page/actors/actors-sort-option/ActorsSortOption.tsx +++ b/src/components/pages/index-page/actors/actors-sort-option/ActorsSortOption.tsx @@ -1,18 +1,15 @@ import React from 'react'; import { Select } from 'semantic-ui-react'; -import { actorSortOptions } from './ActorsSortOption.constants'; -import { IndexPageStore } from '../../IndexPage.store'; -import { useLocalStore } from '../../../../shared/local-store-provider/LocalStoreProvider'; +import { ActorSortBy, actorSortOptions } from './ActorsSortOption.constants'; +import { actorSortByVar } from '../../IndexPage.state'; interface IProps {} export const ActorsSortOption: React.FC = (props) => { - const localStore = useLocalStore(); - return (