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
53 changes: 32 additions & 21 deletions fullstack/task/.pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
76 changes: 38 additions & 38 deletions fullstack/task/packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
{
"name": "client",
"private": true,
"version": "1.0.0",
"dependencies": {
"@apollo/client": "3.7.10",
"graphql": "16.6.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",
"styled-components": "5.3.9",
"vite-tsconfig-paths": "^4.0.7"
},
"devDependencies": {
"@babel/core": "7.23.0",
"@dishboard/eslint-config": "1.0.0",
"@dishboard/jest-config": "1.0.0",
"@types/jest": "28.1.8",
"@types/react": "18.0.17",
"@types/react-dom": "18.0.6",
"@types/styled-components": "^5.1.26",
"@vitejs/plugin-react": "2.1.0",
"cross-env": "7.0.3",
"eslint-import-resolver-typescript": "3.5.3",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-react": "7.31.8",
"jest": "28.1.3",
"ts-jest": "28.0.8",
"vite": "3.1.0"
},
"peerDependencies": {
"eslint": "*",
"prettier": "*",
"typescript": "*"
}
}
{
"name": "client",
"private": true,
"version": "1.0.0",
"dependencies": {
"@apollo/client": "3.7.10",
"graphql": "16.6.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",
"styled-components": "5.3.9",
"vite-tsconfig-paths": "^4.0.7"
},
"devDependencies": {
"@babel/core": "7.23.0",
"@dishboard/eslint-config": "1.0.0",
"@dishboard/jest-config": "1.0.0",
"@types/jest": "28.1.8",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.22",
"@types/styled-components": "^5.1.26",
"@vitejs/plugin-react": "2.1.0",
"cross-env": "7.0.3",
"eslint-import-resolver-typescript": "3.5.3",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-react": "7.31.8",
"jest": "28.1.3",
"ts-jest": "28.0.8",
"vite": "3.1.0"
},
"peerDependencies": {
"eslint": "*",
"prettier": "*",
"typescript": "*"
}
}
4 changes: 3 additions & 1 deletion fullstack/task/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import RatesTable from './components/RatesTable/RatesTable';

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

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

const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;

export const StyledLoader = styled.div`
border: 16px solid #f3f3f3;
border-radius: 50%;
border-top: 16px solid #3498db;
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite; /* Safari */
animation: ${rotate} 2s linear infinite;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { StyledLoader } from './Loader.styles';

export default function Loader() {
return <StyledLoader />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import styled from 'styled-components';

export const RatesWrapper = styled.div`
display: flex;
width: 100%;
height: 100vh;
justify-content: center;
align-items: center;
flex-direction: column;
`;
export const RatesTableContainer = styled.div`
padding: 10px;
width: 80%;
overflow-x: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 8px;
height: 60vh;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { gql, useQuery } from '@apollo/client';
import { useMemo } from 'react';
import { GET_EXCHANGE_RATES } from 'src/graphql/rates/queries';
import { minutesPassed } from 'src/utils';
import Loader from '../Loader/Loader';
import Table from '../Table/Table';
import { rateColumns } from './columns';
import { RatesTableContainer, RatesWrapper } from './RatesTable.styles';

export default function RatesTable() {
const { data, loading } = useQuery(GET_EXCHANGE_RATES);
const rates = data?.exchangeRates.rates;
const lastCachedAt = data?.exchangeRates.lastCachedAt;
const date = new Date(lastCachedAt);
const columns = useMemo(() => {
return rateColumns;
}, []);
return (
<RatesWrapper>
{loading ? (
<Loader />
) : (
<>
<RatesTableContainer>
<Table columns={columns} data={rates} />
</RatesTableContainer>
<p>
{date.toLocaleString()} (Last cached at {minutesPassed(date)})
</p>
</>
)}
</RatesWrapper>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const rateColumns = [
{
header: 'Code',
key: 'code',
},
{
header: 'Currency',
key: 'currency',
},
{
header: 'Amount',
key: 'amount',
},
{
header: 'Rate',
key: 'exchangeRate',
},
{
header: 'Country',
key: 'country',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import styled from 'styled-components';

export const StyledTable = styled.table`
width: 100%;
border-collapse: collapse; /* Removes double borders */
font-family: Arial, sans-serif;
font-size: 14px;
`;

export const TableHeader = styled.th`
background-color: #f8f9fa;
color: #333;
text-align: left;
padding: 12px 15px;
border-bottom: 2px solid #ddd;
text-transform: uppercase;
letter-spacing: 0.05em;
`;

export const TableRow = styled.tr`
&:nth-child(even) {
background-color: #f4f6f9;
}

&:hover {
background-color: #e9ecef;
transition: background-color 0.2s ease;
}
`;

export const TableCell = styled.td`
padding: 12px 15px;
border-bottom: 1px solid #eee;
color: #555;
`;
40 changes: 40 additions & 0 deletions fullstack/task/packages/client/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { StyledTable, TableCell, TableHeader, TableRow } from './Table.styles';

export interface TableProps<TData extends Record<string, any>> {
data: TData[];
columns: TableColumn<TData>[];
}

export interface TableColumn<TData extends Record<string, any>> {
header: string;
key: keyof TData;
render?: (row: TData) => React.ReactNode;
}

export default function Table<TData extends Record<string, any>>({
columns,
data = [],
}: TableProps<TData>) {
return (
<StyledTable>
<thead>
<TableRow>
{columns.map((column) => (
<TableHeader key={column.key as string}>{column.header}</TableHeader>
))}
</TableRow>
</thead>
<tbody>
{data.map((row, rowIndex) => (
<TableRow key={rowIndex}>
{columns.map((column) => (
<TableCell key={column.key as string}>
{column.render ? column.render(row) : row[column.key]}
</TableCell>
))}
</TableRow>
))}
</tbody>
</StyledTable>
);
}
Loading