├── .editorconfig ├── .eslintrc.json ├── .github ├── actions │ └── aws-s3-sync │ │ └── action.yml └── workflows │ ├── cron.yml │ ├── master.yml │ └── pull_request.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── README.md ├── _resources └── img │ ├── header.png │ └── logo-dark-vertical-1.png ├── index.css ├── index.html ├── lib └── rick-and-morty-api-client │ ├── .openapi-generator-ignore │ ├── .openapi-generator │ ├── FILES │ └── VERSION │ ├── apis │ ├── DefaultApi.ts │ └── index.ts │ ├── index.ts │ ├── models │ ├── Character.ts │ ├── CharacterListResponse.ts │ ├── CharacterListResponseInfo.ts │ ├── CharacterLocation.ts │ ├── CharacterOrigin.ts │ ├── FetchAllCharacters200Response.ts │ ├── FetchAllCharacters200ResponseInfo.ts │ └── index.ts │ └── runtime.ts ├── package-lock.json ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── src ├── App.tsx ├── context │ └── ConfigContext.tsx ├── loaders │ └── CharacterLoader.ts ├── main.tsx ├── pages │ ├── CharacterDetails.tsx │ └── Characters.tsx ├── types │ ├── types.ts │ └── vite-env.d.ts └── utils │ ├── CharactersProcessor.test.ts │ └── CharactersProcessor.ts ├── tailwind.config.js ├── tests └── e2e │ ├── pages │ ├── CharacterDetailsPage.ts │ └── CharactersPage.ts │ ├── tests │ └── details.spec.ts │ └── utils │ └── constants.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "browser": true, "es2021": true }, 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:react/recommended", 6 | "plugin:react-hooks/recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "prettier" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": {}, 14 | "settings": { "react": { "version": "detect" } } 15 | } 16 | -------------------------------------------------------------------------------- /.github/actions/aws-s3-sync/action.yml: -------------------------------------------------------------------------------- 1 | name: Sync directory with S3 bucket 2 | description: Composite action to configure AWS and deploy app bundle to S3 3 | 4 | inputs: 5 | bucket-name: 6 | description: 'S3 bucket name' 7 | required: true 8 | bundle-dir: 9 | description: 'Bundle directory' 10 | required: false 11 | default: 'dist' 12 | 13 | runs: 14 | using: 'composite' 15 | steps: 16 | - name: Configure AWS Credentials 17 | uses: aws-actions/configure-aws-credentials@v4.0.2 18 | with: 19 | aws-region: 'eu-central-1' 20 | 21 | - name: Sync S3 bucket 22 | run: aws s3 sync ${{ inputs.bundle-dir }} s3://${{ inputs.bucket-name }} 23 | shell: bash 24 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Weekly Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version-file: '.nvmrc' 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Run unit tests 25 | run: npm run test 26 | 27 | - name: Run e2e tests 28 | run: npm run test:e2e 29 | env: 30 | E2E_BASE_URL: 'https://frontend-bootstrap.vercel.app/' 31 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: Master Workflow 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version-file: '.nvmrc' 19 | 20 | - name: Install deps 21 | run: npm ci 22 | 23 | - name: Lint code 24 | run: npm run lint 25 | 26 | - name: Run unit tests 27 | run: npm run test 28 | 29 | - name: Build project 30 | run: npm run build 31 | 32 | # - name: Deploy to S3 33 | # uses: ./.github/actions/aws-s3-sync 34 | # with: 35 | # bucket-name: '[BUCKET_NAME]' 36 | # bundle-dir: '[BUNDLE_DIRECTORY]' 37 | # env: 38 | # AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 39 | # AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 40 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Workflow 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | verify-pr: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version-file: '.nvmrc' 17 | 18 | - name: Install deps 19 | run: npm ci 20 | 21 | - name: Lint code 22 | run: npm run lint 23 | 24 | - name: Run unit tests 25 | run: npm run test 26 | 27 | - name: Build project 28 | run: npm run build 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | out 5 | /test-results/ 6 | /playwright-report/ 7 | /blob-report/ 8 | /playwright/.cache/ 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.9.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 100, 5 | "singleAttributePerLine": false 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Opanuj Frontend: AI Edition - Frontend Bootstrap 2 | 3 | ![](./_resources/img/header.png) 4 | 5 | ## Co w środku? 6 | 7 | Bootstrap projektu opartego o następujące narzędzia: 8 | 9 | - [editorconfig](https://editorconfig.org/) 10 | - [eslint](https://eslint.org/) 11 | - [Prettier](https://prettier.io/) 12 | - [React](https://www.npmjs.com/package/@vitejs/plugin-react) (wymień na [Svelte](https://www.npmjs.com/package/@sveltejs/vite-plugin-svelte) lub [Vue](https://www.npmjs.com/package/@vitejs/plugin-vue)) 13 | - [Playwright](https://playwright.dev) 14 | - [Tailwind](https://tailwindui.com/) 15 | - [TypeScript](https://www.typescriptlang.org/) 16 | - [Vite](https://vite.dev/) 17 | - [Vitest](https://vitest.dev/) 18 | 19 | ## Pierwsze kroki 20 | 21 | ```bash 22 | nvm use 23 | npm install 24 | npm run dev 25 | ``` 26 | -------------------------------------------------------------------------------- /_resources/img/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przeprogramowani/frontend-bootstrap/2eea5fd6ee6a3844c0d061eef1a8973b24681214/_resources/img/header.png -------------------------------------------------------------------------------- /_resources/img/logo-dark-vertical-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przeprogramowani/frontend-bootstrap/2eea5fd6ee6a3844c0d061eef1a8973b24681214/_resources/img/logo-dark-vertical-1.png -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello! 7 | 8 | 9 | 10 |
11 | Logo projektu Opanuj Frontend 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | apis/DefaultApi.ts 2 | apis/index.ts 3 | index.ts 4 | models/Character.ts 5 | models/CharacterLocation.ts 6 | models/CharacterOrigin.ts 7 | models/FetchAllCharacters200Response.ts 8 | models/FetchAllCharacters200ResponseInfo.ts 9 | models/index.ts 10 | runtime.ts 11 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 7.5.0-SNAPSHOT 2 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/apis/DefaultApi.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Rick and Morty API 5 | * API for fetching character information from Rick and Morty series 6 | * 7 | * The version of the OpenAPI document: 1.0.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | 16 | import * as runtime from '../runtime'; 17 | import type { 18 | Character, 19 | FetchAllCharacters200Response, 20 | } from '../models/index'; 21 | import { 22 | CharacterFromJSON, 23 | CharacterToJSON, 24 | FetchAllCharacters200ResponseFromJSON, 25 | FetchAllCharacters200ResponseToJSON, 26 | } from '../models/index'; 27 | 28 | export interface FetchSingleCharacterRequest { 29 | id: number; 30 | } 31 | 32 | /** 33 | * 34 | */ 35 | export class DefaultApi extends runtime.BaseAPI { 36 | 37 | /** 38 | * Fetch all characters 39 | */ 40 | async fetchAllCharactersRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { 41 | const queryParameters: any = {}; 42 | 43 | const headerParameters: runtime.HTTPHeaders = {}; 44 | 45 | if (this.configuration && this.configuration.apiKey) { 46 | headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication 47 | } 48 | 49 | const response = await this.request({ 50 | path: `/character`, 51 | method: 'GET', 52 | headers: headerParameters, 53 | query: queryParameters, 54 | }, initOverrides); 55 | 56 | return new runtime.JSONApiResponse(response, (jsonValue) => FetchAllCharacters200ResponseFromJSON(jsonValue)); 57 | } 58 | 59 | /** 60 | * Fetch all characters 61 | */ 62 | async fetchAllCharacters(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { 63 | const response = await this.fetchAllCharactersRaw(initOverrides); 64 | return await response.value(); 65 | } 66 | 67 | /** 68 | * Fetch a single character by ID 69 | */ 70 | async fetchSingleCharacterRaw(requestParameters: FetchSingleCharacterRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { 71 | if (requestParameters['id'] == null) { 72 | throw new runtime.RequiredError( 73 | 'id', 74 | 'Required parameter "id" was null or undefined when calling fetchSingleCharacter().' 75 | ); 76 | } 77 | 78 | const queryParameters: any = {}; 79 | 80 | const headerParameters: runtime.HTTPHeaders = {}; 81 | 82 | if (this.configuration && this.configuration.apiKey) { 83 | headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication 84 | } 85 | 86 | const response = await this.request({ 87 | path: `/character/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), 88 | method: 'GET', 89 | headers: headerParameters, 90 | query: queryParameters, 91 | }, initOverrides); 92 | 93 | return new runtime.JSONApiResponse(response, (jsonValue) => CharacterFromJSON(jsonValue)); 94 | } 95 | 96 | /** 97 | * Fetch a single character by ID 98 | */ 99 | async fetchSingleCharacter(requestParameters: FetchSingleCharacterRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { 100 | const response = await this.fetchSingleCharacterRaw(requestParameters, initOverrides); 101 | return await response.value(); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/apis/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export * from './DefaultApi'; 4 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export * from './runtime'; 4 | export * from './apis/index'; 5 | export * from './models/index'; 6 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/models/Character.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Rick and Morty API 5 | * API for fetching character information from Rick and Morty series 6 | * 7 | * The version of the OpenAPI document: 1.0.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { mapValues } from '../runtime'; 16 | import type { CharacterLocation } from './CharacterLocation'; 17 | import { 18 | CharacterLocationFromJSON, 19 | CharacterLocationFromJSONTyped, 20 | CharacterLocationToJSON, 21 | } from './CharacterLocation'; 22 | import type { CharacterOrigin } from './CharacterOrigin'; 23 | import { 24 | CharacterOriginFromJSON, 25 | CharacterOriginFromJSONTyped, 26 | CharacterOriginToJSON, 27 | } from './CharacterOrigin'; 28 | 29 | /** 30 | * 31 | * @export 32 | * @interface Character 33 | */ 34 | export interface Character { 35 | /** 36 | * 37 | * @type {number} 38 | * @memberof Character 39 | */ 40 | id?: number; 41 | /** 42 | * 43 | * @type {string} 44 | * @memberof Character 45 | */ 46 | name?: string; 47 | /** 48 | * 49 | * @type {string} 50 | * @memberof Character 51 | */ 52 | status?: string; 53 | /** 54 | * 55 | * @type {string} 56 | * @memberof Character 57 | */ 58 | species?: string; 59 | /** 60 | * 61 | * @type {string} 62 | * @memberof Character 63 | */ 64 | type?: string; 65 | /** 66 | * 67 | * @type {string} 68 | * @memberof Character 69 | */ 70 | gender?: string; 71 | /** 72 | * 73 | * @type {CharacterOrigin} 74 | * @memberof Character 75 | */ 76 | origin?: CharacterOrigin; 77 | /** 78 | * 79 | * @type {CharacterLocation} 80 | * @memberof Character 81 | */ 82 | location?: CharacterLocation; 83 | /** 84 | * 85 | * @type {string} 86 | * @memberof Character 87 | */ 88 | image?: string; 89 | /** 90 | * 91 | * @type {Array} 92 | * @memberof Character 93 | */ 94 | episode?: Array; 95 | /** 96 | * 97 | * @type {string} 98 | * @memberof Character 99 | */ 100 | url?: string; 101 | /** 102 | * 103 | * @type {Date} 104 | * @memberof Character 105 | */ 106 | created?: Date; 107 | } 108 | 109 | /** 110 | * Check if a given object implements the Character interface. 111 | */ 112 | export function instanceOfCharacter(value: object): boolean { 113 | return true; 114 | } 115 | 116 | export function CharacterFromJSON(json: any): Character { 117 | return CharacterFromJSONTyped(json, false); 118 | } 119 | 120 | export function CharacterFromJSONTyped(json: any, ignoreDiscriminator: boolean): Character { 121 | if (json == null) { 122 | return json; 123 | } 124 | return { 125 | 126 | 'id': json['id'] == null ? undefined : json['id'], 127 | 'name': json['name'] == null ? undefined : json['name'], 128 | 'status': json['status'] == null ? undefined : json['status'], 129 | 'species': json['species'] == null ? undefined : json['species'], 130 | 'type': json['type'] == null ? undefined : json['type'], 131 | 'gender': json['gender'] == null ? undefined : json['gender'], 132 | 'origin': json['origin'] == null ? undefined : CharacterOriginFromJSON(json['origin']), 133 | 'location': json['location'] == null ? undefined : CharacterLocationFromJSON(json['location']), 134 | 'image': json['image'] == null ? undefined : json['image'], 135 | 'episode': json['episode'] == null ? undefined : json['episode'], 136 | 'url': json['url'] == null ? undefined : json['url'], 137 | 'created': json['created'] == null ? undefined : (new Date(json['created'])), 138 | }; 139 | } 140 | 141 | export function CharacterToJSON(value?: Character | null): any { 142 | if (value == null) { 143 | return value; 144 | } 145 | return { 146 | 147 | 'id': value['id'], 148 | 'name': value['name'], 149 | 'status': value['status'], 150 | 'species': value['species'], 151 | 'type': value['type'], 152 | 'gender': value['gender'], 153 | 'origin': CharacterOriginToJSON(value['origin']), 154 | 'location': CharacterLocationToJSON(value['location']), 155 | 'image': value['image'], 156 | 'episode': value['episode'], 157 | 'url': value['url'], 158 | 'created': value['created'] == null ? undefined : ((value['created']).toISOString()), 159 | }; 160 | } 161 | 162 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/models/CharacterListResponse.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Rick and Morty API 5 | * Access information about characters from Rick and Morty. 6 | * 7 | * The version of the OpenAPI document: 1.0.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | import type { Character } from './Character'; 17 | import { 18 | CharacterFromJSON, 19 | CharacterFromJSONTyped, 20 | CharacterToJSON, 21 | } from './Character'; 22 | import type { CharacterListResponseInfo } from './CharacterListResponseInfo'; 23 | import { 24 | CharacterListResponseInfoFromJSON, 25 | CharacterListResponseInfoFromJSONTyped, 26 | CharacterListResponseInfoToJSON, 27 | } from './CharacterListResponseInfo'; 28 | 29 | /** 30 | * 31 | * @export 32 | * @interface CharacterListResponse 33 | */ 34 | export interface CharacterListResponse { 35 | /** 36 | * 37 | * @type {CharacterListResponseInfo} 38 | * @memberof CharacterListResponse 39 | */ 40 | info?: CharacterListResponseInfo; 41 | /** 42 | * 43 | * @type {Array} 44 | * @memberof CharacterListResponse 45 | */ 46 | results?: Array; 47 | } 48 | 49 | /** 50 | * Check if a given object implements the CharacterListResponse interface. 51 | */ 52 | export function instanceOfCharacterListResponse(value: object): boolean { 53 | let isInstance = true; 54 | 55 | return isInstance; 56 | } 57 | 58 | export function CharacterListResponseFromJSON(json: any): CharacterListResponse { 59 | return CharacterListResponseFromJSONTyped(json, false); 60 | } 61 | 62 | export function CharacterListResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): CharacterListResponse { 63 | if ((json === undefined) || (json === null)) { 64 | return json; 65 | } 66 | return { 67 | 68 | 'info': !exists(json, 'info') ? undefined : CharacterListResponseInfoFromJSON(json['info']), 69 | 'results': !exists(json, 'results') ? undefined : ((json['results'] as Array).map(CharacterFromJSON)), 70 | }; 71 | } 72 | 73 | export function CharacterListResponseToJSON(value?: CharacterListResponse | null): any { 74 | if (value === undefined) { 75 | return undefined; 76 | } 77 | if (value === null) { 78 | return null; 79 | } 80 | return { 81 | 82 | 'info': CharacterListResponseInfoToJSON(value.info), 83 | 'results': value.results === undefined ? undefined : ((value.results as Array).map(CharacterToJSON)), 84 | }; 85 | } 86 | 87 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/models/CharacterListResponseInfo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Rick and Morty API 5 | * Access information about characters from Rick and Morty. 6 | * 7 | * The version of the OpenAPI document: 1.0.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { exists, mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface CharacterListResponseInfo 20 | */ 21 | export interface CharacterListResponseInfo { 22 | /** 23 | * The total number of characters. 24 | * @type {number} 25 | * @memberof CharacterListResponseInfo 26 | */ 27 | count?: number; 28 | /** 29 | * The total number of pages. 30 | * @type {number} 31 | * @memberof CharacterListResponseInfo 32 | */ 33 | pages?: number; 34 | /** 35 | * URL of the next page. 36 | * @type {string} 37 | * @memberof CharacterListResponseInfo 38 | */ 39 | next?: string | null; 40 | /** 41 | * URL of the previous page. 42 | * @type {string} 43 | * @memberof CharacterListResponseInfo 44 | */ 45 | prev?: string | null; 46 | } 47 | 48 | /** 49 | * Check if a given object implements the CharacterListResponseInfo interface. 50 | */ 51 | export function instanceOfCharacterListResponseInfo(value: object): boolean { 52 | let isInstance = true; 53 | 54 | return isInstance; 55 | } 56 | 57 | export function CharacterListResponseInfoFromJSON(json: any): CharacterListResponseInfo { 58 | return CharacterListResponseInfoFromJSONTyped(json, false); 59 | } 60 | 61 | export function CharacterListResponseInfoFromJSONTyped(json: any, ignoreDiscriminator: boolean): CharacterListResponseInfo { 62 | if ((json === undefined) || (json === null)) { 63 | return json; 64 | } 65 | return { 66 | 67 | 'count': !exists(json, 'count') ? undefined : json['count'], 68 | 'pages': !exists(json, 'pages') ? undefined : json['pages'], 69 | 'next': !exists(json, 'next') ? undefined : json['next'], 70 | 'prev': !exists(json, 'prev') ? undefined : json['prev'], 71 | }; 72 | } 73 | 74 | export function CharacterListResponseInfoToJSON(value?: CharacterListResponseInfo | null): any { 75 | if (value === undefined) { 76 | return undefined; 77 | } 78 | if (value === null) { 79 | return null; 80 | } 81 | return { 82 | 83 | 'count': value.count, 84 | 'pages': value.pages, 85 | 'next': value.next, 86 | 'prev': value.prev, 87 | }; 88 | } 89 | 90 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/models/CharacterLocation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Rick and Morty API 5 | * API for fetching character information from Rick and Morty series 6 | * 7 | * The version of the OpenAPI document: 1.0.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface CharacterLocation 20 | */ 21 | export interface CharacterLocation { 22 | /** 23 | * 24 | * @type {string} 25 | * @memberof CharacterLocation 26 | */ 27 | name?: string; 28 | /** 29 | * 30 | * @type {string} 31 | * @memberof CharacterLocation 32 | */ 33 | url?: string; 34 | } 35 | 36 | /** 37 | * Check if a given object implements the CharacterLocation interface. 38 | */ 39 | export function instanceOfCharacterLocation(value: object): boolean { 40 | return true; 41 | } 42 | 43 | export function CharacterLocationFromJSON(json: any): CharacterLocation { 44 | return CharacterLocationFromJSONTyped(json, false); 45 | } 46 | 47 | export function CharacterLocationFromJSONTyped(json: any, ignoreDiscriminator: boolean): CharacterLocation { 48 | if (json == null) { 49 | return json; 50 | } 51 | return { 52 | 53 | 'name': json['name'] == null ? undefined : json['name'], 54 | 'url': json['url'] == null ? undefined : json['url'], 55 | }; 56 | } 57 | 58 | export function CharacterLocationToJSON(value?: CharacterLocation | null): any { 59 | if (value == null) { 60 | return value; 61 | } 62 | return { 63 | 64 | 'name': value['name'], 65 | 'url': value['url'], 66 | }; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/models/CharacterOrigin.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Rick and Morty API 5 | * API for fetching character information from Rick and Morty series 6 | * 7 | * The version of the OpenAPI document: 1.0.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface CharacterOrigin 20 | */ 21 | export interface CharacterOrigin { 22 | /** 23 | * 24 | * @type {string} 25 | * @memberof CharacterOrigin 26 | */ 27 | name?: string; 28 | /** 29 | * 30 | * @type {string} 31 | * @memberof CharacterOrigin 32 | */ 33 | url?: string; 34 | } 35 | 36 | /** 37 | * Check if a given object implements the CharacterOrigin interface. 38 | */ 39 | export function instanceOfCharacterOrigin(value: object): boolean { 40 | return true; 41 | } 42 | 43 | export function CharacterOriginFromJSON(json: any): CharacterOrigin { 44 | return CharacterOriginFromJSONTyped(json, false); 45 | } 46 | 47 | export function CharacterOriginFromJSONTyped(json: any, ignoreDiscriminator: boolean): CharacterOrigin { 48 | if (json == null) { 49 | return json; 50 | } 51 | return { 52 | 53 | 'name': json['name'] == null ? undefined : json['name'], 54 | 'url': json['url'] == null ? undefined : json['url'], 55 | }; 56 | } 57 | 58 | export function CharacterOriginToJSON(value?: CharacterOrigin | null): any { 59 | if (value == null) { 60 | return value; 61 | } 62 | return { 63 | 64 | 'name': value['name'], 65 | 'url': value['url'], 66 | }; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/models/FetchAllCharacters200Response.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Rick and Morty API 5 | * API for fetching character information from Rick and Morty series 6 | * 7 | * The version of the OpenAPI document: 1.0.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { mapValues } from '../runtime'; 16 | import type { Character } from './Character'; 17 | import { 18 | CharacterFromJSON, 19 | CharacterFromJSONTyped, 20 | CharacterToJSON, 21 | } from './Character'; 22 | import type { FetchAllCharacters200ResponseInfo } from './FetchAllCharacters200ResponseInfo'; 23 | import { 24 | FetchAllCharacters200ResponseInfoFromJSON, 25 | FetchAllCharacters200ResponseInfoFromJSONTyped, 26 | FetchAllCharacters200ResponseInfoToJSON, 27 | } from './FetchAllCharacters200ResponseInfo'; 28 | 29 | /** 30 | * 31 | * @export 32 | * @interface FetchAllCharacters200Response 33 | */ 34 | export interface FetchAllCharacters200Response { 35 | /** 36 | * 37 | * @type {FetchAllCharacters200ResponseInfo} 38 | * @memberof FetchAllCharacters200Response 39 | */ 40 | info?: FetchAllCharacters200ResponseInfo; 41 | /** 42 | * 43 | * @type {Array} 44 | * @memberof FetchAllCharacters200Response 45 | */ 46 | results?: Array; 47 | } 48 | 49 | /** 50 | * Check if a given object implements the FetchAllCharacters200Response interface. 51 | */ 52 | export function instanceOfFetchAllCharacters200Response(value: object): boolean { 53 | return true; 54 | } 55 | 56 | export function FetchAllCharacters200ResponseFromJSON(json: any): FetchAllCharacters200Response { 57 | return FetchAllCharacters200ResponseFromJSONTyped(json, false); 58 | } 59 | 60 | export function FetchAllCharacters200ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): FetchAllCharacters200Response { 61 | if (json == null) { 62 | return json; 63 | } 64 | return { 65 | 66 | 'info': json['info'] == null ? undefined : FetchAllCharacters200ResponseInfoFromJSON(json['info']), 67 | 'results': json['results'] == null ? undefined : ((json['results'] as Array).map(CharacterFromJSON)), 68 | }; 69 | } 70 | 71 | export function FetchAllCharacters200ResponseToJSON(value?: FetchAllCharacters200Response | null): any { 72 | if (value == null) { 73 | return value; 74 | } 75 | return { 76 | 77 | 'info': FetchAllCharacters200ResponseInfoToJSON(value['info']), 78 | 'results': value['results'] == null ? undefined : ((value['results'] as Array).map(CharacterToJSON)), 79 | }; 80 | } 81 | 82 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/models/FetchAllCharacters200ResponseInfo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Rick and Morty API 5 | * API for fetching character information from Rick and Morty series 6 | * 7 | * The version of the OpenAPI document: 1.0.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | import { mapValues } from '../runtime'; 16 | /** 17 | * 18 | * @export 19 | * @interface FetchAllCharacters200ResponseInfo 20 | */ 21 | export interface FetchAllCharacters200ResponseInfo { 22 | /** 23 | * 24 | * @type {number} 25 | * @memberof FetchAllCharacters200ResponseInfo 26 | */ 27 | count?: number; 28 | /** 29 | * 30 | * @type {number} 31 | * @memberof FetchAllCharacters200ResponseInfo 32 | */ 33 | pages?: number; 34 | /** 35 | * 36 | * @type {string} 37 | * @memberof FetchAllCharacters200ResponseInfo 38 | */ 39 | next?: string; 40 | /** 41 | * 42 | * @type {string} 43 | * @memberof FetchAllCharacters200ResponseInfo 44 | */ 45 | prev?: string; 46 | } 47 | 48 | /** 49 | * Check if a given object implements the FetchAllCharacters200ResponseInfo interface. 50 | */ 51 | export function instanceOfFetchAllCharacters200ResponseInfo(value: object): boolean { 52 | return true; 53 | } 54 | 55 | export function FetchAllCharacters200ResponseInfoFromJSON(json: any): FetchAllCharacters200ResponseInfo { 56 | return FetchAllCharacters200ResponseInfoFromJSONTyped(json, false); 57 | } 58 | 59 | export function FetchAllCharacters200ResponseInfoFromJSONTyped(json: any, ignoreDiscriminator: boolean): FetchAllCharacters200ResponseInfo { 60 | if (json == null) { 61 | return json; 62 | } 63 | return { 64 | 65 | 'count': json['count'] == null ? undefined : json['count'], 66 | 'pages': json['pages'] == null ? undefined : json['pages'], 67 | 'next': json['next'] == null ? undefined : json['next'], 68 | 'prev': json['prev'] == null ? undefined : json['prev'], 69 | }; 70 | } 71 | 72 | export function FetchAllCharacters200ResponseInfoToJSON(value?: FetchAllCharacters200ResponseInfo | null): any { 73 | if (value == null) { 74 | return value; 75 | } 76 | return { 77 | 78 | 'count': value['count'], 79 | 'pages': value['pages'], 80 | 'next': value['next'], 81 | 'prev': value['prev'], 82 | }; 83 | } 84 | 85 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/models/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export * from './Character'; 4 | export * from './CharacterLocation'; 5 | export * from './CharacterOrigin'; 6 | export * from './FetchAllCharacters200Response'; 7 | export * from './FetchAllCharacters200ResponseInfo'; 8 | -------------------------------------------------------------------------------- /lib/rick-and-morty-api-client/runtime.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Rick and Morty API 5 | * API for fetching character information from Rick and Morty series 6 | * 7 | * The version of the OpenAPI document: 1.0.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | 16 | export const BASE_PATH = "https://rickandmortyapi.com/api".replace(/\/+$/, ""); 17 | 18 | export interface ConfigurationParameters { 19 | basePath?: string; // override base path 20 | fetchApi?: FetchAPI; // override for fetch implementation 21 | middleware?: Middleware[]; // middleware to apply before/after fetch requests 22 | queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings 23 | username?: string; // parameter for basic security 24 | password?: string; // parameter for basic security 25 | apiKey?: string | Promise | ((name: string) => string | Promise); // parameter for apiKey security 26 | accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security 27 | headers?: HTTPHeaders; //header params we want to use on every request 28 | credentials?: RequestCredentials; //value for the credentials param we want to use on each request 29 | } 30 | 31 | export class Configuration { 32 | constructor(private configuration: ConfigurationParameters = {}) {} 33 | 34 | set config(configuration: Configuration) { 35 | this.configuration = configuration; 36 | } 37 | 38 | get basePath(): string { 39 | return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; 40 | } 41 | 42 | get fetchApi(): FetchAPI | undefined { 43 | return this.configuration.fetchApi; 44 | } 45 | 46 | get middleware(): Middleware[] { 47 | return this.configuration.middleware || []; 48 | } 49 | 50 | get queryParamsStringify(): (params: HTTPQuery) => string { 51 | return this.configuration.queryParamsStringify || querystring; 52 | } 53 | 54 | get username(): string | undefined { 55 | return this.configuration.username; 56 | } 57 | 58 | get password(): string | undefined { 59 | return this.configuration.password; 60 | } 61 | 62 | get apiKey(): ((name: string) => string | Promise) | undefined { 63 | const apiKey = this.configuration.apiKey; 64 | if (apiKey) { 65 | return typeof apiKey === 'function' ? apiKey : () => apiKey; 66 | } 67 | return undefined; 68 | } 69 | 70 | get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { 71 | const accessToken = this.configuration.accessToken; 72 | if (accessToken) { 73 | return typeof accessToken === 'function' ? accessToken : async () => accessToken; 74 | } 75 | return undefined; 76 | } 77 | 78 | get headers(): HTTPHeaders | undefined { 79 | return this.configuration.headers; 80 | } 81 | 82 | get credentials(): RequestCredentials | undefined { 83 | return this.configuration.credentials; 84 | } 85 | } 86 | 87 | export const DefaultConfig = new Configuration(); 88 | 89 | /** 90 | * This is the base class for all generated API classes. 91 | */ 92 | export class BaseAPI { 93 | 94 | private static readonly jsonRegex = new RegExp('^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i'); 95 | private middleware: Middleware[]; 96 | 97 | constructor(protected configuration = DefaultConfig) { 98 | this.middleware = configuration.middleware; 99 | } 100 | 101 | withMiddleware(this: T, ...middlewares: Middleware[]) { 102 | const next = this.clone(); 103 | next.middleware = next.middleware.concat(...middlewares); 104 | return next; 105 | } 106 | 107 | withPreMiddleware(this: T, ...preMiddlewares: Array) { 108 | const middlewares = preMiddlewares.map((pre) => ({ pre })); 109 | return this.withMiddleware(...middlewares); 110 | } 111 | 112 | withPostMiddleware(this: T, ...postMiddlewares: Array) { 113 | const middlewares = postMiddlewares.map((post) => ({ post })); 114 | return this.withMiddleware(...middlewares); 115 | } 116 | 117 | /** 118 | * Check if the given MIME is a JSON MIME. 119 | * JSON MIME examples: 120 | * application/json 121 | * application/json; charset=UTF8 122 | * APPLICATION/JSON 123 | * application/vnd.company+json 124 | * @param mime - MIME (Multipurpose Internet Mail Extensions) 125 | * @return True if the given MIME is JSON, false otherwise. 126 | */ 127 | protected isJsonMime(mime: string | null | undefined): boolean { 128 | if (!mime) { 129 | return false; 130 | } 131 | return BaseAPI.jsonRegex.test(mime); 132 | } 133 | 134 | protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { 135 | const { url, init } = await this.createFetchParams(context, initOverrides); 136 | const response = await this.fetchApi(url, init); 137 | if (response && (response.status >= 200 && response.status < 300)) { 138 | return response; 139 | } 140 | throw new ResponseError(response, 'Response returned an error code'); 141 | } 142 | 143 | private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { 144 | let url = this.configuration.basePath + context.path; 145 | if (context.query !== undefined && Object.keys(context.query).length !== 0) { 146 | // only add the querystring to the URL if there are query parameters. 147 | // this is done to avoid urls ending with a "?" character which buggy webservers 148 | // do not handle correctly sometimes. 149 | url += '?' + this.configuration.queryParamsStringify(context.query); 150 | } 151 | 152 | const headers = Object.assign({}, this.configuration.headers, context.headers); 153 | Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); 154 | 155 | const initOverrideFn = 156 | typeof initOverrides === "function" 157 | ? initOverrides 158 | : async () => initOverrides; 159 | 160 | const initParams = { 161 | method: context.method, 162 | headers, 163 | body: context.body, 164 | credentials: this.configuration.credentials, 165 | }; 166 | 167 | const overriddenInit: RequestInit = { 168 | ...initParams, 169 | ...(await initOverrideFn({ 170 | init: initParams, 171 | context, 172 | })) 173 | }; 174 | 175 | let body: any; 176 | if (isFormData(overriddenInit.body) 177 | || (overriddenInit.body instanceof URLSearchParams) 178 | || isBlob(overriddenInit.body)) { 179 | body = overriddenInit.body; 180 | } else if (this.isJsonMime(headers['Content-Type'])) { 181 | body = JSON.stringify(overriddenInit.body); 182 | } else { 183 | body = overriddenInit.body; 184 | } 185 | 186 | const init: RequestInit = { 187 | ...overriddenInit, 188 | body 189 | }; 190 | 191 | return { url, init }; 192 | } 193 | 194 | private fetchApi = async (url: string, init: RequestInit) => { 195 | let fetchParams = { url, init }; 196 | for (const middleware of this.middleware) { 197 | if (middleware.pre) { 198 | fetchParams = await middleware.pre({ 199 | fetch: this.fetchApi, 200 | ...fetchParams, 201 | }) || fetchParams; 202 | } 203 | } 204 | let response: Response | undefined = undefined; 205 | try { 206 | response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); 207 | } catch (e) { 208 | for (const middleware of this.middleware) { 209 | if (middleware.onError) { 210 | response = await middleware.onError({ 211 | fetch: this.fetchApi, 212 | url: fetchParams.url, 213 | init: fetchParams.init, 214 | error: e, 215 | response: response ? response.clone() : undefined, 216 | }) || response; 217 | } 218 | } 219 | if (response === undefined) { 220 | if (e instanceof Error) { 221 | throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); 222 | } else { 223 | throw e; 224 | } 225 | } 226 | } 227 | for (const middleware of this.middleware) { 228 | if (middleware.post) { 229 | response = await middleware.post({ 230 | fetch: this.fetchApi, 231 | url: fetchParams.url, 232 | init: fetchParams.init, 233 | response: response.clone(), 234 | }) || response; 235 | } 236 | } 237 | return response; 238 | } 239 | 240 | /** 241 | * Create a shallow clone of `this` by constructing a new instance 242 | * and then shallow cloning data members. 243 | */ 244 | private clone(this: T): T { 245 | const constructor = this.constructor as any; 246 | const next = new constructor(this.configuration); 247 | next.middleware = this.middleware.slice(); 248 | return next; 249 | } 250 | }; 251 | 252 | function isBlob(value: any): value is Blob { 253 | return typeof Blob !== 'undefined' && value instanceof Blob; 254 | } 255 | 256 | function isFormData(value: any): value is FormData { 257 | return typeof FormData !== "undefined" && value instanceof FormData; 258 | } 259 | 260 | export class ResponseError extends Error { 261 | override name: "ResponseError" = "ResponseError"; 262 | constructor(public response: Response, msg?: string) { 263 | super(msg); 264 | } 265 | } 266 | 267 | export class FetchError extends Error { 268 | override name: "FetchError" = "FetchError"; 269 | constructor(public cause: Error, msg?: string) { 270 | super(msg); 271 | } 272 | } 273 | 274 | export class RequiredError extends Error { 275 | override name: "RequiredError" = "RequiredError"; 276 | constructor(public field: string, msg?: string) { 277 | super(msg); 278 | } 279 | } 280 | 281 | export const COLLECTION_FORMATS = { 282 | csv: ",", 283 | ssv: " ", 284 | tsv: "\t", 285 | pipes: "|", 286 | }; 287 | 288 | export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; 289 | 290 | export type Json = any; 291 | export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; 292 | export type HTTPHeaders = { [key: string]: string }; 293 | export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; 294 | export type HTTPBody = Json | FormData | URLSearchParams; 295 | export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody }; 296 | export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; 297 | 298 | export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise 299 | 300 | export interface FetchParams { 301 | url: string; 302 | init: RequestInit; 303 | } 304 | 305 | export interface RequestOpts { 306 | path: string; 307 | method: HTTPMethod; 308 | headers: HTTPHeaders; 309 | query?: HTTPQuery; 310 | body?: HTTPBody; 311 | } 312 | 313 | export function querystring(params: HTTPQuery, prefix: string = ''): string { 314 | return Object.keys(params) 315 | .map(key => querystringSingleKey(key, params[key], prefix)) 316 | .filter(part => part.length > 0) 317 | .join('&'); 318 | } 319 | 320 | function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { 321 | const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); 322 | if (value instanceof Array) { 323 | const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) 324 | .join(`&${encodeURIComponent(fullKey)}=`); 325 | return `${encodeURIComponent(fullKey)}=${multiValue}`; 326 | } 327 | if (value instanceof Set) { 328 | const valueAsArray = Array.from(value); 329 | return querystringSingleKey(key, valueAsArray, keyPrefix); 330 | } 331 | if (value instanceof Date) { 332 | return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; 333 | } 334 | if (value instanceof Object) { 335 | return querystring(value as HTTPQuery, fullKey); 336 | } 337 | return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; 338 | } 339 | 340 | export function mapValues(data: any, fn: (item: any) => any) { 341 | return Object.keys(data).reduce( 342 | (acc, key) => ({ ...acc, [key]: fn(data[key]) }), 343 | {} 344 | ); 345 | } 346 | 347 | export function canConsumeForm(consumes: Consume[]): boolean { 348 | for (const consume of consumes) { 349 | if ('multipart/form-data' === consume.contentType) { 350 | return true; 351 | } 352 | } 353 | return false; 354 | } 355 | 356 | export interface Consume { 357 | contentType: string; 358 | } 359 | 360 | export interface RequestContext { 361 | fetch: FetchAPI; 362 | url: string; 363 | init: RequestInit; 364 | } 365 | 366 | export interface ResponseContext { 367 | fetch: FetchAPI; 368 | url: string; 369 | init: RequestInit; 370 | response: Response; 371 | } 372 | 373 | export interface ErrorContext { 374 | fetch: FetchAPI; 375 | url: string; 376 | init: RequestInit; 377 | error: unknown; 378 | response?: Response; 379 | } 380 | 381 | export interface Middleware { 382 | pre?(context: RequestContext): Promise; 383 | post?(context: ResponseContext): Promise; 384 | onError?(context: ErrorContext): Promise; 385 | } 386 | 387 | export interface ApiResponse { 388 | raw: Response; 389 | value(): Promise; 390 | } 391 | 392 | export interface ResponseTransformer { 393 | (json: any): T; 394 | } 395 | 396 | export class JSONApiResponse { 397 | constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {} 398 | 399 | async value(): Promise { 400 | return this.transformer(await this.raw.json()); 401 | } 402 | } 403 | 404 | export class VoidApiResponse { 405 | constructor(public raw: Response) {} 406 | 407 | async value(): Promise { 408 | return undefined; 409 | } 410 | } 411 | 412 | export class BlobApiResponse { 413 | constructor(public raw: Response) {} 414 | 415 | async value(): Promise { 416 | return await this.raw.blob(); 417 | }; 418 | } 419 | 420 | export class TextApiResponse { 421 | constructor(public raw: Response) {} 422 | 423 | async value(): Promise { 424 | return await this.raw.text(); 425 | }; 426 | } 427 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-bootstrap", 3 | "version": "1.0.0", 4 | "description": "Opinionated frontend bootstrap", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "vite build", 9 | "dev": "vite", 10 | "lint": "eslint src", 11 | "test": "vitest src", 12 | "test:e2e": "npx playwright install && playwright test" 13 | }, 14 | "author": "Przeprogramowani", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@playwright/test": "1.42.1", 18 | "@types/node": "20.11.28", 19 | "@types/react": "18.2.63", 20 | "@types/react-dom": "18.2.20", 21 | "@typescript-eslint/eslint-plugin": "7.6.0", 22 | "@typescript-eslint/parser": "7.6.0", 23 | "@vitejs/plugin-react": "4.2.1", 24 | "autoprefixer": "10.4.18", 25 | "eslint": "8.57.0", 26 | "eslint-config-prettier": "9.1.0", 27 | "eslint-plugin-prettier": "5.1.3", 28 | "eslint-plugin-react": "7.34.0", 29 | "eslint-plugin-react-hooks": "4.6.0", 30 | "postcss": "8.4.35", 31 | "prettier": "3.2.5", 32 | "tailwindcss": "3.4.1", 33 | "typescript": "5.4.3", 34 | "vite": "5.2.6", 35 | "vitest": "1.4.0" 36 | }, 37 | "dependencies": { 38 | "react": "18.2.0", 39 | "react-dom": "18.2.0", 40 | "react-router-dom": "6.22.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: './tests/e2e', 14 | fullyParallel: true, 15 | forbidOnly: !!process.env.CI, 16 | retries: process.env.CI ? 2 : 0, 17 | workers: process.env.CI ? 1 : undefined, 18 | reporter: 'html', 19 | use: { 20 | baseURL: process.env.CI ? process.env.E2E_BASE_URL : 'http://localhost:3000', 21 | trace: 'on-first-retry', 22 | }, 23 | projects: [ 24 | { 25 | name: 'chromium', 26 | use: { ...devices['Desktop Chrome'] }, 27 | }, 28 | ], 29 | webServer: { 30 | command: 'npm run dev', 31 | url: 'http://localhost:3000/', 32 | reuseExistingServer: !process.env.CI, 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet } from 'react-router-dom'; 3 | 4 | const App = () => { 5 | return ( 6 |
7 |
8 |

🚀 Rick and Morty - Fan Service

9 |
10 | 11 |
12 | ); 13 | }; 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /src/context/ConfigContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, ReactNode } from 'react'; 2 | import { Config } from '../types/types'; 3 | 4 | export const ConfigContext = createContext({} as Config); 5 | 6 | interface ConfigProviderProps { 7 | children: ReactNode; 8 | } 9 | 10 | export const ConfigProvider = ({ children }: ConfigProviderProps) => { 11 | const cfg: Config = { 12 | appVersion: __APP_VERSION__, 13 | }; 14 | 15 | return {children}; 16 | }; 17 | 18 | export const useConfig = (): Config => { 19 | const context = useContext(ConfigContext); 20 | if (context === undefined) { 21 | throw new Error('useConfig must be used within a ConfigProvider'); 22 | } 23 | return context; 24 | }; 25 | -------------------------------------------------------------------------------- /src/loaders/CharacterLoader.ts: -------------------------------------------------------------------------------- 1 | import { getTopCharacters } from '../utils/CharactersProcessor'; 2 | import { CharacterRouteParams } from '../types/types'; 3 | import { DefaultApi } from '../../lib/rick-and-morty-api-client'; 4 | 5 | export async function fetchCharacters() { 6 | const api = new DefaultApi(); 7 | const response = await api.fetchAllCharacters(); 8 | return { characters: getTopCharacters(response.results!, 5) }; 9 | } 10 | 11 | export async function fetchCharacter({ params }: CharacterRouteParams) { 12 | const api = new DefaultApi(); 13 | return await api.fetchSingleCharacter({ id: parseInt(params.id) }); 14 | } 15 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { RouterProvider, createHashRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | import { fetchCharacter, fetchCharacters } from './loaders/CharacterLoader'; 6 | import { CharacterRouteParams } from './types/types'; 7 | import { ConfigProvider } from './context/ConfigContext'; 8 | 9 | const Characters = lazy(() => import('./pages/Characters')); 10 | const CharacterDetails = lazy(() => import('./pages/CharacterDetails')); 11 | 12 | const router = createHashRouter([ 13 | { 14 | path: '/', 15 | element: , 16 | children: [ 17 | { 18 | index: true, 19 | loader: fetchCharacters, 20 | element: , 21 | }, 22 | { 23 | path: 'character/:id', 24 | loader: async ({ params }) => { 25 | return fetchCharacter({ params } as CharacterRouteParams); 26 | }, 27 | element: , 28 | }, 29 | ], 30 | }, 31 | ]); 32 | 33 | ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render( 34 | 35 | 36 | 37 | 38 | , 39 | ); 40 | -------------------------------------------------------------------------------- /src/pages/CharacterDetails.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useLoaderData } from 'react-router-dom'; 3 | import { Character } from '../../lib/rick-and-morty-api-client'; 4 | 5 | const CharacterDetails = () => { 6 | const character = useLoaderData() as Character; 7 | 8 | return ( 9 | <> 10 |
11 |
12 |
13 | {`${character.name} 18 |
19 |
20 |

21 | {character.name} 22 |

23 |

Status: {character.status}

24 |

Type: {character.species}

25 |

Location: {character.location?.name}

26 |
27 |
28 |
29 | 34 | Back to list 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default CharacterDetails; 41 | -------------------------------------------------------------------------------- /src/pages/Characters.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useLoaderData } from 'react-router-dom'; 3 | import { Character } from '../../lib/rick-and-morty-api-client'; 4 | 5 | const Characters = () => { 6 | const { characters } = useLoaderData() as { characters: Character[] }; 7 | 8 | return ( 9 |
10 |

Select a character:

11 |
    12 | {characters.map((character) => ( 13 |
  • 14 | 15 |
    16 | {`${character.name} 21 |

    {character.name}

    22 |
    23 | 24 |
  • 25 | ))} 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default Characters; 32 | -------------------------------------------------------------------------------- /src/types/types.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | appVersion: string; 3 | } 4 | 5 | export type CharacterRouteParams = { params: { id: string } }; 6 | -------------------------------------------------------------------------------- /src/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly APP_VERSION: string; 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv; 9 | } 10 | 11 | declare const __APP_VERSION__: string; 12 | -------------------------------------------------------------------------------- /src/utils/CharactersProcessor.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | import { getTopCharacters } from './CharactersProcessor'; 3 | import { Character } from '../../lib/rick-and-morty-api-client'; 4 | 5 | const characters: Character[] = [ 6 | { 7 | id: 1, 8 | name: 'Rick Sanchez', 9 | status: 'Alive', 10 | species: 'Human', 11 | type: '', 12 | gender: 'Male', 13 | origin: { 14 | name: 'Earth (C-137)', 15 | url: 'https://rickandmortyapi.com/api/location/1', 16 | }, 17 | location: { 18 | name: 'Earth (Replacement Dimension)', 19 | url: 'https://rickandmortyapi.com/api/location/20', 20 | }, 21 | image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg', 22 | episode: ['https://rickandmortyapi.com/api/episode/1'], 23 | url: 'https://rickandmortyapi.com/api/character/1', 24 | created: new Date('2017-11-04T18:48:46.250Z'), 25 | }, 26 | { 27 | id: 2, 28 | name: 'Morty Smith', 29 | status: 'Alive', 30 | species: 'Human', 31 | type: '', 32 | gender: 'Male', 33 | origin: { 34 | name: 'Earth (C-137)', 35 | url: 'https://rickandmortyapi.com/api/location/1', 36 | }, 37 | location: { 38 | name: 'Earth (Replacement Dimension)', 39 | url: 'https://rickandmortyapi.com/api/location/20', 40 | }, 41 | image: 'https://rickandmortyapi.com/api/character/avatar/2.jpeg', 42 | episode: ['https://rickandmortyapi.com/api/episode/1'], 43 | url: 'https://rickandmortyapi.com/api/character/2', 44 | created: new Date('2017-11-04T18:50:21.651Z'), 45 | }, 46 | ]; 47 | 48 | describe('CharactersProcessor', () => { 49 | test('should return empty array', () => { 50 | const topCharacters = getTopCharacters([], 2); 51 | expect(topCharacters.length).toBe(0); 52 | }); 53 | 54 | test('should return two characters', () => { 55 | const topCharacters = getTopCharacters(characters, 1); 56 | expect(topCharacters.length).toBe(1); 57 | expect(topCharacters[0].name).toBe('Rick Sanchez'); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/utils/CharactersProcessor.ts: -------------------------------------------------------------------------------- 1 | import { Character } from '../../lib/rick-and-morty-api-client'; 2 | 3 | export function getTopCharacters(characters: Character[], top: number): Character[] { 4 | return characters.slice(0, top); 5 | } 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /tests/e2e/pages/CharacterDetailsPage.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test'; 2 | import { URLs } from '../utils/constants'; 3 | 4 | export class CharacterDetailsPage { 5 | private readonly page: Page; 6 | private readonly url: string = ''; 7 | 8 | constructor(page: Page, id: string) { 9 | this.page = page; 10 | this.url = URLs.CHARACTER_DETAILS_PAGE(id); 11 | } 12 | 13 | navigate() { 14 | return this.page.goto(this.url); 15 | } 16 | 17 | async navigateToCharactersList() { 18 | await this.page.click(`[data-testid="characters-list-link"]`); 19 | await this.page.waitForSelector('[data-testid="characters-list"]'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/e2e/pages/CharactersPage.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test'; 2 | import { URLs } from '../utils/constants'; 3 | 4 | export class CharactersPage { 5 | private readonly page: Page; 6 | private readonly url = URLs.CHARACTERS_PAGE; 7 | 8 | constructor(page: Page) { 9 | this.page = page; 10 | } 11 | 12 | navigate() { 13 | return this.page.goto(this.url); 14 | } 15 | 16 | async navigateToCharacterDetails(characterId: string) { 17 | await this.page.click(`[data-testid="character-link-${characterId}"]`); 18 | await this.page.waitForSelector('[data-testid="character-details"]'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/e2e/tests/details.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | import { CharactersPage } from '../pages/CharactersPage'; 3 | import { CharacterDetailsPage } from '../pages/CharacterDetailsPage'; 4 | 5 | test('navigation to character details', async ({ page }) => { 6 | const mainPage = new CharactersPage(page); 7 | await mainPage.navigate(); 8 | await mainPage.navigateToCharacterDetails('1'); 9 | }); 10 | 11 | test('navigation to characters list', async ({ page }) => { 12 | const detailsPage = new CharacterDetailsPage(page, '3'); 13 | await detailsPage.navigate(); 14 | await detailsPage.navigateToCharactersList(); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/e2e/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const URLs = { 2 | CHARACTERS_PAGE: '/', 3 | CHARACTER_DETAILS_PAGE: (id) => `/#/character/${id}`, 4 | }; 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import packageJson from './package.json'; 4 | 5 | export default defineConfig({ 6 | define: { 7 | __APP_VERSION__: JSON.stringify(packageJson.version), 8 | }, 9 | server: { 10 | port: 3000, 11 | }, 12 | plugins: [react()], 13 | }); 14 | --------------------------------------------------------------------------------