Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2c64def
Match linter expectations
jonspark Nov 11, 2025
1d12a3d
Build out types, make tests pass for getData
jonspark Nov 11, 2025
1ad870b
Convert returned results into something more prop-like
jonspark Nov 11, 2025
a782dff
Amend for results, add simple empty state
jonspark Nov 11, 2025
c57f96c
Block out component structure
jonspark Nov 11, 2025
79c166e
Test for different heading levels
jonspark Nov 11, 2025
0fba330
Output a list of vehicles
jonspark Nov 11, 2025
6f6df4f
Add reset styles, adjust Webpack for font loading
jonspark Nov 11, 2025
13bb872
Initial Vehicle styling pass, responsive grid for VehicleList
jonspark Nov 11, 2025
5e4d508
Add props typedef
jonspark Nov 11, 2025
7caf527
Fix for linting
jonspark Nov 11, 2025
5009d40
Fix style ordering
jonspark Nov 12, 2025
4628e82
Fix srcset syntax
jonspark Nov 12, 2025
2f0a88a
Define var before use, save inline computation
jonspark Nov 12, 2025
e23950b
Add tablet and desktop responsive styles with animation
jonspark Nov 12, 2025
c588cd4
Add a simple loading state
jonspark Nov 12, 2025
2f0194f
Add shade over zoomed image
jonspark Nov 12, 2025
0966945
Simplify Vehicle props
jonspark Nov 12, 2025
642d584
Add initial Modal component, handle open/close state with URL Pushstate
jonspark Nov 12, 2025
7c510bf
Style modal data, humanise meta output.
jonspark Nov 12, 2025
b5c1f5c
Styles tidy
jonspark Nov 12, 2025
ce1f872
Merge branch 'feature/modal' into submission/jon-park
jonspark Nov 12, 2025
e81dca6
Correct tests
jonspark Nov 12, 2025
3c8054c
Fix HTML validation, use hidden button for backdrop close
jonspark Nov 12, 2025
1c04308
Correct backdrop trigger parent
jonspark Nov 12, 2025
18b4ae4
Use Land Rover site font set
jonspark Nov 12, 2025
9b1aa48
Correct sizings for new fonts
jonspark Nov 12, 2025
40c9021
Normalise vehicle description at transformation time
jonspark Nov 12, 2025
246098c
Reset API tests back to skipped
jonspark Nov 12, 2025
8beb105
Amend for keyboard navigation.
jonspark Nov 12, 2025
54b48a4
Fix for linting.
jonspark Nov 12, 2025
24f919e
Fix for the non-conflicting linting rules
jonspark Nov 12, 2025
947325a
Tweak font sizes, modal close spacing when focussed.
jonspark Nov 12, 2025
e14329b
Add some missing assertions
jonspark Nov 12, 2025
9e649b4
Reduce motion on hover effect
jonspark Nov 12, 2025
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: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ root = true

