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
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ APP_SECRET=8b2d716c2c4d34c8c6929a5e67c1add4
#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
#TRUSTED_HOSTS='^(localhost|example\.com)$'
###< symfony/framework-bundle ###

API_NBP_BASE_URL=https://api.nbp.pl
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots

API_NBP_BASE_URL=http://test.nbp.api
74 changes: 14 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,69 +1,23 @@
Fullstack Developer - Tasks
==========
# Telemedi Zadanie Rekrutacyjne

------------
### Czas realizacji

### :warning: Zapoznaj się z poniższymi wytycznymi do pracy.
### :warning: Treść zadań do wykonania przesłaliśmy mailem.
Zadanie zajęło łącznie około 8 godzin pracy. Główny nacisk położyłem na stronę backendową, implementując logikę
pobierania i przetwarzania danych.

------------
### Prezentacja danych

Jak zacząć pracę
------------
1. Należy zrobić Fork z tego repozytorium [Jak forkować repozytorium w GitHub](https://docs.github.com/en/get-started/quickstart/fork-a-repo), w ten sposób tworząc sobie prywatne miejsce do pracy.
1. Następnie w stworzonym przez siebie forku repozytorium stwórz branch od gałęzi master, na którym będziesz pracować, np: ` $ git checkout -b MojeZadanieJanKowalski `
Prezentacja danych na frontendzie została wykonana w najprostszy możliwy sposób, który spełnia minimalne wymagania
zadania. W bardziej rozbudowanym projekcie ulepszyłbym interfejs użytkownika.

### Setup środowiska
### Uwagi techniczne

1. Skonfiguruj sobie lokalny serwer (np. Apache) pod development; ustaw vHosta tak, żeby pod wybraną domeną pokazywał na odpowiedni katalog na dysku (tj. katalog `public/` z repo) - przykład poniżej:
Praca nad tym zadaniem byłaby bardziej interesująca i efektywna, gdyby do dyspozycji była:

```
<VirtualHost *:80>
# Root - katalog /public z repozytorium z Github
DocumentRoot "C:/xampp/htdocs/recruitment_task_fullstack/public/"
# domena lokalna
ServerName telemedi-zadanie.localhost
</VirtualHost>
```
1. Jeśli Twoja skonfigurowana domena jest inna niż `telemedi-zadanie.localhost` - zmień ją w pliku `assets/js/components/SetupCheck.js` w metodzie getBaseUrl()
1. Zainstaluj paczki composera i npm (`$ composer install && npm install`)
1. Zbuduj appkę frontową w trybie watch (`$ npm run watch --dev`)
1. …i już, do dzieła! :)
- wersja PHP co najmniej 8.1,
- możliwość użycia TypeScript.

### Setup środowiska za pomocą dockera
### Dostępność

1. Uruchom komendę:

```
docker compose up -d
```
1. Pod adresem `http://telemedi-zadanie.localhost` powinna uruchomić się aplikacja

------------
_FYI: tak wygląda działająca aplikacja, gotowa do developmentu:_

![Working_app_image](https://github.com/telemedico/recruitment_task_fullstack/blob/master/assets/img/working_app_preview.png?raw=true)

------------

Wytyczne dot. implementacji
------------

**Głównym celem implementacji powinno być pokazanie się z dobrej strony jako programista, czyli nie ma jednego słusznego podejścia! :)**

1. W ramach implementacji nie należy dodawać nowych paczek do composer’a/npm’a. Zachęcamy do korzystania z tych, które już są dodane.
1. Development należy prowadzić pod kątem kompatybilności PHP z wersją 7.2.5 (zgodnie z composer.json)
1. Napisanie testów jest elementem oceny.
1. **Ocenie podlegać będzie całość podejścia do zadania.**

Niedokończone zadanie też warto podesłać, np. z komentarzem, co by można było dodać - rozumiemy, że czasem nie starcza czasu na wszystko co się chce zrobić!

