diff --git a/fullstack/task/jest.config.ts b/fullstack/task/jest.config.ts
index 0050fc39..50c8386f 100644
--- a/fullstack/task/jest.config.ts
+++ b/fullstack/task/jest.config.ts
@@ -1,11 +1,17 @@
import type { Config } from 'jest';
const config: Config = {
- preset: 'ts-jest',
- testEnvironment: 'node',
- transform: {
- '^.+\\.ts?$': 'ts-jest',
- },
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ testMatch: ['**/?(*.)+(spec|test).ts'],
+ transform: {
+ '^.+\\.(t|j)s$': 'ts-jest',
+ },
+ moduleFileExtensions: ['ts', 'js', 'json'],
+ moduleDirectories: ['node_modules', 'src'],
+ collectCoverage: true,
+ collectCoverageFrom: ['src/**/*.ts', '!src/**/main.ts', '!**/migrations/**'],
+ verbose: true,
};
export default config;
diff --git a/fullstack/task/packages/client/package.json b/fullstack/task/packages/client/package.json
index 819a4b34..10aac4b1 100644
--- a/fullstack/task/packages/client/package.json
+++ b/fullstack/task/packages/client/package.json
@@ -4,6 +4,10 @@
"version": "1.0.0",
"dependencies": {
"@apollo/client": "3.7.10",
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@mui/icons-material": "^7.1.0",
+ "@mui/material": "^7.1.0",
"graphql": "16.6.0",
"graphql-tag": "2.12.6",
"graphql-ws": "5.12.0",
diff --git a/fullstack/task/packages/client/src/App.tsx b/fullstack/task/packages/client/src/App.tsx
index f62d2a02..3e6a207c 100644
--- a/fullstack/task/packages/client/src/App.tsx
+++ b/fullstack/task/packages/client/src/App.tsx
@@ -1,5 +1,35 @@
-function App() {
- return
TODO
;
-}
+import React, { useContext } from 'react';
+import { Box, IconButton, Paper, Typography } from '@mui/material';
+import { useTheme } from '@mui/material/styles';
+import Brightness4Icon from '@mui/icons-material/Brightness4';
+import Brightness7Icon from '@mui/icons-material/Brightness7';
+import { ColorModeContext } from './theme';
+import ExchangeRates from './components/ExchangeRates/ExchangeRates';
+
+const App: React.FC = () => {
+ const theme = useTheme();
+ const colorMode = useContext(ColorModeContext);
+
+ return (
+
+
+
+ Exchange Rates
+
+ {theme.palette.mode === 'dark' ? : }
+
+
+
+
+
+
+ );
+};
export default App;
diff --git a/fullstack/task/packages/client/src/components/ExchangeRates/ExchangeRates.tsx b/fullstack/task/packages/client/src/components/ExchangeRates/ExchangeRates.tsx
new file mode 100644
index 00000000..a544c736
--- /dev/null
+++ b/fullstack/task/packages/client/src/components/ExchangeRates/ExchangeRates.tsx
@@ -0,0 +1,125 @@
+import React, { useState, useMemo } from 'react';
+import RefreshIcon from '@mui/icons-material/Refresh';
+import AccessTimeIcon from '@mui/icons-material/AccessTime';
+import {
+ Box,
+ Button,
+ Chip,
+ CircularProgress,
+ IconButton,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TextField,
+ Typography,
+} from '@mui/material';
+import { useExchangeRates } from '../../hooks/useExchangeRates';
+
+const ExchangeRates: React.FC = () => {
+ const { exchangeRates, loading, error, age, refetch } = useExchangeRates();
+ const [search, setSearch] = useState('');
+
+ const filtered = useMemo(() => {
+ if (!search) return exchangeRates;
+ const lower = search.toLowerCase();
+ return exchangeRates.filter(r =>
+ r.currency.toLowerCase().includes(lower) ||
+ r.code.toLowerCase().includes(lower) ||
+ String(r.amount).includes(lower) ||
+ String(r.rate).includes(lower)
+ );
+ }, [exchangeRates, search]);
+
+ // highlight matching text
+ const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const highlight = (value: string | number) => {
+ const str = String(value);
+ if (!search) return str;
+ const parts = str.split(new RegExp(`(${escapeRegExp(search)})`, 'gi'));
+ return parts.map((part, i) =>
+ part.toLowerCase() === search.toLowerCase() ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ );
+ };
+
+ return (
+
+
+
+ }
+ label={`Last fetched: ${age}`}
+ variant="outlined"
+ />
+ refetch({ bypassCache: true })}
+ aria-label="refresh rates"
+ >
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+ {error ? (
+
+
+ Error fetching exchange rates: {error.message}
+
+
+ ) : loading ? (
+
+
+
+ ) : (
+
+
+
+ Currency
+ Code
+ Amount
+ Rate
+
+
+
+ {filtered.length === 0 ? (
+
+
+ No matching records
+
+
+ ) : (
+ filtered.map(r => (
+
+ {highlight(r.currency)}
+ {highlight(r.code)}
+ {highlight(r.amount)}
+ {highlight(r.rate.toFixed(4))}
+
+ ))
+ )}
+
+
+ )}
+
+
+ );
+};
+
+export default ExchangeRates;
diff --git a/fullstack/task/packages/client/src/hooks/useExchangeRates.ts b/fullstack/task/packages/client/src/hooks/useExchangeRates.ts
new file mode 100644
index 00000000..6834dfbe
--- /dev/null
+++ b/fullstack/task/packages/client/src/hooks/useExchangeRates.ts
@@ -0,0 +1,59 @@
+import { gql, useQuery } from '@apollo/client';
+import { useEffect, useMemo, useState } from 'react';
+
+export interface ExchangeRate {
+ id: string;
+ currency: string;
+ code: string;
+ amount: number;
+ rate: number;
+ createdAtUtc: string;
+}
+
+const GET_EXCHANGE_RATES = gql`
+ query GetExchangeRates($bypassCache: Boolean!) {
+ exchangeRates(bypassCache: $bypassCache) {
+ id
+ currency
+ code
+ amount
+ rate
+ createdAtUtc
+ }
+ }
+`;
+
+export function useExchangeRates() {
+ const { data, loading, error, refetch } = useQuery<
+ { exchangeRates: ExchangeRate[] },
+ { bypassCache: boolean }
+ >(GET_EXCHANGE_RATES, {
+ variables: { bypassCache: false },
+ pollInterval: 30_000,
+ fetchPolicy: 'cache-first',
+ notifyOnNetworkStatusChange: true,
+ });
+
+ const [lastFetched, setLastFetched] = useState(null);
+
+ useEffect(() => {
+ const ts = data?.exchangeRates?.[0]?.createdAtUtc;
+ if (ts) setLastFetched(new Date(ts));
+ }, [data]);
+
+ const age = useMemo(() => {
+ if (!lastFetched) return '';
+ const secs = Math.floor((Date.now() - lastFetched.getTime()) / 1000);
+ if (secs < 60) return `${secs} second${secs !== 1 ? 's' : ''} ago`;
+ const mins = Math.floor(secs / 60);
+ return `${mins} minute${mins !== 1 ? 's' : ''} ago`;
+ }, [lastFetched]);
+
+ return {
+ exchangeRates: data?.exchangeRates ?? [],
+ loading,
+ error,
+ age,
+ refetch,
+ };
+}
diff --git a/fullstack/task/packages/client/src/main.tsx b/fullstack/task/packages/client/src/main.tsx
index ef5f39c2..61101bd9 100644
--- a/fullstack/task/packages/client/src/main.tsx
+++ b/fullstack/task/packages/client/src/main.tsx
@@ -1,17 +1,44 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
+import { ThemeProvider, CssBaseline, createTheme } from '@mui/material';
+import { getDesignTokens, ColorModeContext } from './theme';
import App from './App';
-const client = new ApolloClient({
- uri: 'http://localhost:4000/graphql',
+function Root() {
+ const [mode, setMode] = React.useState<'light' | 'dark'>('dark');
+
+ const colorMode = React.useMemo(() => ({
+ toggleColorMode: () => {
+ setMode(prev => (prev === 'light' ? 'dark' : 'light'));
+ }
+ }), []);
+
+ const theme = React.useMemo(
+ () => createTheme(getDesignTokens(mode)),
+ [mode]
+ );
+
+ const client = new ApolloClient({
+ // Ilkin: better come from env. Also port 4000 was not working locally
+ uri: 'http://localhost:4001/graphql',
cache: new InMemoryCache(),
-});
+ });
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
-ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
-
-
-
-
-
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
);
diff --git a/fullstack/task/packages/client/src/theme.ts b/fullstack/task/packages/client/src/theme.ts
new file mode 100644
index 00000000..fe60a2ed
--- /dev/null
+++ b/fullstack/task/packages/client/src/theme.ts
@@ -0,0 +1,23 @@
+import React from 'react';
+import { PaletteMode, ThemeOptions } from '@mui/material';
+import { deepOrange, teal } from '@mui/material/colors';
+
+export const ColorModeContext = React.createContext({
+ toggleColorMode: () => {},
+});
+
+export const getDesignTokens = (mode: PaletteMode): ThemeOptions => ({
+ palette: {
+ mode,
+ primary: mode === 'light' ? teal : deepOrange,
+ },
+ components: {
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ textTransform: 'none',
+ },
+ },
+ },
+ },
+});
diff --git a/fullstack/task/packages/server/package.json b/fullstack/task/packages/server/package.json
index 09d138c0..2114ae80 100644
--- a/fullstack/task/packages/server/package.json
+++ b/fullstack/task/packages/server/package.json
@@ -6,7 +6,10 @@
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
- "serve": "node dist/main"
+ "serve": "node dist/main",
+ "test": "jest --passWithNoTests",
+ "test:watch": "jest --watch",
+ "test:cov": "jest --coverage"
},
"dependencies": {
"@nestjs/apollo": "10.1.3",
diff --git a/fullstack/task/packages/server/schema.gql b/fullstack/task/packages/server/schema.gql
index 2453a887..828953a1 100644
--- a/fullstack/task/packages/server/schema.gql
+++ b/fullstack/task/packages/server/schema.gql
@@ -2,14 +2,16 @@
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
-type Example {
+type ExchangeRate {
id: ID!
createdAtUtc: DateTime!
updatedAtUtc: DateTime
deleteDateUtc: DateTime
version: Int!
- name: String!
- value: String!
+ currency: String!
+ code: String!
+ rate: Float!
+ amount: Int!
}
"""
@@ -17,8 +19,18 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date
"""
scalar DateTime
+type Example {
+ id: ID!
+ createdAtUtc: DateTime!
+ updatedAtUtc: DateTime
+ deleteDateUtc: DateTime
+ version: Int!
+ name: String!
+ value: String!
+}
+
type Query {
- exchangeRates: String!
+ exchangeRates(bypassCache: Boolean = false): [ExchangeRate!]!
exampleByName(name: String!): Example
}
diff --git a/fullstack/task/packages/server/src/entities/exchange-rate.entity.ts b/fullstack/task/packages/server/src/entities/exchange-rate.entity.ts
new file mode 100644
index 00000000..2d25c743
--- /dev/null
+++ b/fullstack/task/packages/server/src/entities/exchange-rate.entity.ts
@@ -0,0 +1,34 @@
+import { Field, Float, Int, ObjectType } from '@nestjs/graphql';
+import { Column, Entity } from 'typeorm';
+import { IsInt, IsNumber, IsString, MinLength } from 'class-validator';
+import { EntityWithMeta } from '../common';
+import { VAR_CHAR } from './constants';
+
+@ObjectType()
+@Entity()
+export class ExchangeRate extends EntityWithMeta {
+ @IsString()
+ @MinLength(1)
+ @Field(() => String)
+ @Column({ ...VAR_CHAR })
+ public currency!: string;
+
+ @IsString()
+ @MinLength(1)
+ @Field(() => String)
+ @Column({ ...VAR_CHAR })
+ public code!: string;
+
+ @IsNumber()
+ @Field(() => Float)
+ @Column('decimal')
+ public rate!: number;
+
+ @IsInt()
+ @Field(() => Int)
+ @Column('int')
+ public amount!: number;
+
+ @Field(() => Date)
+ public createdAtUtc!: Date;
+}
diff --git a/fullstack/task/packages/server/src/entities/index.ts b/fullstack/task/packages/server/src/entities/index.ts
index 6b536a4a..a0486afb 100644
--- a/fullstack/task/packages/server/src/entities/index.ts
+++ b/fullstack/task/packages/server/src/entities/index.ts
@@ -1 +1,2 @@
+export * from './exchange-rate.entity';
export * from './example.entity';
diff --git a/fullstack/task/packages/server/src/migrations/1748367064065-create-exchange-rate.ts b/fullstack/task/packages/server/src/migrations/1748367064065-create-exchange-rate.ts
new file mode 100644
index 00000000..a72c82a1
--- /dev/null
+++ b/fullstack/task/packages/server/src/migrations/1748367064065-create-exchange-rate.ts
@@ -0,0 +1,26 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class createExchangeRate1695820000000 implements MigrationInterface {
+ name = 'createExchangeRate1748367064065';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`
+ CREATE TABLE "exchange_rate" (
+ "id" uuid NOT NULL DEFAULT uuid_generate_v4(),
+ "createdAtUtc" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
+ "updatedAtUtc" TIMESTAMP WITH TIME ZONE DEFAULT now(),
+ "deleteDateUtc" TIMESTAMP WITH TIME ZONE,
+ "version" integer NOT NULL,
+ "currency" character varying(255) NOT NULL,
+ "code" character varying(255) NOT NULL,
+ "rate" decimal NOT NULL,
+ "amount" integer NOT NULL,
+ CONSTRAINT "PK_exchange_rate_id" PRIMARY KEY ("id")
+ )
+ `);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`DROP TABLE "exchange_rate"`);
+ }
+}
diff --git a/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.resolver.spec.ts b/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.resolver.spec.ts
new file mode 100644
index 00000000..dedeabda
--- /dev/null
+++ b/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.resolver.spec.ts
@@ -0,0 +1,36 @@
+import { ExchangeRateResolver } from '../exchange-rate.resolver';
+import { ExchangeRateService } from '../exchange-rate.service';
+import { ExchangeRate } from '../../../entities/exchange-rate.entity';
+
+describe('ExchangeRateResolver (unit)', () => {
+ let resolver: ExchangeRateResolver;
+ let service: jest.Mocked;
+
+ beforeEach(() => {
+ service = {
+ getExchangeRates: jest.fn().mockResolvedValue([
+ Object.assign(new ExchangeRate(), {
+ id: 'abc',
+ currency: 'Test',
+ code: 'TST',
+ amount: 1,
+ rate: 1.0,
+ createdAtUtc: new Date(),
+ }),
+ ]),
+ } as any;
+
+ resolver = new ExchangeRateResolver(service);
+ });
+
+ it('calls getExchangeRates with bypassCache=false by default', async () => {
+ const result = await resolver.exchangeRates(false);
+ expect(result).toHaveLength(1);
+ expect(service.getExchangeRates).toHaveBeenCalledWith(false);
+ });
+
+ it('passes the bypassCache argument through', async () => {
+ await resolver.exchangeRates(true);
+ expect(service.getExchangeRates).toHaveBeenCalledWith(true);
+ });
+});
diff --git a/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.service.spec.ts b/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.service.spec.ts
new file mode 100644
index 00000000..2a0cda98
--- /dev/null
+++ b/fullstack/task/packages/server/src/services/exchange-rate/__tests__/exchange-rate.service.spec.ts
@@ -0,0 +1,78 @@
+import axios from 'axios';
+import { InternalServerErrorException } from '@nestjs/common';
+import { ExchangeRateService } from '../exchange-rate.service';
+import { ExchangeRate } from '../../../entities/exchange-rate.entity';
+import { MoreThan, FindOperator, Repository } from 'typeorm';
+
+jest.mock('axios');
+const mockedAxios = axios as jest.Mocked;
+
+describe('ExchangeRateService (unit)', () => {
+ let service: ExchangeRateService;
+ let repo: jest.Mocked, 'find' | 'clear' | 'save' | 'create'>>;
+
+ beforeEach(() => {
+ repo = {
+ find: jest.fn(),
+ clear: jest.fn(),
+ save: jest.fn(),
+ create: jest.fn(dto => Object.assign(new ExchangeRate(), dto)), // <-- now defined
+ } as any;
+
+ service = new ExchangeRateService(repo as any);
+ });
+
+ it('returns cached rates if they exist and are fresh', async () => {
+ const now = new Date();
+ const cached = [
+ Object.assign(new ExchangeRate(), {
+ id: '1',
+ currency: 'Foo',
+ code: 'FOO',
+ amount: 1,
+ rate: 1.23,
+ createdAtUtc: now,
+ }),
+ ];
+ repo.find.mockResolvedValue(cached);
+
+ const result = await service.getExchangeRates(false);
+ expect(result).toBe(cached);
+ expect(mockedAxios.get).not.toHaveBeenCalled();
+ expect(repo.find).toHaveBeenCalledWith({
+ where: { createdAtUtc: expect.any(FindOperator) },
+ });
+ });
+
+ it('fetches, parses, clears, saves and returns new rates when cache is stale', async () => {
+ repo.find.mockResolvedValue([]);
+
+ const sampleTxt = [
+ 'Header line 1',
+ 'Header line 2',
+ 'Country|USD|1|USD|23,45',
+ 'Country|EUR|1|EUR|25,60',
+ ].join('\n');
+ mockedAxios.get.mockResolvedValue({ data: sampleTxt });
+
+ repo.clear.mockResolvedValue(undefined);
+ repo.save.mockResolvedValue([] as any);
+
+ const rates = await service.getExchangeRates(false);
+
+ expect(repo.clear).toHaveBeenCalled();
+ expect(repo.save).toHaveBeenCalled();
+ expect(rates).toHaveLength(2);
+ expect(rates[0].code).toBe('USD');
+ expect(rates[1].code).toBe('EUR');
+ });
+
+ it('throws InternalServerErrorException when the HTTP fetch fails', async () => {
+ repo.find.mockResolvedValue([]);
+ mockedAxios.get.mockRejectedValue(new Error('network down'));
+
+ await expect(service.getExchangeRates(false)).rejects.toBeInstanceOf(
+ InternalServerErrorException,
+ );
+ });
+});
diff --git a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.module.ts b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.module.ts
index d972c6e4..6b78ad78 100644
--- a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.module.ts
+++ b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.module.ts
@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { ExchangeRate } from '../../entities/exchange-rate.entity';
import { ExchangeRateService } from './exchange-rate.service';
import { ExchangeRateResolver } from './exchange-rate.resolver';
@Module({
- imports: [],
+ imports: [TypeOrmModule.forFeature([ExchangeRate])],
providers: [ExchangeRateService, ExchangeRateResolver],
- exports: [ExchangeRateService],
})
export class ExchangeRateModule {}
diff --git a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.resolver.ts b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.resolver.ts
index 39087b66..2ab42621 100644
--- a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.resolver.ts
+++ b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.resolver.ts
@@ -1,13 +1,20 @@
-import { Query, Resolver } from '@nestjs/graphql';
+import { Args, Query, Resolver } from '@nestjs/graphql';
+import { ExchangeRate } from '../../entities/exchange-rate.entity';
import { ExchangeRateService } from './exchange-rate.service';
-@Resolver()
+@Resolver(() => ExchangeRate)
export class ExchangeRateResolver {
constructor(private readonly exchangeRateService: ExchangeRateService) {}
- // TODO: Implement a GraphQL Query that returns the exchange rates
- @Query(() => String)
- async exchangeRates(): Promise {
- return 'Hello';
+ @Query(() => [ExchangeRate])
+ async exchangeRates(
+ @Args('bypassCache', {
+ type: () => Boolean,
+ nullable: true,
+ defaultValue: false,
+ })
+ bypassCache: boolean
+ ): Promise {
+ return this.exchangeRateService.getExchangeRates(bypassCache);
}
}
diff --git a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.service.ts b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.service.ts
index d9ee80bb..473fe471 100644
--- a/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.service.ts
+++ b/fullstack/task/packages/server/src/services/exchange-rate/exchange-rate.service.ts
@@ -1,11 +1,63 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, InternalServerErrorException } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { MoreThan, Repository } from 'typeorm';
+import axios from 'axios';
+import { ExchangeRate } from '../../entities/exchange-rate.entity';
+
+const CNB_EXCHANGE_URL =
+ 'https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/daily.txt?date=';
@Injectable()
export class ExchangeRateService {
- public getExchangeRates = async () => {
- // TODO: Implement the fetching and parsing of the exchange rates.
- // Use this method in the resolver.
+ constructor(
+ @InjectRepository(ExchangeRate)
+ private readonly exchangeRateRepo: Repository
+ ) {}
+
+ async getExchangeRates(bypassCache = false): Promise {
+ if (!bypassCache) {
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
+ const cached = await this.exchangeRateRepo.find({
+ where: { createdAtUtc: MoreThan(fiveMinutesAgo) },
+ });
+ if (cached.length > 0) {
+ return cached;
+ }
+ }
+
+ const now = new Date();
+ const day = String(now.getDate()).padStart(2, '0');
+ const month = String(now.getMonth() + 1).padStart(2, '0');
+ const year = now.getFullYear();
+ const dateStr = `${day}.${month}.${year}`;
+ const url = `${CNB_EXCHANGE_URL}${dateStr}`;
+
+ let raw: string;
+ try {
+ const { data } = await axios.get(url);
+ raw = data;
+ } catch (err: any) {
+ throw new InternalServerErrorException(
+ `Failed to fetch exchange rates from CNB: ${err.message}`
+ );
+ }
+
+ const lines = raw.split('\n').slice(2);
+ await this.exchangeRateRepo.clear();
+
+ const rates = lines
+ .filter((line) => line.trim())
+ .map((line) => {
+ const [, currency, amount, code, rate] = line.split('|');
+ return this.exchangeRateRepo.create({
+ currency,
+ code,
+ amount: parseInt(amount, 10),
+ rate: parseFloat(rate.replace(',', '.')),
+ });
+ });
- return [];
- };
+ await this.exchangeRateRepo.save(rates);
+ return rates;
+ }
}
diff --git a/fullstack/task/yarn.lock b/fullstack/task/yarn.lock
index cb18ed2c..b790085b 100644
--- a/fullstack/task/yarn.lock
+++ b/fullstack/task/yarn.lock
@@ -300,6 +300,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/code-frame@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/code-frame@npm:7.27.1"
+ dependencies:
+ "@babel/helper-validator-identifier": ^7.27.1
+ js-tokens: ^4.0.0
+ picocolors: ^1.1.1
+ checksum: 5874edc5d37406c4a0bb14cf79c8e51ad412fb0423d176775ac14fc0259831be1bf95bdda9c2aa651126990505e09a9f0ed85deaa99893bc316d2682c5115bdc
+ languageName: node
+ linkType: hard
+
"@babel/compat-data@npm:^7.22.9":
version: 7.22.20
resolution: "@babel/compat-data@npm:7.22.20"
@@ -342,6 +353,19 @@ __metadata:
languageName: node
linkType: hard
+"@babel/generator@npm:^7.27.3":
+ version: 7.27.3
+ resolution: "@babel/generator@npm:7.27.3"
+ dependencies:
+ "@babel/parser": ^7.27.3
+ "@babel/types": ^7.27.3
+ "@jridgewell/gen-mapping": ^0.3.5
+ "@jridgewell/trace-mapping": ^0.3.25
+ jsesc: ^3.0.2
+ checksum: c0b1b399ff62fa0f1903679ab2b088fd4312c33154c0ae78228094c196ecf53ce8e525b8f5e537ac3117ff9a49cdf7b3640f129114908dfadc6541853f3747a2
+ languageName: node
+ linkType: hard
+
"@babel/helper-annotate-as-pure@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-annotate-as-pure@npm:7.22.5"
@@ -399,6 +423,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-module-imports@npm:^7.16.7":
+ version: 7.27.1
+ resolution: "@babel/helper-module-imports@npm:7.27.1"
+ dependencies:
+ "@babel/traverse": ^7.27.1
+ "@babel/types": ^7.27.1
+ checksum: 92d01c71c0e4aacdc2babce418a9a1a27a8f7d770a210ffa0f3933f321befab18b655bc1241bebc40767516731de0b85639140c42e45a8210abe1e792f115b28
+ languageName: node
+ linkType: hard
+
"@babel/helper-module-transforms@npm:^7.23.0":
version: 7.23.0
resolution: "@babel/helper-module-transforms@npm:7.23.0"
@@ -446,6 +480,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-string-parser@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-string-parser@npm:7.27.1"
+ checksum: 0a8464adc4b39b138aedcb443b09f4005d86207d7126e5e079177e05c3116107d856ec08282b365e9a79a9872f40f4092a6127f8d74c8a01c1ef789dacfc25d6
+ languageName: node
+ linkType: hard
+
"@babel/helper-validator-identifier@npm:^7.22.20":
version: 7.22.20
resolution: "@babel/helper-validator-identifier@npm:7.22.20"
@@ -453,6 +494,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-validator-identifier@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-validator-identifier@npm:7.27.1"
+ checksum: 3c7e8391e59d6c85baeefe9afb86432f2ab821c6232b00ea9082a51d3e7e95a2f3fb083d74dc1f49ac82cf238e1d2295dafcb001f7b0fab479f3f56af5eaaa47
+ languageName: node
+ linkType: hard
+
"@babel/helper-validator-option@npm:^7.22.15":
version: 7.22.15
resolution: "@babel/helper-validator-option@npm:7.22.15"
@@ -491,6 +539,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.3":
+ version: 7.27.3
+ resolution: "@babel/parser@npm:7.27.3"
+ dependencies:
+ "@babel/types": ^7.27.3
+ bin:
+ parser: ./bin/babel-parser.js
+ checksum: aef2cfd154e47a639615d173d3f05a8ce8007fcc5a0ade013c953adee71a8bc19465a147e060cc67388fd748b62a3b42bf3b5cc3e83d4f8add526b3b722e2231
+ languageName: node
+ linkType: hard
+
"@babel/plugin-syntax-async-generators@npm:^7.8.4":
version: 7.8.4
resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4"
@@ -712,6 +771,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.27.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
+ version: 7.27.3
+ resolution: "@babel/runtime@npm:7.27.3"
+ checksum: 4eadd745756c9de7a9bba1b2b138b8b99f34aa544cf69c7e6fd1d2a120ba6d31580e93f00657145add7a8945db8ffea5ef7fdbe18f0f0f8e82f2929cb56a0d61
+ languageName: node
+ linkType: hard
+
"@babel/template@npm:^7.22.15, @babel/template@npm:^7.3.3":
version: 7.22.15
resolution: "@babel/template@npm:7.22.15"
@@ -723,6 +789,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/template@npm:^7.27.2":
+ version: 7.27.2
+ resolution: "@babel/template@npm:7.27.2"
+ dependencies:
+ "@babel/code-frame": ^7.27.1
+ "@babel/parser": ^7.27.2
+ "@babel/types": ^7.27.1
+ checksum: ff5628bc066060624afd970616090e5bba91c6240c2e4b458d13267a523572cbfcbf549391eec8217b94b064cf96571c6273f0c04b28a8567b96edc675c28e27
+ languageName: node
+ linkType: hard
+
"@babel/traverse@npm:^7.23.0, @babel/traverse@npm:^7.4.5, @babel/traverse@npm:^7.7.2":
version: 7.23.0
resolution: "@babel/traverse@npm:7.23.0"
@@ -741,6 +818,21 @@ __metadata:
languageName: node
linkType: hard
+"@babel/traverse@npm:^7.27.1":
+ version: 7.27.3
+ resolution: "@babel/traverse@npm:7.27.3"
+ dependencies:
+ "@babel/code-frame": ^7.27.1
+ "@babel/generator": ^7.27.3
+ "@babel/parser": ^7.27.3
+ "@babel/template": ^7.27.2
+ "@babel/types": ^7.27.3
+ debug: ^4.3.1
+ globals: ^11.1.0
+ checksum: 914402382921b796b740f7c90d56ba130ffd5eeda8d18dc82f1243a1a510ff21a26d5b713df08c8e8aad2ffc969ce4624cee309406d69bcee8efa350483688c9
+ languageName: node
+ linkType: hard
+
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3":
version: 7.23.0
resolution: "@babel/types@npm:7.23.0"
@@ -752,6 +844,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3":
+ version: 7.27.3
+ resolution: "@babel/types@npm:7.27.3"
+ dependencies:
+ "@babel/helper-string-parser": ^7.27.1
+ "@babel/helper-validator-identifier": ^7.27.1
+ checksum: f0d43c0231f3ebc118480e149292dcd92ea128e2650285ced99ff2e5610db2171305f59aa07406ba0cb36af8e4331a53a69576d6b0c3f3176144dd3ad514b9ae
+ languageName: node
+ linkType: hard
+
"@bcoe/v8-coverage@npm:^0.2.3":
version: 0.2.3
resolution: "@bcoe/v8-coverage@npm:0.2.3"
@@ -802,6 +904,45 @@ __metadata:
languageName: unknown
linkType: soft
+"@emotion/babel-plugin@npm:^11.13.5":
+ version: 11.13.5
+ resolution: "@emotion/babel-plugin@npm:11.13.5"
+ dependencies:
+ "@babel/helper-module-imports": ^7.16.7
+ "@babel/runtime": ^7.18.3
+ "@emotion/hash": ^0.9.2
+ "@emotion/memoize": ^0.9.0
+ "@emotion/serialize": ^1.3.3
+ babel-plugin-macros: ^3.1.0
+ convert-source-map: ^1.5.0
+ escape-string-regexp: ^4.0.0
+ find-root: ^1.1.0
+ source-map: ^0.5.7
+ stylis: 4.2.0
+ checksum: c41df7e6c19520e76d1939f884be878bf88b5ba00bd3de9d05c5b6c5baa5051686ab124d7317a0645de1b017b574d8139ae1d6390ec267fbe8e85a5252afb542
+ languageName: node
+ linkType: hard
+
+"@emotion/cache@npm:^11.13.5, @emotion/cache@npm:^11.14.0":
+ version: 11.14.0
+ resolution: "@emotion/cache@npm:11.14.0"
+ dependencies:
+ "@emotion/memoize": ^0.9.0
+ "@emotion/sheet": ^1.4.0
+ "@emotion/utils": ^1.4.2
+ "@emotion/weak-memoize": ^0.4.0
+ stylis: 4.2.0
+ checksum: 0a81591541ea43bc7851742e6444b7800d72e98006f94e775ae6ea0806662d14e0a86ff940f5f19d33b4bb2c427c882aa65d417e7322a6e0d5f20fe65ed920c9
+ languageName: node
+ linkType: hard
+
+"@emotion/hash@npm:^0.9.2":
+ version: 0.9.2
+ resolution: "@emotion/hash@npm:0.9.2"
+ checksum: 379bde2830ccb0328c2617ec009642321c0e009a46aa383dfbe75b679c6aea977ca698c832d225a893901f29d7b3eef0e38cf341f560f6b2b56f1ff23c172387
+ languageName: node
+ linkType: hard
+
"@emotion/is-prop-valid@npm:^1.1.0":
version: 1.2.1
resolution: "@emotion/is-prop-valid@npm:1.2.1"
@@ -811,6 +952,15 @@ __metadata:
languageName: node
linkType: hard
+"@emotion/is-prop-valid@npm:^1.3.0":
+ version: 1.3.1
+ resolution: "@emotion/is-prop-valid@npm:1.3.1"
+ dependencies:
+ "@emotion/memoize": ^0.9.0
+ checksum: fe6549d54f389e1a17cb02d832af7ee85fb6ea126fc18d02ca47216e8ff19332c1983f4a0ba68602cfcd3b325ffd4ebf0b2d0c6270f1e7e6fe3fca4ba7741e1a
+ languageName: node
+ linkType: hard
+
"@emotion/memoize@npm:^0.8.1":
version: 0.8.1
resolution: "@emotion/memoize@npm:0.8.1"
@@ -818,6 +968,74 @@ __metadata:
languageName: node
linkType: hard
+"@emotion/memoize@npm:^0.9.0":
+ version: 0.9.0
+ resolution: "@emotion/memoize@npm:0.9.0"
+ checksum: 038132359397348e378c593a773b1148cd0cf0a2285ffd067a0f63447b945f5278860d9de718f906a74c7c940ba1783ac2ca18f1c06a307b01cc0e3944e783b1
+ languageName: node
+ linkType: hard
+
+"@emotion/react@npm:^11.14.0":
+ version: 11.14.0
+ resolution: "@emotion/react@npm:11.14.0"
+ dependencies:
+ "@babel/runtime": ^7.18.3
+ "@emotion/babel-plugin": ^11.13.5
+ "@emotion/cache": ^11.14.0
+ "@emotion/serialize": ^1.3.3
+ "@emotion/use-insertion-effect-with-fallbacks": ^1.2.0
+ "@emotion/utils": ^1.4.2
+ "@emotion/weak-memoize": ^0.4.0
+ hoist-non-react-statics: ^3.3.1
+ peerDependencies:
+ react: ">=16.8.0"
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 3cf023b11d132b56168713764d6fced8e5a1f0687dfe0caa2782dfd428c8f9e30f9826a919965a311d87b523cd196722aaf75919cd0f6bd0fd57f8a6a0281500
+ languageName: node
+ linkType: hard
+
+"@emotion/serialize@npm:^1.3.3":
+ version: 1.3.3
+ resolution: "@emotion/serialize@npm:1.3.3"
+ dependencies:
+ "@emotion/hash": ^0.9.2
+ "@emotion/memoize": ^0.9.0
+ "@emotion/unitless": ^0.10.0
+ "@emotion/utils": ^1.4.2
+ csstype: ^3.0.2
+ checksum: 510331233767ae4e09e925287ca2c7269b320fa1d737ea86db5b3c861a734483ea832394c0c1fe5b21468fe335624a75e72818831d303ba38125f54f44ba02e7
+ languageName: node
+ linkType: hard
+
+"@emotion/sheet@npm:^1.4.0":
+ version: 1.4.0
+ resolution: "@emotion/sheet@npm:1.4.0"
+ checksum: eeb1212e3289db8e083e72e7e401cd6d1a84deece87e9ce184f7b96b9b5dbd6f070a89057255a6ff14d9865c3ce31f27c39248a053e4cdd875540359042586b4
+ languageName: node
+ linkType: hard
+
+"@emotion/styled@npm:^11.14.0":
+ version: 11.14.0
+ resolution: "@emotion/styled@npm:11.14.0"
+ dependencies:
+ "@babel/runtime": ^7.18.3
+ "@emotion/babel-plugin": ^11.13.5
+ "@emotion/is-prop-valid": ^1.3.0
+ "@emotion/serialize": ^1.3.3
+ "@emotion/use-insertion-effect-with-fallbacks": ^1.2.0
+ "@emotion/utils": ^1.4.2
+ peerDependencies:
+ "@emotion/react": ^11.0.0-rc.0
+ react: ">=16.8.0"
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 9c1b842e942e69fb6037d1ab161046d2bcfeff95fd2ccfdab30acaaf6b89dc07b14bb00f8cc8ec14df11e6746c8e4e1d781bc54d10bd739aab44966ded64d4fb
+ languageName: node
+ linkType: hard
+
"@emotion/stylis@npm:^0.8.4":
version: 0.8.5
resolution: "@emotion/stylis@npm:0.8.5"
@@ -825,6 +1043,13 @@ __metadata:
languageName: node
linkType: hard
+"@emotion/unitless@npm:^0.10.0":
+ version: 0.10.0
+ resolution: "@emotion/unitless@npm:0.10.0"
+ checksum: d79346df31a933e6d33518e92636afeb603ce043f3857d0a39a2ac78a09ef0be8bedff40130930cb25df1beeee12d96ee38613963886fa377c681a89970b787c
+ languageName: node
+ linkType: hard
+
"@emotion/unitless@npm:^0.7.4":
version: 0.7.5
resolution: "@emotion/unitless@npm:0.7.5"
@@ -832,6 +1057,29 @@ __metadata:
languageName: node
linkType: hard
+"@emotion/use-insertion-effect-with-fallbacks@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.2.0"
+ peerDependencies:
+ react: ">=16.8.0"
+ checksum: 8ff6aec7f2924526ff8c8f8f93d4b8236376e2e12c435314a18c9a373016e24dfdf984e82bbc83712b8e90ff4783cd765eb39fc7050d1a43245e5728740ddd71
+ languageName: node
+ linkType: hard
+
+"@emotion/utils@npm:^1.4.2":
+ version: 1.4.2
+ resolution: "@emotion/utils@npm:1.4.2"
+ checksum: 04cf76849c6401205c058b82689fd0ec5bf501aed6974880fe9681a1d61543efb97e848f4c38664ac4a9068c7ad2d1cb84f73bde6cf95f1208aa3c28e0190321
+ languageName: node
+ linkType: hard
+
+"@emotion/weak-memoize@npm:^0.4.0":
+ version: 0.4.0
+ resolution: "@emotion/weak-memoize@npm:0.4.0"
+ checksum: db5da0e89bd752c78b6bd65a1e56231f0abebe2f71c0bd8fc47dff96408f7065b02e214080f99924f6a3bfe7ee15afc48dad999d76df86b39b16e513f7a94f52
+ languageName: node
+ linkType: hard
+
"@esbuild/android-arm@npm:0.15.18":
version: 0.15.18
resolution: "@esbuild/android-arm@npm:0.15.18"
@@ -1313,6 +1561,17 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/gen-mapping@npm:^0.3.5":
+ version: 0.3.8
+ resolution: "@jridgewell/gen-mapping@npm:0.3.8"
+ dependencies:
+ "@jridgewell/set-array": ^1.2.1
+ "@jridgewell/sourcemap-codec": ^1.4.10
+ "@jridgewell/trace-mapping": ^0.3.24
+ checksum: c0687b5227461717aa537fe71a42e356bcd1c43293b3353796a148bf3b0d6f59109def46c22f05b60e29a46f19b2e4676d027959a7c53a6c92b9d5b0d87d0420
+ languageName: node
+ linkType: hard
+
"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0":
version: 3.1.1
resolution: "@jridgewell/resolve-uri@npm:3.1.1"
@@ -1327,6 +1586,13 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/set-array@npm:^1.2.1":
+ version: 1.2.1
+ resolution: "@jridgewell/set-array@npm:1.2.1"
+ checksum: 832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10
+ languageName: node
+ linkType: hard
+
"@jridgewell/source-map@npm:^0.3.3":
version: 0.3.5
resolution: "@jridgewell/source-map@npm:0.3.5"
@@ -1364,6 +1630,16 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25":
+ version: 0.3.25
+ resolution: "@jridgewell/trace-mapping@npm:0.3.25"
+ dependencies:
+ "@jridgewell/resolve-uri": ^3.1.0
+ "@jridgewell/sourcemap-codec": ^1.4.14
+ checksum: 9d3c40d225e139987b50c48988f8717a54a8c994d8a948ee42e1412e08988761d0754d7d10b803061cc3aebf35f92a5dbbab493bd0e1a9ef9e89a2130e83ba34
+ languageName: node
+ linkType: hard
+
"@lukeed/csprng@npm:^1.0.0":
version: 1.1.0
resolution: "@lukeed/csprng@npm:1.1.0"
@@ -1390,6 +1666,167 @@ __metadata:
languageName: node
linkType: hard
+"@mui/core-downloads-tracker@npm:^7.1.0":
+ version: 7.1.0
+ resolution: "@mui/core-downloads-tracker@npm:7.1.0"
+ checksum: bf625749ef497f1707d12ef0ab85db0da1417c47df39ef91207de283c8fcd4ef1aab4ce2c4a1f1db532e88a9ba8133d0cc4348cb83a28692047b74fb9952a151
+ languageName: node
+ linkType: hard
+
+"@mui/icons-material@npm:^7.1.0":
+ version: 7.1.0
+ resolution: "@mui/icons-material@npm:7.1.0"
+ dependencies:
+ "@babel/runtime": ^7.27.1
+ peerDependencies:
+ "@mui/material": ^7.1.0
+ "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 582d1bdaffa7f712598a537b6852ec12f87fe576239e3bc743c72a9b470fb9aa67b3655b5b70ed69893eccce91d80e8ecc6c3356b1b42edf217f4bc7e2256620
+ languageName: node
+ linkType: hard
+
+"@mui/material@npm:^7.1.0":
+ version: 7.1.0
+ resolution: "@mui/material@npm:7.1.0"
+ dependencies:
+ "@babel/runtime": ^7.27.1
+ "@mui/core-downloads-tracker": ^7.1.0
+ "@mui/system": ^7.1.0
+ "@mui/types": ^7.4.2
+ "@mui/utils": ^7.1.0
+ "@popperjs/core": ^2.11.8
+ "@types/react-transition-group": ^4.4.12
+ clsx: ^2.1.1
+ csstype: ^3.1.3
+ prop-types: ^15.8.1
+ react-is: ^19.1.0
+ react-transition-group: ^4.4.5
+ peerDependencies:
+ "@emotion/react": ^11.5.0
+ "@emotion/styled": ^11.3.0
+ "@mui/material-pigment-css": ^7.1.0
+ "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/react":
+ optional: true
+ "@emotion/styled":
+ optional: true
+ "@mui/material-pigment-css":
+ optional: true
+ "@types/react":
+ optional: true
+ checksum: 9522571d7ba2ea87b9c208c0b28ea975b28211bb66129ebe13bf6f13854dc2e1f0f853e70a8bcbf6b68693a347f4971c40bc5d247f8c4d9c66ab67d065e7cdbf
+ languageName: node
+ linkType: hard
+
+"@mui/private-theming@npm:^7.1.0":
+ version: 7.1.0
+ resolution: "@mui/private-theming@npm:7.1.0"
+ dependencies:
+ "@babel/runtime": ^7.27.1
+ "@mui/utils": ^7.1.0
+ prop-types: ^15.8.1
+ peerDependencies:
+ "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 639888a7f69eb42ee673f66c7c62bd9414cc02254456865417312d7fc3245e723b2ea92902258f7c6a10c6949c0725da09026d4f96cea259d5c9f49762473f8a
+ languageName: node
+ linkType: hard
+
+"@mui/styled-engine@npm:^7.1.0":
+ version: 7.1.0
+ resolution: "@mui/styled-engine@npm:7.1.0"
+ dependencies:
+ "@babel/runtime": ^7.27.1
+ "@emotion/cache": ^11.13.5
+ "@emotion/serialize": ^1.3.3
+ "@emotion/sheet": ^1.4.0
+ csstype: ^3.1.3
+ prop-types: ^15.8.1
+ peerDependencies:
+ "@emotion/react": ^11.4.1
+ "@emotion/styled": ^11.3.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/react":
+ optional: true
+ "@emotion/styled":
+ optional: true
+ checksum: b23daa7188f5e9a7f5f9db5b0836dbea784a14dbe9e3cded2a072651d23085f0b36f179b063c9a3533903b9fc067558379ac31bf0c447ba8d1315c77ce221700
+ languageName: node
+ linkType: hard
+
+"@mui/system@npm:^7.1.0":
+ version: 7.1.0
+ resolution: "@mui/system@npm:7.1.0"
+ dependencies:
+ "@babel/runtime": ^7.27.1
+ "@mui/private-theming": ^7.1.0
+ "@mui/styled-engine": ^7.1.0
+ "@mui/types": ^7.4.2
+ "@mui/utils": ^7.1.0
+ clsx: ^2.1.1
+ csstype: ^3.1.3
+ prop-types: ^15.8.1
+ peerDependencies:
+ "@emotion/react": ^11.5.0
+ "@emotion/styled": ^11.3.0
+ "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/react":
+ optional: true
+ "@emotion/styled":
+ optional: true
+ "@types/react":
+ optional: true
+ checksum: 9a77664fe82b0687da1fa07997aaa7438e36b0ea7701f7bceb8173ddc969f08f838b05eeecc123b842184f30988c6521e18244a67a2b893b1ab2c842812a1a57
+ languageName: node
+ linkType: hard
+
+"@mui/types@npm:^7.4.2":
+ version: 7.4.2
+ resolution: "@mui/types@npm:7.4.2"
+ dependencies:
+ "@babel/runtime": ^7.27.1
+ peerDependencies:
+ "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: eb0c8eb4af9270faab8420ce46bed98ce880cc4324078211885e0fc6a61bb1afc1e5784944d27cfcb2f08ce6b56daaec33dd7b5bc0981a620be0ce59acd22eac
+ languageName: node
+ linkType: hard
+
+"@mui/utils@npm:^7.1.0":
+ version: 7.1.0
+ resolution: "@mui/utils@npm:7.1.0"
+ dependencies:
+ "@babel/runtime": ^7.27.1
+ "@mui/types": ^7.4.2
+ "@types/prop-types": ^15.7.14
+ clsx: ^2.1.1
+ prop-types: ^15.8.1
+ react-is: ^19.1.0
+ peerDependencies:
+ "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 85e4b101e8f3c9a97df47b8eecef34e65b3bfeba4c6b3dece42ff505c6268db28f7f4cc72877481494dbbc42efe267585e0b0025e3fbfb48a0755149ef3d3d1c
+ languageName: node
+ linkType: hard
+
"@nestjs/apollo@npm:10.1.3":
version: 10.1.3
resolution: "@nestjs/apollo@npm:10.1.3"
@@ -1792,6 +2229,13 @@ __metadata:
languageName: node
linkType: hard
+"@popperjs/core@npm:^2.11.8":
+ version: 2.11.8
+ resolution: "@popperjs/core@npm:2.11.8"
+ checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0
+ languageName: node
+ linkType: hard
+
"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2":
version: 1.1.2
resolution: "@protobufjs/aspromise@npm:1.1.2"
@@ -2390,6 +2834,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/prop-types@npm:^15.7.14":
+ version: 15.7.14
+ resolution: "@types/prop-types@npm:15.7.14"
+ checksum: d0c5407b9ccc3dd5fae0ccf9b1007e7622ba5e6f1c18399b4f24dff33619d469da4b9fa918a374f19dc0d9fe6a013362aab0b844b606cfc10676efba3f5f736d
+ languageName: node
+ linkType: hard
+
"@types/qs@npm:*":
version: 6.9.8
resolution: "@types/qs@npm:6.9.8"
@@ -2413,6 +2864,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/react-transition-group@npm:^4.4.12":
+ version: 4.4.12
+ resolution: "@types/react-transition-group@npm:4.4.12"
+ peerDependencies:
+ "@types/react": "*"
+ checksum: 13d36396cae4d3c316b03d4a0ba299f0d039c59368ba65e04b0c3dc06fd0a16f59d2c669c3e32d6d525a95423f156b84e550d26bff0bdd8df285f305f8f3a0ed
+ languageName: node
+ linkType: hard
+
"@types/react@npm:*":
version: 18.2.22
resolution: "@types/react@npm:18.2.22"
@@ -3527,6 +3987,17 @@ __metadata:
languageName: node
linkType: hard
+"babel-plugin-macros@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "babel-plugin-macros@npm:3.1.0"
+ dependencies:
+ "@babel/runtime": ^7.12.5
+ cosmiconfig: ^7.0.0
+ resolve: ^1.19.0
+ checksum: 765de4abebd3e4688ebdfbff8571ddc8cd8061f839bb6c3e550b0344a4027b04c60491f843296ce3f3379fb356cc873d57a9ee6694262547eb822c14a25be9a6
+ languageName: node
+ linkType: hard
+
"babel-plugin-styled-components@npm:>= 1.12.0":
version: 2.1.4
resolution: "babel-plugin-styled-components@npm:2.1.4"
@@ -4075,6 +4546,10 @@ __metadata:
"@babel/core": 7.23.0
"@dishboard/eslint-config": 1.0.0
"@dishboard/jest-config": 1.0.0
+ "@emotion/react": ^11.14.0
+ "@emotion/styled": ^11.14.0
+ "@mui/icons-material": ^7.1.0
+ "@mui/material": ^7.1.0
"@types/jest": 28.1.8
"@types/react": 18.0.17
"@types/react-dom": 18.0.6
@@ -4131,6 +4606,13 @@ __metadata:
languageName: node
linkType: hard
+"clsx@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "clsx@npm:2.1.1"
+ checksum: acd3e1ab9d8a433ecb3cc2f6a05ab95fe50b4a3cfc5ba47abb6cbf3754585fcb87b84e90c822a1f256c4198e3b41c7f6c391577ffc8678ad587fc0976b24fd57
+ languageName: node
+ linkType: hard
+
"co@npm:^4.6.0":
version: 4.6.0
resolution: "co@npm:4.6.0"
@@ -4294,7 +4776,7 @@ __metadata:
languageName: node
linkType: hard
-"convert-source-map@npm:^1.4.0, convert-source-map@npm:^1.6.0":
+"convert-source-map@npm:^1.4.0, convert-source-map@npm:^1.5.0, convert-source-map@npm:^1.6.0":
version: 1.9.0
resolution: "convert-source-map@npm:1.9.0"
checksum: dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8
@@ -4360,7 +4842,7 @@ __metadata:
languageName: node
linkType: hard
-"cosmiconfig@npm:^7.0.1":
+"cosmiconfig@npm:^7.0.0, cosmiconfig@npm:^7.0.1":
version: 7.1.0
resolution: "cosmiconfig@npm:7.1.0"
dependencies:
@@ -4442,6 +4924,13 @@ __metadata:
languageName: node
linkType: hard
+"csstype@npm:^3.1.3":
+ version: 3.1.3
+ resolution: "csstype@npm:3.1.3"
+ checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7
+ languageName: node
+ linkType: hard
+
"damerau-levenshtein@npm:^1.0.8":
version: 1.0.8
resolution: "damerau-levenshtein@npm:1.0.8"
@@ -4683,6 +5172,16 @@ __metadata:
languageName: node
linkType: hard
+"dom-helpers@npm:^5.0.1":
+ version: 5.2.1
+ resolution: "dom-helpers@npm:5.2.1"
+ dependencies:
+ "@babel/runtime": ^7.8.7
+ csstype: ^3.0.2
+ checksum: 863ba9e086f7093df3376b43e74ce4422571d404fc9828bf2c56140963d5edf0e56160f9b2f3bb61b282c07f8fc8134f023c98fd684bddcb12daf7b0f14d951c
+ languageName: node
+ linkType: hard
+
"dotenv-expand@npm:8.0.3":
version: 8.0.3
resolution: "dotenv-expand@npm:8.0.3"
@@ -5848,6 +6347,13 @@ __metadata:
languageName: node
linkType: hard
+"find-root@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "find-root@npm:1.1.0"
+ checksum: b2a59fe4b6c932eef36c45a048ae8f93c85640212ebe8363164814990ee20f154197505965f3f4f102efc33bfb1cbc26fd17c4a2fc739ebc51b886b137cbefaf
+ languageName: node
+ linkType: hard
+
"find-up@npm:^4.0.0, find-up@npm:^4.1.0":
version: 4.1.0
resolution: "find-up@npm:4.1.0"
@@ -6090,6 +6596,13 @@ __metadata:
languageName: node
linkType: hard
+"function-bind@npm:^1.1.2":
+ version: 1.1.2
+ resolution: "function-bind@npm:1.1.2"
+ checksum: 2b0ff4ce708d99715ad14a6d1f894e2a83242e4a52ccfcefaee5e40050562e5f6dafc1adbb4ce2d4ab47279a45dc736ab91ea5042d843c3c092820dfe032efb1
+ languageName: node
+ linkType: hard
+
"function.prototype.name@npm:^1.1.6":
version: 1.1.6
resolution: "function.prototype.name@npm:1.1.6"
@@ -6476,6 +6989,15 @@ __metadata:
languageName: node
linkType: hard
+"hasown@npm:^2.0.2":
+ version: 2.0.2
+ resolution: "hasown@npm:2.0.2"
+ dependencies:
+ function-bind: ^1.1.2
+ checksum: e8516f776a15149ca6c6ed2ae3110c417a00b62260e222590e54aa367cbcd6ed99122020b37b7fbdf05748df57b265e70095d7bf35a47660587619b15ffb93db
+ languageName: node
+ linkType: hard
+
"help-me@npm:^3.0.0":
version: 3.0.0
resolution: "help-me@npm:3.0.0"
@@ -6493,7 +7015,7 @@ __metadata:
languageName: node
linkType: hard
-"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
+"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
dependencies:
@@ -6803,6 +7325,15 @@ __metadata:
languageName: node
linkType: hard
+"is-core-module@npm:^2.16.0":
+ version: 2.16.1
+ resolution: "is-core-module@npm:2.16.1"
+ dependencies:
+ hasown: ^2.0.2
+ checksum: 6ec5b3c42d9cbf1ac23f164b16b8a140c3cec338bf8f884c076ca89950c7cc04c33e78f02b8cae7ff4751f3247e3174b2330f1fe4de194c7210deb8b1ea316a7
+ languageName: node
+ linkType: hard
+
"is-date-object@npm:^1.0.1":
version: 1.0.5
resolution: "is-date-object@npm:1.0.5"
@@ -7596,6 +8127,15 @@ __metadata:
languageName: node
linkType: hard
+"jsesc@npm:^3.0.2":
+ version: 3.1.0
+ resolution: "jsesc@npm:3.1.0"
+ bin:
+ jsesc: bin/jsesc
+ checksum: 19c94095ea026725540c0d29da33ab03144f6bcf2d4159e4833d534976e99e0c09c38cefa9a575279a51fc36b31166f8d6d05c9fe2645d5f15851d690b41f17f
+ languageName: node
+ linkType: hard
+
"json-buffer@npm:3.0.1":
version: 3.0.1
resolution: "json-buffer@npm:3.0.1"
@@ -9181,6 +9721,13 @@ __metadata:
languageName: node
linkType: hard
+"picocolors@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "picocolors@npm:1.1.1"
+ checksum: e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045
+ languageName: node
+ linkType: hard
+
"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1":
version: 2.3.1
resolution: "picomatch@npm:2.3.1"
@@ -9323,7 +9870,7 @@ __metadata:
languageName: node
linkType: hard
-"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
+"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@@ -9468,6 +10015,13 @@ __metadata:
languageName: node
linkType: hard
+"react-is@npm:^19.1.0":
+ version: 19.1.0
+ resolution: "react-is@npm:19.1.0"
+ checksum: 3eb4eac7f09bf178bdc6fa98d384f5f243b85de7c99679a88b0154ead4d818ad94386ccb00ea31ec52409ffd13299057f5ec6ca2eaec06f9f7eddc1ad4832332
+ languageName: node
+ linkType: hard
+
"react-refresh@npm:^0.14.0":
version: 0.14.0
resolution: "react-refresh@npm:0.14.0"
@@ -9475,6 +10029,21 @@ __metadata:
languageName: node
linkType: hard
+"react-transition-group@npm:^4.4.5":
+ version: 4.4.5
+ resolution: "react-transition-group@npm:4.4.5"
+ dependencies:
+ "@babel/runtime": ^7.5.5
+ dom-helpers: ^5.0.1
+ loose-envify: ^1.4.0
+ prop-types: ^15.6.2
+ peerDependencies:
+ react: ">=16.6.0"
+ react-dom: ">=16.6.0"
+ checksum: 75602840106aa9c6545149d6d7ae1502fb7b7abadcce70a6954c4b64a438ff1cd16fc77a0a1e5197cdd72da398f39eb929ea06f9005c45b132ed34e056ebdeb1
+ languageName: node
+ linkType: hard
+
"react@npm:18.2.0":
version: 18.2.0
resolution: "react@npm:18.2.0"
@@ -9631,6 +10200,19 @@ __metadata:
languageName: node
linkType: hard
+"resolve@npm:^1.19.0":
+ version: 1.22.10
+ resolution: "resolve@npm:1.22.10"
+ dependencies:
+ is-core-module: ^2.16.0
+ path-parse: ^1.0.7
+ supports-preserve-symlinks-flag: ^1.0.0
+ bin:
+ resolve: bin/resolve
+ checksum: ab7a32ff4046fcd7c6fdd525b24a7527847d03c3650c733b909b01b757f92eb23510afa9cc3e9bf3f26a3e073b48c88c706dfd4c1d2fb4a16a96b73b6328ddcf
+ languageName: node
+ linkType: hard
+
"resolve@npm:^2.0.0-next.3":
version: 2.0.0-next.4
resolution: "resolve@npm:2.0.0-next.4"
@@ -9657,6 +10239,19 @@ __metadata:
languageName: node
linkType: hard
+"resolve@patch:resolve@^1.19.0#~builtin":
+ version: 1.22.10
+ resolution: "resolve@patch:resolve@npm%3A1.22.10#~builtin::version=1.22.10&hash=07638b"
+ dependencies:
+ is-core-module: ^2.16.0
+ path-parse: ^1.0.7
+ supports-preserve-symlinks-flag: ^1.0.0
+ bin:
+ resolve: bin/resolve
+ checksum: 8aac1e4e4628bd00bf4b94b23de137dd3fe44097a8d528fd66db74484be929936e20c696e1a3edf4488f37e14180b73df6f600992baea3e089e8674291f16c9d
+ languageName: node
+ linkType: hard
+
"resolve@patch:resolve@^2.0.0-next.3#~builtin":
version: 2.0.0-next.4
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin::version=2.0.0-next.4&hash=07638b"
@@ -10202,6 +10797,13 @@ __metadata:
languageName: node
linkType: hard
+"source-map@npm:^0.5.7":
+ version: 0.5.7
+ resolution: "source-map@npm:0.5.7"
+ checksum: 5dc2043b93d2f194142c7f38f74a24670cd7a0063acdaf4bf01d2964b402257ae843c2a8fa822ad5b71013b5fcafa55af7421383da919752f22ff488bc553f4d
+ languageName: node
+ linkType: hard
+
"source-map@npm:^0.6.0, source-map@npm:^0.6.1":
version: 0.6.1
resolution: "source-map@npm:0.6.1"
@@ -10466,6 +11068,13 @@ __metadata:
languageName: node
linkType: hard
+"stylis@npm:4.2.0":
+ version: 4.2.0
+ resolution: "stylis@npm:4.2.0"
+ checksum: 0eb6cc1b866dc17a6037d0a82ac7fa877eba6a757443e79e7c4f35bacedbf6421fadcab4363b39667b43355cbaaa570a3cde850f776498e5450f32ed2f9b7584
+ languageName: node
+ linkType: hard
+
"subscriptions-transport-ws@npm:0.11.0":
version: 0.11.0
resolution: "subscriptions-transport-ws@npm:0.11.0"