[*]
indent_style = space
indent_size = 4
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
Expand Down
51 changes: 37 additions & 14 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.22.0",
"file-loader": "^4.3.0",
"html-webpack-plugin": "^4.5.1",
"jest": "^26.6.3",
"sass": "^1.32.5",
Expand Down
11 changes: 6 additions & 5 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#f99d27" />
<meta name="description" content="An example react based technical test."/>
<meta name="description" content="An example react based technical test." />
<title>Front-end Technical Test</title>
</head>
<body>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div class="root"></div>
</body>
<div id="modal-root"></div>
</body>
</html>
10 changes: 7 additions & 3 deletions src/api/__tests__/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe.skip('getData Tests', () => {

it('Should traverse and make further api calls on main results', async () => {
expect.assertions(3);
request.mockResolvedValueOnce([{ apiUrl: '/api/vehicle_ftype.json' }, { apiUrl: '/api/vehicle_xj.json' }]);
request.mockResolvedValueOnce([{ apiUrl: '/api/vehicle_ftype.json', id: 'ftype' }, { apiUrl: '/api/vehicle_xj.json', id: 'xj' }]);
request.mockResolvedValueOnce({ id: 'ftype', price: '£36,000' });
request.mockResolvedValueOnce({ id: 'xj', price: '£40,000' });
await safelyCallApi();
Expand All @@ -39,7 +39,7 @@ describe.skip('getData Tests', () => {
});

it('Should ignore failed API calls during traversing', () => {
request.mockResolvedValueOnce([{ apiUrl: '/api/vehicle_ftype.json' }, { apiUrl: '/api/vehicle_xj.json' }]);
request.mockResolvedValueOnce([{ apiUrl: '/api/vehicle_ftype.json', id: 'ftype' }, { apiUrl: '/api/vehicle_xj.json', id: 'xj' }]);
request.mockResolvedValueOnce({ id: 'ftype', price: '£36,000' });
request.mockRejectedValueOnce('An error occurred');

Expand All @@ -49,7 +49,11 @@ describe.skip('getData Tests', () => {
});

it('Should ignore vehicles without valid price during traversing', () => {
request.mockResolvedValueOnce([{ apiUrl: '/api/ftype.json' }, { apiUrl: '/api/xe.json' }, { apiUrl: '/api/xj.json' }]);
request.mockResolvedValueOnce([
{ apiUrl: '/api/ftype.json', id: 'ftype' },
{ apiUrl: '/api/xe.json', id: 'xe' },
{ apiUrl: '/api/xj.json', id: 'xj' }
]);
request.mockResolvedValueOnce({ id: 'ftype', price: '' });
request.mockResolvedValueOnce({ id: 'xe' });
request.mockResolvedValueOnce({ id: 'xj', price: '£40,000' });
Expand Down
26 changes: 26 additions & 0 deletions src/api/api_docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,37 @@
* @property {string} url - URL of image
*/

/**
* @typedef {Object} vehicleMeta
* @property {Array<string>} bodystyles - A list of available body styles
* @property {Array<string>} drivetrain - A list of available drivetrains
* @property {number} passengers - Number of passenger seats
* @property {{ template: string, value: number }} emissions - Emission description
*/

/**
* @typedef {Object} vehicleSummaryResponse
* @property {string} id - ID of the vehicle
* @property {string} apiUrl - API URL for price, description & other details
* @property {string} description - Description
* @property {string} price - Price
* @property {Array.<vehicleMedia>} media - Array of vehicle images
*/

/**
* @typedef {Object} vehicleDetailResponse
* @property {string} id - ID of the vehicle
* @property {string} description - Description
* @property {string} price - Price
* @property {vehicleMeta} meta - Vehicle metadata
*/

/**
* @typedef {Object} vehicleSummaryPayload
* @property {string} id - ID of the vehicle
* @property {string} apiUrl - API URL for price, description & other details
* @property {string} description - Description
* @property {string} price - Price
* @property {Array.<vehicleMedia>} media - Array of vehicle images
* @property {vehicleMeta} meta - Vehicle metadata
*/
14 changes: 13 additions & 1 deletion src/api/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,17 @@
* @return {Promise<Object>}
*/
export async function request(apiUrl) {
return apiUrl;
try {
const response = await fetch(apiUrl, {
// Might not be needed here, but may be an API requirement
headers: { Accepts: 'application/json' }
});

return !response.ok
// Returning an empty object to match the test requirements, but could error log here.
? {}
: response.json();
} catch (error) {
return {};
}
}
43 changes: 41 additions & 2 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,46 @@ import { request } from './helpers';
*
* @return {Promise<Array.<vehicleSummaryPayload>>}
*/
// TODO: All API related logic should be made inside this function.
export default async function getData() {
return [];
// Make the initial list request.
/** @type Array.vehicleSummaryPayload */
const vehicleList = await request('/api/vehicles.json');

/** @type Record<string, vehicleSummaryResponse */
const vehicleMap = {};
const vehicleDetailsPromises = [];

// Traverse the list to get each vehicle's details.
vehicleList.forEach((/** @type vehicleSummaryResponse */ vehicle) => {
const { apiUrl, id } = vehicle;

// Add the vehicle details request to an array and continue
if (apiUrl) {
vehicleMap[id] = vehicle;
vehicleDetailsPromises.push(request(apiUrl).catch(() => {
// TODO Log an error with a monitoring system.
}));
}
});

// Wait for all the details requests to finish.
/** @type Array.vehicleDetailResponse */
const vehicleDetailResponses = await Promise.all(vehicleDetailsPromises);

// Build a list of valid responses with prices
const vehicleDetails = [];
vehicleDetailResponses.forEach((details) => {
// Verify the entry is valid
if (details && typeof details === 'object') {
const { id, price } = details;

// Check that it has a price
if ((id in vehicleMap) && price) {
// Combine the summary and detail information
vehicleDetails.push({ ...vehicleMap[id], ...details });
}
}
});

return vehicleDetails;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
10 changes: 10 additions & 0 deletions src/components/VehicleList/Loader/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import './style.scss';

export default function Loader() {
return (
<div data-testid="loading" className="Loader">
<div className="Loader__spinner" aria-label="Loading vehicles." />
</div>
);
}
27 changes: 27 additions & 0 deletions src/components/VehicleList/Loader/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@layer components {

.Loader {
min-height: 10rem;
padding: 1rem;

&__spinner {
animation: spin .9s linear infinite;
border: .125rem solid #e0e0e0;
border-block-start-color: inherit;
border-radius: 50%;
height: 1.5rem;
width: 1.5rem;
}
}

@keyframes spin {

from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}
}
Loading