Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions fullstack/task/packages/client/codegen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
overwrite: true
schema: "http://localhost:4001/graphql"
documents: "src/**/*.{tsx,ts}"
generates:
src/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
config:
withHooks: true
apolloReactHooksImportFrom: "@apollo/client"
maybeValue: T | undefined
scalars:
DateTime: string
16 changes: 14 additions & 2 deletions fullstack/task/packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@
"name": "client",
"private": true,
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"generate-types": "graphql-codegen --config codegen.yml"
},
"dependencies": {
"@apollo/client": "3.7.10",
"graphql": "16.6.0",
"@apollo/client": "^3.13.4",
"@graphql-codegen/cli": "^5.0.5",
"@graphql-codegen/typescript": "^4.1.5",
"@graphql-codegen/typescript-operations": "^4.5.1",
"@graphql-codegen/typescript-react-apollo": "^4.3.2",
"bootstrap": "^5.3.3",
"graphql": "^16.10.0",
"graphql-tag": "2.12.6",
"graphql-ws": "5.12.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-is": "18.2.0",
"react-router": "^7.3.0",
"styled-components": "5.3.9",
"vite-tsconfig-paths": "^4.0.7"
},
Expand Down
16 changes: 13 additions & 3 deletions fullstack/task/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
function App() {
return <p>TODO</p>;
}
import { Route, Routes } from 'react-router';
import ExchangeRates from './pages/ExchangeRates';
import Home from './pages/home';
import 'bootstrap/dist/css/bootstrap.min.css';

const App = () => {
return (
<Routes>
<Route index element={<Home />} />
<Route path="/exchange-rates" element={<ExchangeRates />} />
</Routes>
);
};

export default App;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ExchangeRate } from '../../generated/graphql';

interface ExchangeRatesTableProps {
rates?: ExchangeRate[];
}