Zakończenie pracy i wysłanie wyniku
------------
1. **W swoim forku utwórz Pull Request do brancha master. Nie rób PR do oryginalnego repozytorium** (Pull Requesty do publicznych repo są publiczne)
1. **Poza implementacją zależy nam też na informacjach zwrotnych, które posłużą nam w poprawie jakości zadań.** Dlatego prosimy Cię o umieszczenie dodatkowo informacji w opisie tworzonego Pull Requesta:
1. Faktycznie poświęconego czasu na zadanie (po zakończeniu implementacji)
1. Feedbacku do samego zadania
1. Twoich komentarzy dot. podejścia do zadania itd
1. np. _“Robiąc X miałem na względzie Y, zastosowałem podejście Z”_
1. **Prosimy, potwierdź nam mailowo wykonanie zadania, wysyłając link do Pull Requesta w swoim forku.**
Strona z kursami walut jest dostępna pod adresem:
[http://telemedi-zadanie.localhost/exchange-rates](http://telemedi-zadanie.localhost/exchange-rates)
19 changes: 19 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
body {
background-color: lightgray;
}

.date-picker-container > * {
display: inline-block;
margin: 4px;
}

#date-picker {
background-color: lightgray;
}

.highlighted-rate {
background-color: #bfbfbf;
font-weight: bold;
border: 1px solid #a6c9e2;
}

.error-message {
color: indianred;
}
164 changes: 164 additions & 0 deletions assets/js/components/ExchangeRates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React, {Component} from 'react';
import axios from 'axios';

class ExchangeRates extends Component {
constructor(props) {
super(props);
this.state = {
rates: [],
loading: true,
errorMessage: '',
userDate: '',
latestDate: ''
};
}

getBaseUrl() {
return 'http://telemedi-zadanie.localhost';
}

componentDidMount() {
this.fetchData();
}

getTodaysDate = () => {
return (new Date()).toISOString().split('T')[0];
};

adjustDate(date) {
const inputDate = new Date(date);
const currentHour = new Date().getHours();
if (currentHour < 12) {
inputDate.setDate(inputDate.getDate() - 1);
}

const year = inputDate.getFullYear();
const month = String(inputDate.getMonth() + 1).padStart(2, '0');
const day = String(inputDate.getDate()).padStart(2, '0');

return `${year}-${month}-${day}`;
}

getQueryParamDate(param) {
const searchParams = new URLSearchParams(this.props.location.search);

return searchParams.get(param);
}

handleDateChange = (event) => {
const selectedDate = event.target.value;
this.setState({userDate: selectedDate}, () => {
this.props.history.push({
pathname: '/exchange-rates',
search: `?date=${selectedDate}`,
});
this.fetchData();
});
};

fetchData() {
const todaysDate = this.getTodaysDate();
const ratesDate = this.adjustDate(this.state.userDate || this.getQueryParamDate('date') || todaysDate);

axios.get(`${this.getBaseUrl()}/api/exchange-rates?userDate=${ratesDate}&latestDate=${this.adjustDate(todaysDate)}`)
.then(response => {
if (200 === response.status) {
this.setState({
rates: response.data.rates,
userDate: response.data.userDate,
latestDate: response.data.latestDate
});
} else {
this.setState({userDate: ratesDate, errorMessage: 'Something went wrong.'});
}
})
.catch(error => {
if (error.response.headers && error.response.headers.has('X-Validation-Errors')) {
this.setState({
loading: false,
errorMessage: `Fix following errors and try again: ${error.response.headers.get('X-Validation-Errors')}`
});
} else {
this.setState({userDate: ratesDate, errorMessage: 'Something went wrong.'});
}
})
.finally(() => {
this.setState({loading: false});
})
}

renderTableRows() {
return this.state.rates.map((rate, index) => (
<tr key={index}>
<td><b>{rate.currencyCode}</b> ({rate.currencyName})</td>
<td className="highlighted-rate">{rate.userDateBidRate ?? 'N/A'}</td>
<td className="highlighted-rate">{rate.userDateAskRate ?? 'N/A'}</td>
<td className="highlighted-rate">{rate.userDateNbpRate ?? 'N/A'}</td>
<td>{rate.latestBidRate ?? 'N/A'}</td>
<td>{rate.latestAskRate ?? 'N/A'}</td>
<td>{rate.latestNbpRate ?? 'N/A'}</td>
</tr>
));
}

render() {
const {rates, loading, userDate, latestDate, errorMessage} = this.state;

return (
<div className="container mt-5">
<h2 className="text-center">Exchange Rates @ Telemedi by Adam Guła</h2>
{loading ? (
<div className="text-center">
<span className="fa fa-spin fa-spinner fa-4x"></span>
</div>
) : (
<>
<div className="text-center mb-4 date-picker-container">
<label htmlFor="date-picker">Selected date</label>
<input
id="date-picker"
type="date"
value={userDate}
min="2023-01-01"
max={this.getTodaysDate()}
onChange={this.handleDateChange}
/>
</div>
<table className="table table-bordered">
<thead>
<tr>
<th rowSpan="1"></th>
<th colSpan="3" className="text-center highlighted-rate">Rates for selected date
({userDate})
</th>
<th colSpan="3" className="text-center">Latest rates ({latestDate})</th>
</tr>
<tr>
<th>Currency</th>
<th className="highlighted-rate">Bid</th>
<th className="highlighted-rate">Ask</th>
<th className="highlighted-rate">NBP</th>
<th>Bid</th>
<th>Ask</th>
<th>NBP</th>
</tr>
</thead>
<tbody>
{rates.length > 0 ? this.renderTableRows() : (
<tr>
{errorMessage.length > 0 ? (
<td colSpan="6" className="text-center error-message">{errorMessage}</td>) : (
<td colSpan="6" className="text-center">No data available</td>)}
</tr>
)}

</tbody>
</table>
</>
)}
</div>
);
}
}

