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
5 changes: 4 additions & 1 deletion fullstack/task/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React from 'react';
import ExchangeRatesTable from './components/ExchangeRatesTable';

function App() {
return <p>TODO</p>;
return <ExchangeRatesTable />;
}

export default App;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import styled from 'styled-components';

export const ExchangeRatesContainer = styled.div`
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;

export const ExchangeRatesHeader = styled.h1`
text-align: center;
color: #333;
margin-bottom: 30px;
`;

export const ExchangeRatesTableContent = styled.table`
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
`;

export const ExchangeRatesTableHeader = styled.th`
background-color: #f8f9fa;
padding: 12px;
text-align: left;
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6;
`;

export const ExchangeRatesTableCell = styled.td`
padding: 12px;
border-bottom: 1px solid #dee2e6;
`;

export const ExchangeRatesTableRow = styled.tr`
&:hover {
background-color: #f8f9fa;
}
`;

export const ExchangeRatesLoadingMessage = styled.div`
text-align: center;
padding: 40px;
color: #6c757d;
font-size: 16px;
`;

export const ExchangeRatesErrorMessage = styled.div`
text-align: center;
padding: 40px;
color: #dc3545;
font-size: 16px;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 8px;
margin-bottom: 20px;
`;

export const ExchangeRatesCacheInfo = styled.div`
text-align: center;
padding: 15px;
background-color: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 8px;
color: #0066cc;
font-size: 14px;
margin-bottom: 20px;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import { useExchangeRates } from '../../hooks';
import { formatDate, formatRate } from '../../utils';
import {
ExchangeRatesContainer,
ExchangeRatesHeader,
ExchangeRatesTableContent,
ExchangeRatesTableHeader,
ExchangeRatesTableCell,
ExchangeRatesTableRow,
ExchangeRatesLoadingMessage,
ExchangeRatesErrorMessage,
ExchangeRatesCacheInfo,
} from './ExchangeRatesTable.styles';

interface ExchangeRatesTableProps {}

const ExchangeRatesTable = ({}: ExchangeRatesTableProps) => {
const { exchangeRates, loading, error } = useExchangeRates();

if (loading) {
return (
<ExchangeRatesContainer>
<ExchangeRatesHeader>Exchange Rates</ExchangeRatesHeader>
<ExchangeRatesLoadingMessage>Loading exchange rates...</ExchangeRatesLoadingMessage>
</ExchangeRatesContainer>
);
}

if (error) {
return (
<ExchangeRatesContainer>
<ExchangeRatesHeader>Exchange Rates</ExchangeRatesHeader>
<ExchangeRatesErrorMessage>
Error loading exchange rates: {error.message}
</ExchangeRatesErrorMessage>
</ExchangeRatesContainer>
);
}

const latestFetchTime = exchangeRates.length > 0 ? exchangeRates[0].fetchedAt : null;

return (
<ExchangeRatesContainer>
<ExchangeRatesHeader>Exchange Rates</ExchangeRatesHeader>

{latestFetchTime && (
<ExchangeRatesCacheInfo>
Last updated: {formatDate(latestFetchTime)}
</ExchangeRatesCacheInfo>
)}

<ExchangeRatesTableContent>
<thead>
<tr>
<ExchangeRatesTableHeader>Country</ExchangeRatesTableHeader>
<ExchangeRatesTableHeader>Currency</ExchangeRatesTableHeader>
<ExchangeRatesTableHeader>Amount</ExchangeRatesTableHeader>
<ExchangeRatesTableHeader>Code</ExchangeRatesTableHeader>
<ExchangeRatesTableHeader>Rate (CZK)</ExchangeRatesTableHeader>
</tr>
</thead>
<tbody>
{exchangeRates.map((rate) => (
<ExchangeRatesTableRow key={rate.id}>
<ExchangeRatesTableCell>{rate.country}</ExchangeRatesTableCell>
<ExchangeRatesTableCell>{rate.currency}</ExchangeRatesTableCell>
<ExchangeRatesTableCell>{rate.amount}</ExchangeRatesTableCell>
<ExchangeRatesTableCell>{rate.code}</ExchangeRatesTableCell>
<ExchangeRatesTableCell>{formatRate(rate.rate)}</ExchangeRatesTableCell>
</ExchangeRatesTableRow>
))}
</tbody>
</ExchangeRatesTableContent>
</ExchangeRatesContainer>
);
};

