├── .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 | 
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 |
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 |

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 |

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 |
--------------------------------------------------------------------------------