export const ExchangeRatesTable = ({ rates }: ExchangeRatesTableProps) => {
return (
<div className="table-responsive">
<table className="table table-striped table-hover">
<thead className="table-dark">
<tr>
<th>Country</th>
<th>Currency</th>
<th>Amount</th>
<th>Code</th>
<th>Rate (CZK)</th>
</tr>
</thead>
<tbody>
{rates?.map((rate) => (
<tr key={rate.code}>
<td>{rate.country}</td>
<td>{rate.currency}</td>
<td>{rate.amount}</td>
<td>{rate.code}</td>
<td>{rate.rate}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
interface LastUpdatedPlaceholderProps {
lastFetchTimestamp?: string;
}

export const LastUpdatedPlaceholder = ({ lastFetchTimestamp }: LastUpdatedPlaceholderProps) => {
return (
<div>
{lastFetchTimestamp && (
<div className="text-muted mb-3">
Last updated: {new Date(lastFetchTimestamp).toLocaleString()}
</div>
)}
</div>
);
};
175 changes: 175 additions & 0 deletions fullstack/task/packages/client/src/generated/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { gql } from '@apollo/client';
import * as ApolloReactHooks from '@apollo/client';

export type Maybe<T> = T | undefined;
export type InputMaybe<T> = T | undefined;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = {
[_ in K]?: never;
};
export type Incremental<T> =
| T
| { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
const defaultOptions = {} as const;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string };
String: { input: string; output: string };
Boolean: { input: boolean; output: boolean };
Int: { input: number; output: number };
Float: { input: number; output: number };
DateTime: { input: string; output: string };
};

export type CreateExampleInputType = {
name: Scalars['String']['input'];
value: Scalars['String']['input'];
};

export type Example = {
__typename?: 'Example';
createdAtUtc: Scalars['DateTime']['output'];
deleteDateUtc?: Maybe<Scalars['DateTime']['output']>;
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
updatedAtUtc?: Maybe<Scalars['DateTime']['output']>;
value: Scalars['String']['output'];
version: Scalars['Int']['output'];
};

export type ExchangeRate = {
__typename?: 'ExchangeRate';
amount: Scalars['Float']['output'];
code: Scalars['String']['output'];
country: Scalars['String']['output'];
currency: Scalars['String']['output'];
rate: Scalars['Float']['output'];
};

export type ExchangeRates = {
__typename?: 'ExchangeRates';
lastFetchTimestamp: Scalars['DateTime']['output'];
rates: Array<ExchangeRate>;
};

export type Mutation = {
__typename?: 'Mutation';
createExample: Example;
};

export type MutationCreateExampleArgs = {
data: CreateExampleInputType;
};

export type Query = {
__typename?: 'Query';
exampleByName?: Maybe<Example>;
exchangeRates: ExchangeRates;
};

export type QueryExampleByNameArgs = {
name: Scalars['String']['input'];
};

export type GetExchangeRatesQueryVariables = Exact<{ [key: string]: never }>;

export type GetExchangeRatesQuery = {
__typename?: 'Query';
exchangeRates: {
__typename?: 'ExchangeRates';
lastFetchTimestamp: string;
rates: Array<{
__typename?: 'ExchangeRate';
country: string;
code: string;
currency: string;
amount: number;
rate: number;
}>;
};
};

export const GetExchangeRatesDocument = gql`
query GetExchangeRates {
exchangeRates {
rates {
country
code
currency
amount
rate
}
lastFetchTimestamp
}
}
`;

/**
* __useGetExchangeRatesQuery__
*
* To run a query within a React component, call `useGetExchangeRatesQuery` and pass it any options that fit your needs.
* When your component renders, `useGetExchangeRatesQuery` returns an object from Apollo Client that contains loading, error, and
* data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on:
* https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetExchangeRatesQuery({
* variables: {
* },
* });
*/
export function useGetExchangeRatesQuery(
baseOptions?: ApolloReactHooks.QueryHookOptions<
GetExchangeRatesQuery,
GetExchangeRatesQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return ApolloReactHooks.useQuery<GetExchangeRatesQuery, GetExchangeRatesQueryVariables>(
GetExchangeRatesDocument,
options
);
}
export function useGetExchangeRatesLazyQuery(
baseOptions?: ApolloReactHooks.LazyQueryHookOptions<
GetExchangeRatesQuery,
GetExchangeRatesQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return ApolloReactHooks.useLazyQuery<GetExchangeRatesQuery, GetExchangeRatesQueryVariables>(
GetExchangeRatesDocument,
options
);
}
export function useGetExchangeRatesSuspenseQuery(
baseOptions?:
| ApolloReactHooks.SkipToken
| ApolloReactHooks.SuspenseQueryHookOptions<
GetExchangeRatesQuery,
GetExchangeRatesQueryVariables
>
) {
const options =
baseOptions === ApolloReactHooks.skipToken
? baseOptions
: { ...defaultOptions, ...baseOptions };
return ApolloReactHooks.useSuspenseQuery<GetExchangeRatesQuery, GetExchangeRatesQueryVariables>(
GetExchangeRatesDocument,
options
);
}
export type GetExchangeRatesQueryHookResult = ReturnType<typeof useGetExchangeRatesQuery>;
export type GetExchangeRatesLazyQueryHookResult = ReturnType<typeof useGetExchangeRatesLazyQuery>;
export type GetExchangeRatesSuspenseQueryHookResult = ReturnType<
typeof useGetExchangeRatesSuspenseQuery
>;
export type GetExchangeRatesQueryResult = ApolloReactHooks.QueryResult<
GetExchangeRatesQuery,
GetExchangeRatesQueryVariables
>;
7 changes: 5 additions & 2 deletions fullstack/task/packages/client/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { BrowserRouter } from 'react-router';
import App from './App';

const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
uri: 'http://localhost:4001/graphql',
cache: new InMemoryCache(),
});

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</ApolloProvider>
</React.StrictMode>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { gql } from '@apollo/client';

export const EXCHANGE_RATES_QUERY = gql`
query GetExchangeRates {
exchangeRates {
rates {
country
code
currency
amount
rate
}
lastFetchTimestamp
}
}
`;

export interface ExchangeRate {
country: string;
code: string;
currency: string;
amount: number;
rate: number;
}

export interface ExchangeRatesResponse {
exchangeRates: {
rates: ExchangeRate[];
lastFetchTimestamp: string;
};
}
45 changes: 45 additions & 0 deletions fullstack/task/packages/client/src/pages/ExchangeRates/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useQuery } from '@apollo/client';
import { ExchangeRatesTable } from '../../components/exchangeRates/ExchangeRatesTable';
import { LastUpdatedPlaceholder } from '../../components/exchangeRates/LastUpdatedPlaceholder';
import { EXCHANGE_RATES_QUERY, ExchangeRatesResponse } from './graphql/query';

const handleError = (error: any) => {
if (error.networkError) {
return `Network error: ${error.networkError.message}`;
}
return `GraphQL error: ${error.message}`;
};

const ExchangeRates = () => {
const { loading, error, data } = useQuery<ExchangeRatesResponse>(EXCHANGE_RATES_QUERY, {
fetchPolicy: 'cache-and-network',
pollInterval: 300000,
});

if (loading)
return (
<div className="text-center mt-4">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);

if (error)
return (
<div className="alert alert-danger mt-4">
Error: {handleError(error)}
<div className="mt-2">Make sure the backend server is running!</div>
</div>
);

return (
<div className="container mt-4">
<h2 className="mb-4">Current Exchange Rates (CZK)</h2>
<LastUpdatedPlaceholder lastFetchTimestamp={data?.exchangeRates.lastFetchTimestamp} />
<ExchangeRatesTable rates={data?.exchangeRates?.rates} />
</div>
);
};

export default ExchangeRates;
Loading