export default ExchangeRatesTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ExchangeRatesTable';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { gql } from 'graphql-tag';

export const GET_EXCHANGE_RATES = gql`
query GetExchangeRates {
exchangeRates {
id
country
currency
amount
code
rate
fetchedAt
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './exchange-rates.queries';
1 change: 1 addition & 0 deletions fullstack/task/packages/client/src/graphql/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './exchange-rates';
1 change: 1 addition & 0 deletions fullstack/task/packages/client/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useExchangeRates';
30 changes: 30 additions & 0 deletions fullstack/task/packages/client/src/hooks/useExchangeRates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useQuery } from '@apollo/client';
import { GET_EXCHANGE_RATES } from '../graphql';

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

export interface UseExchangeRatesResult {
exchangeRates: ExchangeRate[];
loading: boolean;
error: Error | null;
refetch: () => void;
}

export const useExchangeRates = (): UseExchangeRatesResult => {
const { loading, error, data, refetch } = useQuery(GET_EXCHANGE_RATES);

return {
exchangeRates: data?.exchangeRates || [],
loading,
error: error || null,
refetch,
};
};
2 changes: 1 addition & 1 deletion fullstack/task/packages/client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import App from './App';

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

Expand Down
11 changes: 11 additions & 0 deletions fullstack/task/packages/client/src/utils/dateUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
};
2 changes: 2 additions & 0 deletions fullstack/task/packages/client/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './dateUtils';
export * from './numberUtils';
3 changes: 3 additions & 0 deletions fullstack/task/packages/client/src/utils/numberUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const formatRate = (rate: number): string => {
return rate.toFixed(4);
};
1 change: 1 addition & 0 deletions fullstack/task/packages/server/.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ GQL_PLAYGROUND=true
GQL_INTROSPECTION=true

PORT=4001
CNB_API_URL=https://www.cnb.cz/cs/financni-trhy/devizovy-trh/kurzy-devizoveho-trhu/kurzy-devizoveho-trhu/denni_kurz.txt
16 changes: 15 additions & 1 deletion fullstack/task/packages/server/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,22 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date
"""
scalar DateTime

type ExchangeRate {
id: ID!
createdAtUtc: DateTime!
updatedAtUtc: DateTime
deleteDateUtc: DateTime
version: Int!
country: String!
currency: String!
amount: Float!
code: String!
rate: Float!
fetchedAt: DateTime!
}

type Query {
exchangeRates: String!
exchangeRates: [ExchangeRate!]!
exampleByName(name: String!): Example
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,5 @@ export const omittedEntityMetaColumns: (keyof EntityWithMeta)[] = [
'version',
'updatedAtUtc',
'createdAtUtc',
'deleteDateUtc',
'deleteDateUtc',
'deleteDateUtc'
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsDate, IsNumber, IsString, MinLength } from 'class-validator';
import { Column, Entity } from 'typeorm';
import { EntityWithMeta } from '../common';
import { VAR_CHAR } from './constants';

@ObjectType()
@Entity()
export class ExchangeRate extends EntityWithMeta {
@IsString()
@MinLength(1)
@Field(() => String)
@Column({ ...VAR_CHAR })
country!: string;

@IsString()
@MinLength(1)
@Field(() => String)
@Column({ ...VAR_CHAR })
currency!: string;

@IsNumber()
@Field(() => Number)
@Column({ type: 'int' })
amount!: number;

@IsString()
@Field(() => String)
@Column({ ...VAR_CHAR })
code!: string;

@IsNumber()
@Field(() => Number)
@Column({ type: 'decimal', precision: 10, scale: 4 })
rate!: number;

@IsDate()
@Field(() => Date)
@Column({ type: 'timestamp' })
fetchedAt!: Date;
}
2 changes: 2 additions & 0 deletions fullstack/task/packages/server/src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './example.entity';
export * from './exchange-rate.entity';
export * from './constants';
Loading