From 260a6c49400e6104c7f79d24bac23e41577cbf66 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 20 Dec 2020 12:17:12 +0100 Subject: [PATCH] Improved welcome screen --- package-lock.json | 6 +-- package.json | 2 +- src/common/Home.scss | 35 +++++++++++++--- src/common/Home.tsx | 34 +++++++++++++--- src/common/img/ShlinkLogo.tsx | 6 ++- src/servers/ServersListGroup.scss | 25 +++++++++++- src/servers/ServersListGroup.tsx | 12 +++--- src/utils/theme/index.ts | 7 ++++ src/visits/helpers/DefaultChart.tsx | 9 +++-- src/visits/helpers/LineChartCard.tsx | 5 ++- test/common/Home.test.tsx | 26 +++++++----- test/common/img/ShlinkLogo.test.tsx | 34 ++++++++++++++++ test/servers/ServersListGroup.test.tsx | 49 ++++++++++++++++++----- test/visits/helpers/DefaultChart.test.tsx | 5 ++- 14 files changed, 203 insertions(+), 52 deletions(-) create mode 100644 src/utils/theme/index.ts create mode 100644 test/common/img/ShlinkLogo.test.tsx diff --git a/package-lock.json b/package-lock.json index 9c87279e..40bb319c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23075,9 +23075,9 @@ "dev": true }, "react-external-link": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/react-external-link/-/react-external-link-1.1.1.tgz", - "integrity": "sha512-e2WnTWkg81cuqxmDfjOalliAE20+Y/uD+lserN4uuwkwu+ciGLB3BMz4m7GnXh2+TowIi4sLtCL7zr7aDnIaqA==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-external-link/-/react-external-link-1.2.0.tgz", + "integrity": "sha512-6Lm0pD6rxrvZGVrWIWda809cAtrIlM3fDsKh9y5bKEVpOI0FTO2nTnxaqEood8+rw3svHJgpJGN6lZHO69ZTAQ==" }, "react-is": { "version": "16.7.0", diff --git a/package.json b/package.json index e4e1524d..95928585 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "react-copy-to-clipboard": "^5.0.2", "react-datepicker": "^3.3.0", "react-dom": "^17.0.1", - "react-external-link": "^1.1.1", + "react-external-link": "^1.2.0", "react-leaflet": "^3.0.2", "react-moment": "^1.0.0", "react-redux": "^7.2.2", diff --git a/src/common/Home.scss b/src/common/Home.scss index d82a49ce..775de88f 100644 --- a/src/common/Home.scss +++ b/src/common/Home.scss @@ -1,18 +1,41 @@ @import '../utils/base'; +@import '../utils/mixins/vertical-align'; .home { - text-align: center; - height: calc(100vh - #{$headerHeight} - #{($footer-height + $footer-margin)}); - display: flex; - align-items: center; - justify-content: center; - flex-flow: column; + position: relative; + padding-top: 15px; + + @media (min-width: $mdMin) { + padding-top: 0; + height: calc(100vh - #{$headerHeight} - #{($footer-height + $footer-margin)}); + } +} + +.home__logo { + @include vertical-align(); +} + +.home__main-card { + margin: 0 auto; + max-width: 720px; + + @media (min-width: $mdMin) { + @include vertical-align(); + } } .home__title { + text-align: center; font-size: 1.75rem; + margin: 0; @media (min-width: $mdMin) { font-size: 2.2rem; } } + +.home__servers-container { + @media (min-width: $mdMin) { + border-left: 1px solid rgba(0, 0, 0, .125); + } +} diff --git a/src/common/Home.tsx b/src/common/Home.tsx index 17e9e4b2..a72b1ee1 100644 --- a/src/common/Home.tsx +++ b/src/common/Home.tsx @@ -1,8 +1,11 @@ import { isEmpty, values } from 'ramda'; import { Link } from 'react-router-dom'; +import { Card, Row } from 'reactstrap'; +import { ExternalLink } from 'react-external-link'; import ServersListGroup from '../servers/ServersListGroup'; import './Home.scss'; import { ServersMap } from '../servers/data'; +import { ShlinkLogo } from './img/ShlinkLogo'; export interface HomeProps { servers: ServersMap; @@ -14,11 +17,32 @@ const Home = ({ servers }: HomeProps) => { return (
-