export default ExchangeRates;
10 changes: 6 additions & 4 deletions assets/js/components/Home.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// ./assets/js/components/Home.js

import React, {Component} from 'react';
import {Route, Redirect, Switch, Link} from 'react-router-dom';
import SetupCheck from "./SetupCheck";
import ExchangeRates from "./ExchangeRates";

class Home extends Component {

Expand All @@ -16,13 +15,16 @@ class Home extends Component {
<li className="nav-item">
<Link className={"nav-link"} to={"/setup-check"}> React Setup Check </Link>
</li>

<li className="nav-item">
<Link className={"nav-link"} to={"/exchange-rates"}> Exchange Rates </Link>
</li>
</ul>
</div>
</nav>
<Switch>
<Redirect exact from="/" to="/setup-check" />
<Redirect exact from="/" to="/setup-check"/>
<Route path="/setup-check" component={SetupCheck} />
<Route path="/exchange-rates" component={ExchangeRates} />
</Switch>
</div>
)
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "telemedi/test-purposes",
"autoload": {
"psr-4": {
"App\\": "src/App/"
"App\\": "src/App/",
"App\\Tests\\": "tests/"
},
"classmap": [
"src/Kernel.php"
Expand Down
29 changes: 13 additions & 16 deletions config/routes.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
#home:
# path: /
# defaults: { _controller: 'AppBundle\Controller\DefaultController::indexAction' }
# methods: GET
#

setupcheck:
path: /api/setup-check
controller: App\Controller\DefaultController::setupCheck
path: /api/setup-check
controller: App\Presentation\Controller\DefaultController::setupCheck

exchangerates:
path: /api/exchange-rates
controller: App\Presentation\Controller\ExchangeRatesController

index:
path: /{wildcard}
defaults: {
_controller: App\Controller\DefaultController::index
}
requirements:
wildcard: .*
# controller: App\Controller\DefaultController::index

ndex:
path: /{wildcard}
defaults: {
_controller: App\Presentation\Controller\DefaultController::index
}
requirements:
wildcard: .*
8 changes: 6 additions & 2 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ services:

# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/App/Controller/'
App\Presentation\Controller\:
resource: '../src/App/Presentation/Controller/'
autowire: true
tags: ['controller.service_arguments']

# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

App\Infrastruture\Api\NBP\NbpApi:
arguments:
$baseUrl: '%env(API_NBP_BASE_URL)%'
11 changes: 11 additions & 0 deletions config/services_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
Symfony\Component\HttpClient\MockHttpClient:
public: true
arguments:
- [ ]

App\Infrastruture\Api\NBP\NbpApi:
arguments:
$client: '@Symfony\Component\HttpClient\MockHttpClient'
$logger: '@logger'
$baseUrl: '%env(API_NBP_BASE_URL)%'
2 changes: 1 addition & 1 deletion package-lock.json

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

Loading