Welcome to Shlink

- - {hasServers && Please, select a server.} - {!hasServers && Please, add a server.} - + + +
+
+ +
+
+
+
+

Welcome!

+
+ + {!hasServers && ( +
+

This application will help you to manage your Shlink servers.

+

To start, please, add your first server.

+

+ You still don‘t have a Shlink server? + Learn how to get started. +

+
+ )} +
+
+
+
); }; diff --git a/src/common/img/ShlinkLogo.tsx b/src/common/img/ShlinkLogo.tsx index 757c3b22..40094a67 100644 --- a/src/common/img/ShlinkLogo.tsx +++ b/src/common/img/ShlinkLogo.tsx @@ -1,9 +1,11 @@ -interface ShlinkLogoProps { +import { MAIN_COLOR } from '../../utils/theme'; + +export interface ShlinkLogoProps { color?: string; className?: string; } -export const ShlinkLogo = ({ color = '#4595e3', className }: ShlinkLogoProps) => ( +export const ShlinkLogo = ({ color = MAIN_COLOR, className }: ShlinkLogoProps) => ( ( @@ -17,13 +19,13 @@ const ServerListItem = ({ id, name }: { id: string; name: string }) => ( ); -const ServersListGroup: FC = ({ servers, children }) => ( +const ServersListGroup: FC = ({ servers, children, embedded = false }) => ( <> -
-
{children}
-
+ {children &&
{children}
} {servers.length > 0 && ( - + {servers.map(({ id, name }) => )} )} diff --git a/src/utils/theme/index.ts b/src/utils/theme/index.ts new file mode 100644 index 00000000..a4b5f96b --- /dev/null +++ b/src/utils/theme/index.ts @@ -0,0 +1,7 @@ +export const MAIN_COLOR = '#4696e5'; + +export const MAIN_COLOR_ALPHA = 'rgba(70, 150, 229, 0.4)'; + +export const HIGHLIGHTED_COLOR = '#F77F28'; + +export const HIGHLIGHTED_COLOR_ALPHA = 'rgba(247, 127, 40, 0.4)'; diff --git a/src/visits/helpers/DefaultChart.tsx b/src/visits/helpers/DefaultChart.tsx index 0753ad35..78d1863a 100644 --- a/src/visits/helpers/DefaultChart.tsx +++ b/src/visits/helpers/DefaultChart.tsx @@ -7,6 +7,7 @@ import { fillTheGaps } from '../../utils/helpers/visits'; import { Stats } from '../types'; import { prettify } from '../../utils/helpers/numbers'; import { pointerOnHover, renderDoughnutChartLabel, renderNonDoughnutChartLabel } from '../../utils/helpers/charts'; +import { HIGHLIGHTED_COLOR, HIGHLIGHTED_COLOR_ALPHA, MAIN_COLOR, MAIN_COLOR_ALPHA } from '../../utils/theme'; import './DefaultChart.scss'; export interface DefaultChartProps { @@ -33,7 +34,7 @@ const generateGraphData = ( title, label: highlightedData ? 'Non-selected' : 'Visits', data, - backgroundColor: isBarChart ? 'rgba(70, 150, 229, 0.4)' : [ + backgroundColor: isBarChart ? MAIN_COLOR_ALPHA : [ '#97BBCD', '#F7464A', '#46BFBD', @@ -46,15 +47,15 @@ const generateGraphData = ( '#DCDCDC', '#463730', ], - borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white', + borderColor: isBarChart ? MAIN_COLOR : 'white', borderWidth: 2, }, highlightedData && { title, label: highlightedLabel ?? 'Selected', data: highlightedData, - backgroundColor: 'rgba(247, 127, 40, 0.4)', - borderColor: '#F77F28', + backgroundColor: HIGHLIGHTED_COLOR_ALPHA, + borderColor: HIGHLIGHTED_COLOR, borderWidth: 2, }, ].filter(Boolean) as ChartDataSets[], diff --git a/src/visits/helpers/LineChartCard.tsx b/src/visits/helpers/LineChartCard.tsx index 588f50ea..28f23a0c 100644 --- a/src/visits/helpers/LineChartCard.tsx +++ b/src/visits/helpers/LineChartCard.tsx @@ -19,6 +19,7 @@ import { rangeOf } from '../../utils/utils'; import ToggleSwitch from '../../utils/ToggleSwitch'; import { prettify } from '../../utils/helpers/numbers'; import { pointerOnHover, renderNonDoughnutChartLabel } from '../../utils/helpers/charts'; +import { HIGHLIGHTED_COLOR, MAIN_COLOR } from '../../utils/theme'; import './LineChartCard.scss'; interface LineChartCardProps { @@ -173,8 +174,8 @@ const LineChartCard = ( const data: ChartData = { labels, datasets: [ - generateDataset(groupedVisits, 'Visits', '#4696e5'), - highlightedVisits.length > 0 && generateDataset(groupedHighlighted, highlightedLabel, '#F77F28'), + generateDataset(groupedVisits, 'Visits', MAIN_COLOR), + highlightedVisits.length > 0 && generateDataset(groupedHighlighted, highlightedLabel, HIGHLIGHTED_COLOR), ].filter(Boolean) as ChartDataSets[], }; const options: ChartOptions = { diff --git a/test/common/Home.test.tsx b/test/common/Home.test.tsx index 1f9c5a90..22a88cda 100644 --- a/test/common/Home.test.tsx +++ b/test/common/Home.test.tsx @@ -2,6 +2,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Mock } from 'ts-mockery'; import Home, { HomeProps } from '../../src/common/Home'; import { ServerWithId } from '../../src/servers/data'; +import { ShlinkLogo } from '../../src/common/img/ShlinkLogo'; describe('', () => { let wrapped: ShallowWrapper; @@ -19,21 +20,26 @@ describe('', () => { afterEach(() => wrapped?.unmount()); - it('shows link to create server when no servers exist', () => { + it('renders logo and title', () => { const wrapped = createComponent(); - expect(wrapped.find('Link')).toHaveLength(1); + expect(wrapped.find(ShlinkLogo)).toHaveLength(1); + expect(wrapped.find('.home__title')).toHaveLength(1); }); - it('asks to select a server when servers exist', () => { - const servers = { - '1a': Mock.of({ name: 'foo', id: '1' }), - '2b': Mock.of({ name: 'bar', id: '2' }), - }; + it.each([ + [ + { + '1a': Mock.of({ name: 'foo', id: '1' }), + '2b': Mock.of({ name: 'bar', id: '2' }), + }, + 0, + ], + [{}, 3 ], + ])('shows link to create or set-up server only when no servers exist', (servers, expectedParagraphs) => { const wrapped = createComponent({ servers }); - const span = wrapped.find('span'); + const p = wrapped.find('p'); - expect(span).toHaveLength(1); - expect(span.text()).toContain('Please, select a server.'); + expect(p).toHaveLength(expectedParagraphs); }); }); diff --git a/test/common/img/ShlinkLogo.test.tsx b/test/common/img/ShlinkLogo.test.tsx new file mode 100644 index 00000000..6e4b6494 --- /dev/null +++ b/test/common/img/ShlinkLogo.test.tsx @@ -0,0 +1,34 @@ +import { shallow, ShallowWrapper } from 'enzyme'; +import { ShlinkLogo, ShlinkLogoProps } from '../../../src/common/img/ShlinkLogo'; +import { MAIN_COLOR } from '../../../src/utils/theme'; + +describe('', () => { + let wrapper: ShallowWrapper; + const createWrapper = (props: ShlinkLogoProps) => { + wrapper = shallow(); + + return wrapper; + }; + + afterEach(() => wrapper?.unmount()); + + it.each([ + [ undefined, MAIN_COLOR ], + [ 'red', 'red' ], + [ 'white', 'white' ], + ])('renders expected color', (color, expectedColor) => { + const wrapper = createWrapper({ color }); + + expect(wrapper.find('g').prop('fill')).toEqual(expectedColor); + }); + + it.each([ + [ undefined, undefined ], + [ 'foo', 'foo' ], + [ 'bar', 'bar' ], + ])('renders expected class', (className, expectedClassName) => { + const wrapper = createWrapper({ className }); + + expect(wrapper.prop('className')).toEqual(expectedClassName); + }); +}); diff --git a/test/servers/ServersListGroup.test.tsx b/test/servers/ServersListGroup.test.tsx index bab7c52d..9a6ad591 100644 --- a/test/servers/ServersListGroup.test.tsx +++ b/test/servers/ServersListGroup.test.tsx @@ -5,31 +5,58 @@ import ServersListGroup from '../../src/servers/ServersListGroup'; import { ServerWithId } from '../../src/servers/data'; describe('', () => { + const servers = [ + Mock.of({ name: 'foo', id: '123' }), + Mock.of({ name: 'bar', id: '456' }), + ]; let wrapped: ShallowWrapper; - const createComponent = (servers: ServerWithId[]) => { - wrapped = shallow(The list of servers); + const createComponent = (params: { servers?: ServerWithId[]; withChildren?: boolean; embedded?: boolean }) => { + const { servers = [], withChildren = true, embedded } = params; + + wrapped = shallow( + + {withChildren ? 'The list of servers' : undefined} + , + ); return wrapped; }; afterEach(() => wrapped?.unmount()); - it('Renders title', () => { - const wrapped = createComponent([]); + it('renders title', () => { + const wrapped = createComponent({}); const title = wrapped.find('h5'); expect(title).toHaveLength(1); expect(title.text()).toEqual('The list of servers'); }); - it('shows servers list', () => { - const servers = [ - Mock.of({ name: 'foo', id: '123' }), - Mock.of({ name: 'bar', id: '456' }), - ]; - const wrapped = createComponent(servers); + it('does not render title when children is not provided', () => { + const wrapped = createComponent({ withChildren: false }); + const title = wrapped.find('h5'); - expect(wrapped.find(ListGroup)).toHaveLength(1); + expect(title).toHaveLength(0); + }); + + it.each([ + [ servers ], + [[]], + ])('shows servers list', (servers) => { + const wrapped = createComponent({ servers }); + + expect(wrapped.find(ListGroup)).toHaveLength(servers.length ? 1 : 0); expect(wrapped.find('ServerListItem')).toHaveLength(servers.length); }); + + it.each([ + [ true, 'servers-list__list-group servers-list__list-group--embedded' ], + [ false, 'servers-list__list-group' ], + [ undefined, 'servers-list__list-group' ], + ])('renders proper classes for embedded', (embedded, expectedClasses) => { + const wrapped = createComponent({ servers, embedded }); + const listGroup = wrapped.find(ListGroup); + + expect(listGroup.prop('className')).toEqual(expectedClasses); + }); }); diff --git a/test/visits/helpers/DefaultChart.test.tsx b/test/visits/helpers/DefaultChart.test.tsx index 9f0dee2b..49dd8069 100644 --- a/test/visits/helpers/DefaultChart.test.tsx +++ b/test/visits/helpers/DefaultChart.test.tsx @@ -3,6 +3,7 @@ import { Doughnut, HorizontalBar } from 'react-chartjs-2'; import { keys, values } from 'ramda'; import DefaultChart from '../../../src/visits/helpers/DefaultChart'; import { prettify } from '../../../src/utils/helpers/numbers'; +import { MAIN_COLOR, MAIN_COLOR_ALPHA } from '../../../src/utils/theme'; describe('', () => { let wrapper: ShallowWrapper; @@ -62,8 +63,8 @@ describe('', () => { const { datasets: [{ backgroundColor, borderColor }] } = horizontal.prop('data') as any; const { legend, legendCallback, scales } = horizontal.prop('options') ?? {}; - expect(backgroundColor).toEqual('rgba(70, 150, 229, 0.4)'); - expect(borderColor).toEqual('rgba(70, 150, 229, 1)'); + expect(backgroundColor).toEqual(MAIN_COLOR_ALPHA); + expect(borderColor).toEqual(MAIN_COLOR); expect(legend).toEqual({ display: false }); expect(legendCallback).toEqual(false); expect(scales).toEqual({