poeti8 vor 6 Jahren
Ursprung
Commit
cf537629f2
84 geänderte Dateien mit 767 neuen und 3614 gelöschten Zeilen
  1. 0 172
      client/actions/__test__/auth.js
  2. 0 176
      client/actions/__test__/settings.js
  3. 0 159
      client/actions/__test__/url.js
  4. 0 32
      client/actions/actionTypes.js
  5. 0 92
      client/actions/auth.js
  6. 0 3
      client/actions/index.js
  7. 0 89
      client/actions/settings.js
  8. 0 71
      client/actions/url.js
  9. 13 5
      client/components/ALink.tsx
  10. 40 0
      client/components/AppWrapper.tsx
  11. 0 87
      client/components/BodyWrapper.tsx
  12. 6 17
      client/components/Button.tsx
  13. 17 19
      client/components/Charts/Area.tsx
  14. 5 7
      client/components/Charts/Bar.tsx
  15. 5 8
      client/components/Charts/Pie.tsx
  16. 3 0
      client/components/Charts/index.tsx
  17. 3 3
      client/components/Checkbox.tsx
  18. 3 1
      client/components/Divider.tsx
  19. 0 58
      client/components/Error.tsx
  20. 21 22
      client/components/Extensions.tsx
  21. 18 37
      client/components/Features.tsx
  22. 11 11
      client/components/FeaturesItem.tsx
  23. 36 60
      client/components/Footer.tsx
  24. 2 2
      client/components/Header.tsx
  25. 3 1
      client/components/Icon/Icon.tsx
  26. 1 1
      client/components/Icon/Lock.tsx
  27. 26 28
      client/components/LinksTable.tsx
  28. 4 4
      client/components/NeedToLogin.tsx
  29. 4 3
      client/components/PageLoading.tsx
  30. 9 9
      client/components/Settings/SettingsApi.tsx
  31. 8 12
      client/components/Settings/SettingsBan.tsx
  32. 25 32
      client/components/Settings/SettingsDomain.tsx
  33. 6 5
      client/components/Settings/SettingsPassword.tsx
  34. 59 55
      client/components/Shortener.tsx
  35. 0 146
      client/components/Stats/Stats.tsx
  36. 0 103
      client/components/Stats/StatsCharts/StatsCharts.tsx
  37. 0 1
      client/components/Stats/StatsCharts/index.tsx
  38. 0 75
      client/components/Stats/StatsCharts/withTitle.tsx
  39. 0 40
      client/components/Stats/StatsError.tsx
  40. 0 101
      client/components/Stats/StatsHead.tsx
  41. 0 1
      client/components/Stats/index.tsx
  42. 8 6
      client/components/Table.ts
  43. 0 149
      client/components/Table/TBody/TBody.tsx
  44. 0 65
      client/components/Table/TBody/TBodyButton.tsx
  45. 0 80
      client/components/Table/TBody/TBodyCount.tsx
  46. 0 46
      client/components/Table/TBody/TBodyShortUrl.tsx
  47. 0 1
      client/components/Table/TBody/index.tsx
  48. 0 53
      client/components/Table/THead/THead.tsx
  49. 0 1
      client/components/Table/THead/index.tsx
  50. 0 65
      client/components/TableNav.tsx
  51. 0 168
      client/components/TableOptions.tsx
  52. 41 5
      client/components/Text.tsx
  53. 29 1
      client/consts/consts.ts
  54. 3 3
      client/helpers/analytics.ts
  55. 33 13
      client/pages/_app.tsx
  56. 3 2
      client/pages/_document.tsx
  57. 15 18
      client/pages/banned.tsx
  58. 3 3
      client/pages/index.tsx
  59. 13 11
      client/pages/login.tsx
  60. 14 20
      client/pages/report.tsx
  61. 11 10
      client/pages/reset-password.tsx
  62. 15 21
      client/pages/settings.tsx
  63. 183 8
      client/pages/stats.tsx
  64. 6 10
      client/pages/terms.tsx
  65. 12 13
      client/pages/url-info.tsx
  66. 15 17
      client/pages/url-password.tsx
  67. 26 22
      client/pages/verify.tsx
  68. 0 91
      client/reducers/__test__/auth.js
  69. 0 134
      client/reducers/__test__/error.js
  70. 0 211
      client/reducers/__test__/loading.js
  71. 0 92
      client/reducers/__test__/settings.js
  72. 0 197
      client/reducers/__test__/url.js
  73. 0 32
      client/reducers/auth.js
  74. 0 49
      client/reducers/error.js
  75. 0 17
      client/reducers/index.js
  76. 0 73
      client/reducers/loading.js
  77. 0 38
      client/reducers/settings.js
  78. 0 49
      client/reducers/url.js
  79. 0 6
      client/redux-store/index.js
  80. 0 9
      client/redux-store/store.dev.js
  81. 0 7
      client/redux-store/store.prod.js
  82. 9 0
      client/store/auth.ts
  83. 0 50
      client/with-redux-store.js
  84. BIN
      dump.rdb

+ 0 - 172
client/actions/__test__/auth.js

@@ -1,172 +0,0 @@
-import nock from 'nock';
-import sinon from 'sinon';
-import { expect } from 'chai';
-import cookie from 'js-cookie';
-import thunk from 'redux-thunk';
-import Router from 'next/router';
-import configureMockStore from 'redux-mock-store';
-
-import { signupUser, loginUser, logoutUser, renewAuthUser } from '../auth';
-import {
-  SIGNUP_LOADING,
-  SENT_VERIFICATION,
-  LOGIN_LOADING,
-  AUTH_RENEW,
-  AUTH_USER,
-  SET_DOMAIN,
-  SHOW_PAGE_LOADING,
-  UNAUTH_USER
-} from '../actionTypes';
-
-const middlewares = [thunk];
-const mockStore = configureMockStore(middlewares);
-
-describe('auth actions', () => {
-  const jwt = {
-    domain: '',
-    exp: 1529137738725,
-    iat: 1529137738725,
-    iss: 'ApiAuth',
-    sub: 'test@mail.com',
-  };
-  const email = 'test@mail.com';
-  const password = 'password';
-  const token =
-    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBcGlBdXRoIiwic3ViIjoidGVzdEBtYWlsLmNvbSIsImRvbWFpbiI6IiIsImlhdCI6MTUyOTEzNzczODcyNSwiZXhwIjoxNTI5MTM3NzM4NzI1fQ.tdI7r11bmSCUmbcJBBKIDt7Hkb7POLMRl8VNJv_8O_s';
-
-  describe('#signupUser()', () => {
-    it('should dispatch SENT_VERIFICATION when signing up user has been done', done => {
-      nock('http://localhost')
-        .post('/api/auth/signup')
-        .reply(200, {
-          email,
-          message: 'Verification email has been sent.'
-        });
-
-      const store = mockStore({});
-
-      const expectedActions = [
-        { type: SIGNUP_LOADING },
-        {
-          type: SENT_VERIFICATION,
-          payload: email
-        }
-      ];
-
-      store
-        .dispatch(signupUser(email, password))
-        .then(() => {
-          expect(store.getActions()).to.deep.equal(expectedActions);
-          done();
-        })
-        .catch(error => done(error));
-    });
-  });
-
-  describe('#loginUser()', () => {
-    it('should dispatch AUTH_USER when logining user has been done', done => {
-      const pushStub = sinon.stub(Router, 'push');
-      pushStub.withArgs('/').returns('/');
-      const expectedRoute = '/';
-
-      nock('http://localhost')
-        .post('/api/auth/login')
-        .reply(200, {
-          token
-        });
-
-      const store = mockStore({});
-
-      const expectedActions = [
-        { type: LOGIN_LOADING },
-        { type: AUTH_RENEW },
-        {
-          type: AUTH_USER,
-          payload: jwt
-        },
-        {
-          type: SET_DOMAIN,
-          payload: {
-            customDomain: '',
-          }
-        },
-        { type: SHOW_PAGE_LOADING }
-      ];
-
-      store
-        .dispatch(loginUser(email, password))
-        .then(() => {
-          expect(store.getActions()).to.deep.equal(expectedActions);
-
-          pushStub.restore();
-          sinon.assert.calledWith(pushStub, expectedRoute);
-
-          done();
-        })
-        .catch(error => done(error));
-    });
-  });
-
-  describe('#logoutUser()', () => {
-    it('should dispatch UNAUTH_USER when loging out user has been done', () => {
-      const pushStub = sinon.stub(Router, 'push');
-      pushStub.withArgs('/login').returns('/login');
-      const expectedRoute = '/login';
-
-      const store = mockStore({});
-
-      const expectedActions = [
-        { type: SHOW_PAGE_LOADING },
-        { type: UNAUTH_USER }
-      ];
-
-      store.dispatch(logoutUser());
-      expect(store.getActions()).to.deep.equal(expectedActions);
-
-      pushStub.restore();
-      sinon.assert.calledWith(pushStub, expectedRoute);
-    });
-  });
-
-  describe('#renewAuthUser()', () => {
-    it('should dispatch AUTH_RENEW when renewing auth user has been done', done => {
-      const cookieStub = sinon.stub(cookie, 'get');
-      cookieStub.withArgs('token').returns(token);
-
-      nock('http://localhost', {
-        reqheaders: {
-          Authorization: token
-        }
-      })
-        .post('/api/auth/renew')
-        .reply(200, {
-          token
-        });
-
-      const store = mockStore({ auth: { renew: false } });
-
-      const expectedActions = [
-        { type: AUTH_RENEW },
-        {
-          type: AUTH_USER,
-          payload: jwt
-        },
-        {
-          type: SET_DOMAIN,
-          payload: {
-            customDomain: '',
-          }
-        }
-      ];
-
-      store
-        .dispatch(renewAuthUser())
-        .then(() => {
-          expect(store.getActions()).to.deep.equal(expectedActions);
-          cookieStub.restore();
-          done();
-        })
-        .catch(error => done(error));
-    });
-  });
-});

+ 0 - 176
client/actions/__test__/settings.js

@@ -1,176 +0,0 @@
-import nock from 'nock';
-import sinon from 'sinon';
-import { expect } from 'chai';
-import cookie from 'js-cookie';
-import thunk from 'redux-thunk';
-import configureMockStore from 'redux-mock-store';
-
-import {
-  getUserSettings,
-  setCustomDomain,
-  deleteCustomDomain,
-  generateApiKey
-} from '../settings';
-import {
-  DELETE_DOMAIN,
-  DOMAIN_LOADING,
-  API_LOADING,
-  SET_DOMAIN,
-  SET_APIKEY
-} from '../actionTypes';
-
-const middlewares = [thunk];
-const mockStore = configureMockStore(middlewares);
-
-describe('settings actions', () => {
-  const token =
-    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBcGlBdXRoIiwic3ViIjoidGVzdEBtYWlsLmNvbSIsImRvbWFpbiI6IiIsImlhdCI6MTUyOTEzNzczODcyNSwiZXhwIjoxNTI5MTM3NzM4NzI1fQ.tdI7r11bmSCUmbcJBBKIDt7Hkb7POLMRl8VNJv_8O_s';
-
-  let cookieStub;
-
-  beforeEach(() => {
-    cookieStub = sinon.stub(cookie, 'get');
-    cookieStub.withArgs('token').returns(token);
-  });
-
-  afterEach(() => {
-    cookieStub.restore();
-  });
-
-  describe('#getUserSettings()', () => {
-    it('should dispatch SET_APIKEY and SET_DOMAIN when getting user settings have been done', done => {
-      const apikey = '123';
-      const customDomain = 'test.com';
-      const homepage = '';
-
-      nock('http://localhost', {
-        reqheaders: {
-          Authorization: token
-        }
-      })
-        .get('/api/auth/usersettings')
-        .reply(200, { apikey, customDomain, homepage });
-
-      const store = mockStore({});
-
-      const expectedActions = [
-        {
-          type: SET_DOMAIN,
-          payload: {
-            customDomain,
-            homepage: '',
-          }
-        },
-        {
-          type: SET_APIKEY,
-          payload: apikey
-        }
-      ];
-
-      store
-        .dispatch(getUserSettings())
-        .then(() => {
-          expect(store.getActions()).to.deep.equal(expectedActions);
-          done();
-        })
-        .catch(error => done(error));
-    });
-  });
-
-  describe('#setCustomDomain()', () => {
-    it('should dispatch SET_DOMAIN when setting custom domain has been done', done => {
-      const customDomain = 'test.com';
-      const homepage = '';
-
-      nock('http://localhost', {
-        reqheaders: {
-          Authorization: token
-        }
-      })
-        .post('/api/url/customdomain')
-        .reply(200, { customDomain, homepage });
-
-      const store = mockStore({});
-
-      const expectedActions = [
-        { type: DOMAIN_LOADING },
-        {
-          type: SET_DOMAIN,
-          payload: {
-            customDomain,
-            homepage: '',
-          }
-        }
-      ];
-
-      store
-        .dispatch(setCustomDomain({
-          customDomain,
-          homepage: '',
-        }))
-        .then(() => {
-          expect(store.getActions()).to.deep.equal(expectedActions);
-          done();
-        })
-        .catch(error => done(error));
-    });
-  });
-
-  describe('#deleteCustomDomain()', () => {
-    it('should dispatch DELETE_DOMAIN when deleting custom domain has been done', done => {
-      const customDomain = 'test.com';
-
-      nock('http://localhost', {
-        reqheaders: {
-          Authorization: token
-        }
-      })
-        .delete('/api/url/customdomain')
-        .reply(200, { customDomain });
-
-      const store = mockStore({});
-
-      const expectedActions = [{ type: DELETE_DOMAIN }];
-
-      store
-        .dispatch(deleteCustomDomain(customDomain))
-        .then(() => {
-          expect(store.getActions()).to.deep.equal(expectedActions);
-          done();
-        })
-        .catch(error => done(error));
-    });
-  });
-
-  describe('#generateApiKey()', () => {
-    it('should dispatch SET_APIKEY when generating api key has been done', done => {
-      const apikey = '123';
-
-      nock('http://localhost', {
-        reqheaders: {
-          Authorization: token
-        }
-      })
-        .post('/api/auth/generateapikey')
-        .reply(200, { apikey });
-
-      const store = mockStore({});
-
-      const expectedActions = [
-        { type: API_LOADING },
-        {
-          type: SET_APIKEY,
-          payload: apikey
-        }
-      ];
-
-      store
-        .dispatch(generateApiKey())
-        .then(() => {
-          expect(store.getActions()).to.deep.equal(expectedActions);
-          done();
-        })
-        .catch(error => done(error));
-    });
-  });
-});

+ 0 - 159
client/actions/__test__/url.js

@@ -1,159 +0,0 @@
-import nock from 'nock';
-import sinon from 'sinon';
-import { expect } from 'chai';
-import cookie from 'js-cookie';
-import thunk from 'redux-thunk';
-import configureMockStore from 'redux-mock-store';
-
-import { createShortUrl, getUrlsList, deleteShortUrl } from '../url';
-import { ADD_URL, LIST_URLS, DELETE_URL, TABLE_LOADING } from '../actionTypes';
-
-const middlewares = [thunk];
-const mockStore = configureMockStore(middlewares);
-
-describe('url actions', () => {
-  const token =
-    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBcGlBdXRoIiwic3ViIjoidGVzdEBtYWlsLmNvbSIsImRvbWFpbiI6IiIsImlhdCI6MTUyOTEzNzczODcyNSwiZXhwIjoxNTI5MTM3NzM4NzI1fQ.tdI7r11bmSCUmbcJBBKIDt7Hkb7POLMRl8VNJv_8O_s';
-
-  let cookieStub;
-
-  beforeEach(() => {
-    cookieStub = sinon.stub(cookie, 'get');
-    cookieStub.withArgs('token').returns(token);
-  });
-
-  afterEach(() => {
-    cookieStub.restore();
-  });
-
-  describe('#createShortUrl()', () => {
-    it('should dispatch ADD_URL when creating short url has been done', done => {
-      const url = 'test.com';
-      const mockedItems = {
-        createdAt: '2018-06-16T15:40:35.243Z',
-        id: '123',
-        target: url,
-        password: false,
-        reuse: false,
-        shortLink: 'http://kutt.it/123'
-      };
-
-      nock('http://localhost', {
-        reqheaders: {
-          Authorization: token
-        }
-      })
-        .post('/api/url/submit')
-        .reply(200, mockedItems);
-
-      const store = mockStore({});
-
-      const expectedActions = [
-        {
-          type: ADD_URL,
-          payload: mockedItems
-        }
-      ];
-
-      store
-        .dispatch(createShortUrl(url))
-        .then(() => {
-          expect(store.getActions()).to.deep.equal(expectedActions);
-          done();
-        })
-        .catch(error => done(error));
-    });
-  });
-
-  describe('#getUrlsList()', () => {
-    it('should dispatch LIST_URLS when getting urls list has been done', done => {
-      const mockedQueryParams = {
-        isShortened: false,
-        count: 10,
-        countAll: 1,
-        page: 1,
-        search: ''
-      };
-
-      const mockedItems = {
-        list: [
-          {
-            createdAt: '2018-06-16T16:45:28.607Z',
-            id: 'UkEs33',
-            target: 'https://kutt.it/',
-            password: false,
-            count: 0,
-            shortLink: 'http://test.com/UkEs33'
-          }
-        ],
-        countAll: 1
-      };
-
-      nock('http://localhost', {
-        reqheaders: {
-          Authorization: token
-        }
-      })
-        .get('/api/url/geturls')
-        .query(mockedQueryParams)
-        .reply(200, mockedItems);
-
-      const store = mockStore({ url: { list: [], ...mockedQueryParams } });
-
-      const expectedActions = [
-        { type: TABLE_LOADING },
-        {
-          type: LIST_URLS,
-          payload: mockedItems
-        }
-      ];
-
-      store
-        .dispatch(getUrlsList())
-        .then(() => {
-          expect(store.getActions()).to.deep.equal(expectedActions);
-          done();
-        })
-        .catch(error => done(error));
-    });
-  });
-
-  describe('#deleteShortUrl()', () => {
-    it('should dispatch DELETE_URL when deleting short url has been done', done => {
-      const id = '123';
-      const mockedItems = [
-        {
-          createdAt: '2018-06-16T15:40:35.243Z',
-          id: '123',
-          target: 'test.com',
-          password: false,
-          reuse: false,
-          shortLink: 'http://kutt.it/123'
-        }
-      ];
-
-      nock('http://localhost', {
-        reqheaders: {
-          Authorization: token
-        }
-      })
-        .post('/api/url/deleteurl')
-        .reply(200, { message: 'Short URL deleted successfully' });
-
-      const store = mockStore({ url: { list: mockedItems } });
-
-      const expectedActions = [
-        { type: TABLE_LOADING },
-        { type: DELETE_URL, payload: id }
-      ];
-
-      store
-        .dispatch(deleteShortUrl({ id }))
-        .then(() => {
-          expect(store.getActions()).to.deep.equal(expectedActions);
-          done();
-        })
-        .catch(error => done(error));
-    });
-  });
-});

+ 0 - 32
client/actions/actionTypes.js

@@ -1,32 +0,0 @@
-/* Homepage input actions */
-export const ADD_URL = 'ADD_URL';
-export const UPDATE_URL = 'UPDATE_URL';
-export const UPDATE_URL_LIST = 'UPDATE_URL_LIST';
-export const LIST_URLS = 'LIST_URLS';
-export const DELETE_URL = 'DELETE_URL';
-export const SHORTENER_ERROR = 'SHORTENER_ERROR';
-export const SHORTENER_LOADING = 'SHORTENER_LOADING';
-export const TABLE_LOADING = 'TABLE_LOADING';
-
-/* Page loading actions */
-export const SHOW_PAGE_LOADING = 'SHOW_PAGE_LOADING';
-export const HIDE_PAGE_LOADING = 'HIDE_PAGE_LOADING';
-
-/* Login & signup actions */
-export const AUTH_USER = 'AUTH_USER';
-export const AUTH_RENEW = 'AUTH_RENEW';
-export const UNAUTH_USER = 'UNAUTH_USER';
-export const SENT_VERIFICATION = 'SENT_VERIFICATION';
-export const AUTH_ERROR = 'AUTH_ERROR';
-export const LOGIN_LOADING = 'LOGIN_LOADING';
-export const SIGNUP_LOADING = 'SIGNUP_LOADING';
-
-/* Settings actions */
-export const SET_DOMAIN = 'SET_DOMAIN';
-export const SET_APIKEY = 'SET_APIKEY';
-export const DELETE_DOMAIN = 'DELETE_DOMAIN';
-export const DOMAIN_LOADING = 'DOMAIN_LOADING';
-export const API_LOADING = 'API_LOADING';
-export const DOMAIN_ERROR = 'DOMAIN_ERROR';
-export const SHOW_DOMAIN_INPUT = 'SHOW_DOMAIN_INPUT';
-export const BAN_URL = 'BAN_URL';

+ 0 - 92
client/actions/auth.js

@@ -1,92 +0,0 @@
-import Router from "next/router";
-import axios from "axios";
-import cookie from "js-cookie";
-import decodeJwt from "jwt-decode";
-import {
-  SET_DOMAIN,
-  SHOW_PAGE_LOADING,
-  HIDE_PAGE_LOADING,
-  AUTH_USER,
-  UNAUTH_USER,
-  SENT_VERIFICATION,
-  AUTH_ERROR,
-  LOGIN_LOADING,
-  SIGNUP_LOADING,
-  AUTH_RENEW
-} from "./actionTypes";
-
-const setDomain = payload => ({ type: SET_DOMAIN, payload });
-
-export const showPageLoading = () => ({ type: SHOW_PAGE_LOADING });
-export const hidePageLoading = () => ({ type: HIDE_PAGE_LOADING });
-export const authUser = payload => ({ type: AUTH_USER, payload });
-export const unauthUser = () => ({ type: UNAUTH_USER });
-export const sentVerification = payload => ({
-  type: SENT_VERIFICATION,
-  payload
-});
-export const showAuthError = payload => ({ type: AUTH_ERROR, payload });
-export const showLoginLoading = () => ({ type: LOGIN_LOADING });
-export const showSignupLoading = () => ({ type: SIGNUP_LOADING });
-export const authRenew = () => ({ type: AUTH_RENEW });
-
-export const signupUser = payload => async dispatch => {
-  dispatch(showSignupLoading());
-  try {
-    const {
-      data: { email }
-    } = await axios.post("/api/auth/signup", payload);
-    dispatch(sentVerification(email));
-  } catch ({ response }) {
-    dispatch(showAuthError(response.data.error));
-  }
-};
-
-export const loginUser = payload => async dispatch => {
-  dispatch(showLoginLoading());
-  try {
-    const {
-      data: { token }
-    } = await axios.post("/api/auth/login", payload);
-    cookie.set("token", token, { expires: 7 });
-    dispatch(authRenew());
-    dispatch(authUser(decodeJwt(token)));
-    dispatch(setDomain({ customDomain: decodeJwt(token).domain }));
-    dispatch(showPageLoading());
-    Router.push("/");
-  } catch ({ response }) {
-    dispatch(showAuthError(response.data.error));
-  }
-};
-
-export const logoutUser = () => dispatch => {
-  dispatch(showPageLoading());
-  cookie.remove("token");
-  dispatch(unauthUser());
-  Router.push("/login");
-};
-
-export const renewAuthUser = () => async (dispatch, getState) => {
-  if (getState().auth.renew) {
-    return;
-  }
-
-  const options = {
-    method: "POST",
-    headers: { Authorization: cookie.get("token") },
-    url: "/api/auth/renew"
-  };
-
-  try {
-    const {
-      data: { token }
-    } = await axios(options);
-    cookie.set("token", token, { expires: 7 });
-    dispatch(authRenew());
-    dispatch(authUser(decodeJwt(token)));
-    dispatch(setDomain({ customDomain: decodeJwt(token).domain }));
-  } catch (error) {
-    cookie.remove("token");
-    dispatch(unauthUser());
-  }
-};

+ 0 - 3
client/actions/index.js

@@ -1,3 +0,0 @@
-export * from './url';
-export * from './settings';
-export * from './auth';

+ 0 - 89
client/actions/settings.js

@@ -1,89 +0,0 @@
-import axios from "axios";
-import cookie from "js-cookie";
-import {
-  DELETE_DOMAIN,
-  DOMAIN_ERROR,
-  DOMAIN_LOADING,
-  API_LOADING,
-  SET_DOMAIN,
-  SET_APIKEY,
-  SHOW_DOMAIN_INPUT,
-  BAN_URL
-} from "./actionTypes";
-
-const deleteDomain = () => ({ type: DELETE_DOMAIN });
-const setDomainError = payload => ({ type: DOMAIN_ERROR, payload });
-const showDomainLoading = () => ({ type: DOMAIN_LOADING });
-const showApiLoading = () => ({ type: API_LOADING });
-const urlBanned = () => ({ type: BAN_URL });
-
-export const setDomain = payload => ({ type: SET_DOMAIN, payload });
-export const setApiKey = payload => ({ type: SET_APIKEY, payload });
-export const showDomainInput = () => ({ type: SHOW_DOMAIN_INPUT });
-
-export const getUserSettings = () => async dispatch => {
-  try {
-    const {
-      data: { apikey, customDomain, homepage }
-    } = await axios.get("/api/auth/usersettings", {
-      headers: { Authorization: cookie.get("token") }
-    });
-    dispatch(setDomain({ customDomain, homepage }));
-    dispatch(setApiKey(apikey));
-  } catch (error) {
-    //
-  }
-};
-
-export const setCustomDomain = params => async dispatch => {
-  dispatch(showDomainLoading());
-  try {
-    const {
-      data: { customDomain, homepage }
-    } = await axios.post("/api/url/customdomain", params, {
-      headers: { Authorization: cookie.get("token") }
-    });
-    dispatch(setDomain({ customDomain, homepage }));
-  } catch ({ response }) {
-    dispatch(setDomainError(response.data.error));
-  }
-};
-
-export const deleteCustomDomain = () => dispatch =>
-  new Promise(async (res, rej) => {
-    try {
-      await axios.delete("/api/url/customdomain", {
-        headers: { Authorization: cookie.get("token") }
-      });
-      setTimeout(() => {
-        res();
-      }, 4000);
-      dispatch(deleteDomain());
-    } catch ({ response }) {
-      dispatch(setDomainError(response.data.error));
-    }
-  });
-
-export const generateApiKey = () => async dispatch => {
-  dispatch(showApiLoading());
-  try {
-    const { data } = await axios.post("/api/auth/generateapikey", null, {
-      headers: { Authorization: cookie.get("token") }
-    });
-    dispatch(setApiKey(data.apikey));
-  } catch (error) {
-    //
-  }
-};
-
-export const banUrl = params => async dispatch => {
-  try {
-    const { data } = await axios.post("/api/url/admin/ban", params, {
-      headers: { Authorization: cookie.get("token") }
-    });
-    dispatch(urlBanned());
-    return data.message;
-  } catch ({ response }) {
-    return Promise.reject(response.data && response.data.error);
-  }
-};

+ 0 - 71
client/actions/url.js

@@ -1,71 +0,0 @@
-import axios from "axios";
-import cookie from "js-cookie";
-import {
-  ADD_URL,
-  LIST_URLS,
-  UPDATE_URL_LIST,
-  DELETE_URL,
-  SHORTENER_LOADING,
-  TABLE_LOADING,
-  SHORTENER_ERROR
-} from "./actionTypes";
-
-const addUrl = payload => ({ type: ADD_URL, payload });
-const listUrls = payload => ({ type: LIST_URLS, payload });
-const updateUrlList = payload => ({ type: UPDATE_URL_LIST, payload });
-const deleteUrl = payload => ({ type: DELETE_URL, payload });
-const showTableLoading = () => ({ type: TABLE_LOADING });
-
-export const setShortenerFormError = payload => ({
-  type: SHORTENER_ERROR,
-  payload
-});
-
-export const showShortenerLoading = () => ({ type: SHORTENER_LOADING });
-
-export const createShortUrl = params => async dispatch => {
-  try {
-    const { data } = await axios.post("/api/url/submit", params, {
-      headers: { Authorization: cookie.get("token") }
-    });
-    dispatch(addUrl(data));
-  } catch ({ response }) {
-    dispatch(setShortenerFormError(response.data.error));
-  }
-};
-
-export const getUrlsList = params => async (dispatch, getState) => {
-  if (params) {
-    dispatch(updateUrlList(params));
-  }
-
-  dispatch(showTableLoading());
-
-  const { url } = getState();
-  const { list, ...queryParams } = url;
-  const query = Object.keys(queryParams).reduce(
-    (string, item) => `${string + item}=${queryParams[item]}&`,
-    "?"
-  );
-
-  try {
-    const { data } = await axios.get(`/api/url/geturls${query}`, {
-      headers: { Authorization: cookie.get("token") }
-    });
-    dispatch(listUrls(data));
-  } catch (error) {
-    //
-  }
-};
-
-export const deleteShortUrl = params => async dispatch => {
-  dispatch(showTableLoading());
-  try {
-    await axios.post("/api/url/deleteurl", params, {
-      headers: { Authorization: cookie.get("token") }
-    });
-    dispatch(deleteUrl(params.id));
-  } catch ({ response }) {
-    dispatch(setShortenerFormError(response.data.error));
-  }
-};

+ 13 - 5
client/components/ALink.tsx

@@ -1,11 +1,13 @@
-import styled from "styled-components";
 import { Box, BoxProps } from "reflexbox/styled-components";
+import styled, { css } from "styled-components";
+import { ifProp } from "styled-tools";
 
 interface Props extends BoxProps {
   href?: string;
   title?: string;
   target?: string;
   rel?: string;
+  forButton?: boolean;
 }
 const ALink = styled(Box).attrs({
   as: "a"
@@ -16,13 +18,19 @@ const ALink = styled(Box).attrs({
   text-decoration: none;
   transition: all 0.2s ease-out;
 
-  :hover {
-    border-bottom-color: #2196f3;
-  }
+  ${ifProp(
+    { forButton: false },
+    css`
+      :hover {
+        border-bottom-color: #2196f3;
+      }
+    `
+  )}
 `;
 
 ALink.defaultProps = {
-  pb: "1px"
+  pb: "1px",
+  forButton: false
 };
 
 export default ALink;

+ 40 - 0
client/components/AppWrapper.tsx

@@ -0,0 +1,40 @@
+import { Flex } from "reflexbox/styled-components";
+import styled from "styled-components";
+import React from "react";
+
+import { useStoreState } from "../store";
+import PageLoading from "./PageLoading";
+import Header from "./Header";
+
+const Wrapper = styled(Flex)`
+  input {
+    filter: none;
+  }
+
+  * {
+    box-sizing: border-box;
+  }
+
+  *::-moz-focus-inner {
+    border: none;
+  }
+`;
+
+const AppWrapper = ({ children }: { children: any }) => {
+  const loading = useStoreState(s => s.loading.loading);
+
+  return (
+    <Wrapper
+      minHeight="100vh"
+      width={1}
+      flex="0 0 auto"
+      alignItems="center"
+      flexDirection="column"
+    >
+      <Header />
+      {loading ? <PageLoading /> : children}
+    </Wrapper>
+  );
+};
+
+export default AppWrapper;

+ 0 - 87
client/components/BodyWrapper.tsx

@@ -1,87 +0,0 @@
-import React, { FC, useEffect } from "react";
-import { bindActionCreators } from "redux";
-import { connect } from "react-redux";
-import styled from "styled-components";
-import cookie from "js-cookie";
-import { Flex } from "reflexbox/styled-components";
-
-import Header from "./Header";
-import PageLoading from "./PageLoading";
-import { renewAuthUser } from "../actions";
-import { initGA, logPageView } from "../helpers/analytics";
-import { useStoreState } from "../store";
-
-interface Props {
-  norenew?: boolean;
-  pageLoading: boolean;
-  renewAuthUser: any; // TODO: better typing;
-}
-
-const Wrapper = styled.div`
-  position: relative;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  box-sizing: border-box;
-
-  * {
-    box-sizing: border-box;
-  }
-
-  *::-moz-focus-inner {
-    border: none;
-  }
-
-  @media only screen and (max-width: 448px) {
-    font-size: 14px;
-  }
-`;
-
-const BodyWrapper: FC<Props> = ({ children, norenew, renewAuthUser }) => {
-  const loading = useStoreState(s => s.loading.loading);
-
-  useEffect(() => {
-    // FIXME: types bro
-    if (process.env.GOOGLE_ANALYTICS) {
-      if (!(window as any).GA_INITIALIZED) {
-        initGA();
-        (window as any).GA_INITIALIZED = true;
-      }
-      logPageView();
-    }
-
-    const token = cookie.get("token");
-    if (!token || norenew) return undefined;
-    renewAuthUser(token);
-  }, []);
-
-  const content = loading ? <PageLoading /> : children;
-  return (
-    <Wrapper>
-      <Flex
-        minHeight="100vh"
-        width={1}
-        flex="0 0 auto"
-        alignItems="center"
-        flexDirection="column"
-      >
-        <Header />
-        {content}
-      </Flex>
-    </Wrapper>
-  );
-};
-
-BodyWrapper.defaultProps = {
-  norenew: false
-};
-
-const mapDispatchToProps = dispatch => ({
-  renewAuthUser: bindActionCreators(renewAuthUser, dispatch)
-});
-
-export default connect(
-  null,
-  mapDispatchToProps
-)(BodyWrapper);

+ 6 - 17
client/components/Button.tsx

@@ -9,7 +9,7 @@ import SVG from "react-inlinesvg";
 import { spin } from "../helpers/animations";
 
 interface Props extends BoxProps {
-  color?: "purple" | "gray" | "blue";
+  color?: "purple" | "gray" | "blue" | "red";
   disabled?: boolean;
   icon?: string; // TODO: better typing
   isRound?: boolean;
@@ -27,16 +27,19 @@ const StyledButton = styled(Flex)<Props>`
   word-break: keep-all;
   color: ${switchProp(prop("color", "blue"), {
     blue: "white",
+    red: "white",
     purple: "white",
     gray: "#444"
   })};
   background: ${switchProp(prop("color", "blue"), {
     blue: "linear-gradient(to right, #42a5f5, #2979ff)",
+    red: "linear-gradient(to right, #ee3b3b, #e11c1c)",
     purple: "linear-gradient(to right, #7e57c2, #6200ea)",
     gray: "linear-gradient(to right, #e0e0e0, #bdbdbd)"
   })};
   box-shadow: ${switchProp(prop("color", "blue"), {
     blue: "0 5px 6px rgba(66, 165, 245, 0.5)",
+    red: "0 5px 6px rgba(168, 45, 45, 0.5)",
     purple: "0 5px 6px rgba(81, 45, 168, 0.5)",
     gray: "0 5px 6px rgba(160, 160, 160, 0.5)"
   })};
@@ -51,26 +54,12 @@ const StyledButton = styled(Flex)<Props>`
     outline: none;
     box-shadow: ${switchProp(prop("color", "blue"), {
       blue: "0 6px 15px rgba(66, 165, 245, 0.5)",
+      red: "0 6px 15px rgba(168, 45, 45, 0.5)",
       purple: "0 6px 15px rgba(81, 45, 168, 0.5)",
       gray: "0 6px 15px rgba(160, 160, 160, 0.5)"
     })};
     transform: translateY(-2px) scale(1.02, 1.02);
   }
-
-  a & {
-    text-decoration: none;
-    border: none;
-  }
-
-  ${ifProp(
-    { size: "big" },
-    css`
-      height: 56px;
-      @media only screen and (max-width: 448px) {
-        height: 40px;
-      }
-    `
-  )}
 `;
 
 const Icon = styled(SVG)`
@@ -162,7 +151,7 @@ export const NavButton = styled(Flex)<NavButtonProps>`
   ${ifProp(
     "disabled",
     css`
-      background-color: #f5f5f5;
+      background-color: #f6f6f6;
       box-shadow: 0 0px 5px rgba(150, 150, 150, 0.1);
       cursor: default;
 

+ 17 - 19
client/components/Stats/StatsCharts/Area.tsx → client/components/Charts/Area.tsx

@@ -1,8 +1,8 @@
-import React, { FC } from 'react';
-import subHours from 'date-fns/subHours';
-import subDays from 'date-fns/subDays';
-import subMonths from 'date-fns/subMonths';
-import formatDate from 'date-fns/format';
+import subMonths from "date-fns/subMonths";
+import subHours from "date-fns/subHours";
+import formatDate from "date-fns/format";
+import subDays from "date-fns/subDays";
+import React, { FC } from "react";
 import {
   AreaChart,
   Area,
@@ -10,10 +10,8 @@ import {
   YAxis,
   CartesianGrid,
   ResponsiveContainer,
-  Tooltip,
-} from 'recharts';
-
-import withTitle from './withTitle';
+  Tooltip
+} from "recharts";
 
 interface Props {
   data: number[];
@@ -24,22 +22,22 @@ const ChartArea: FC<Props> = ({ data: rawData, period }) => {
   const now = new Date();
   const getDate = index => {
     switch (period) {
-      case 'allTime':
+      case "allTime":
         return formatDate(
           subMonths(now, rawData.length - index - 1),
-          'MMM yyy'
+          "MMM yyy"
         );
-      case 'lastDay':
-        return formatDate(subHours(now, rawData.length - index - 1), 'HH:00');
-      case 'lastMonth':
-      case 'lastWeek':
+      case "lastDay":
+        return formatDate(subHours(now, rawData.length - index - 1), "HH:00");
+      case "lastMonth":
+      case "lastWeek":
       default:
-        return formatDate(subDays(now, rawData.length - index - 1), 'MMM dd');
+        return formatDate(subDays(now, rawData.length - index - 1), "MMM dd");
     }
   };
   const data = rawData.map((view, index) => ({
     name: getDate(index),
-    views: view,
+    views: view
   }));
 
   return (
@@ -53,7 +51,7 @@ const ChartArea: FC<Props> = ({ data: rawData, period }) => {
           top: 16,
           right: 0,
           left: 0,
-          bottom: 16,
+          bottom: 16
         }}
       >
         <defs>
@@ -79,4 +77,4 @@ const ChartArea: FC<Props> = ({ data: rawData, period }) => {
   );
 };
 
-export default withTitle(ChartArea);
+export default ChartArea;

+ 5 - 7
client/components/Stats/StatsCharts/Bar.tsx → client/components/Charts/Bar.tsx

@@ -1,4 +1,4 @@
-import React, { FC } from 'react';
+import React, { FC } from "react";
 import {
   BarChart,
   Bar,
@@ -6,10 +6,8 @@ import {
   YAxis,
   CartesianGrid,
   Tooltip,
-  ResponsiveContainer,
-} from 'recharts';
-
-import withTitle from './withTitle';
+  ResponsiveContainer
+} from "recharts";
 
 interface Props {
   data: any[]; // TODO: types
@@ -27,7 +25,7 @@ const ChartBar: FC<Props> = ({ data }) => (
         top: 0,
         right: 0,
         left: 24,
-        bottom: 0,
+        bottom: 0
       }}
     >
       <XAxis type="number" dataKey="value" />
@@ -39,4 +37,4 @@ const ChartBar: FC<Props> = ({ data }) => (
   </ResponsiveContainer>
 );
 
-export default withTitle(ChartBar);
+export default ChartBar;

+ 5 - 8
client/components/Stats/StatsCharts/Pie.tsx → client/components/Charts/Pie.tsx

@@ -1,13 +1,10 @@
-import React, { FC } from 'react';
-import { PieChart, Pie, Tooltip, ResponsiveContainer } from 'recharts';
-import withTitle from './withTitle';
+import { PieChart, Pie, Tooltip, ResponsiveContainer } from "recharts";
+import React, { FC } from "react";
 
 interface Props {
   data: any[]; // TODO: types
 }
 
-const renderCustomLabel = ({ name }) => name;
-
 const ChartPie: FC<Props> = ({ data }) => (
   <ResponsiveContainer
     width="100%"
@@ -18,7 +15,7 @@ const ChartPie: FC<Props> = ({ data }) => (
         top: window.innerWidth < 468 ? 56 : 0,
         right: window.innerWidth < 468 ? 56 : 0,
         bottom: window.innerWidth < 468 ? 56 : 0,
-        left: window.innerWidth < 468 ? 56 : 0,
+        left: window.innerWidth < 468 ? 56 : 0
       }}
     >
       <Pie
@@ -26,11 +23,11 @@ const ChartPie: FC<Props> = ({ data }) => (
         dataKey="value"
         innerRadius={window.innerWidth < 468 ? 20 : 80}
         fill="#B39DDB"
-        label={renderCustomLabel}
+        label={({ name }) => name}
       />
       <Tooltip />
     </PieChart>
   </ResponsiveContainer>
 );
 
-export default withTitle(ChartPie);
+export default ChartPie;

+ 3 - 0
client/components/Charts/index.tsx

@@ -0,0 +1,3 @@
+export { default as Area } from "./Area";
+export { default as Bar } from "./Bar";
+export { default as Pie } from "./Pie";

+ 3 - 3
client/components/Checkbox.tsx

@@ -3,7 +3,7 @@ import styled, { css } from "styled-components";
 import { ifProp } from "styled-tools";
 import { Flex, BoxProps } from "reflexbox/styled-components";
 
-import Text from "./Text";
+import Text, { Span } from "./Text";
 
 interface InputProps {
   checked: boolean;
@@ -82,9 +82,9 @@ const Checkbox: FC<Props> = ({
     >
       <Input name={name} id={id} checked={checked} />
       <Box checked={checked} width={width} height={height} />
-      <Text as="span" ml={12} color="#555">
+      <Span ml={12} color="#555">
         {label}
-      </Text>
+      </Span>
     </Flex>
   );
 };

+ 3 - 1
client/components/Divider.tsx

@@ -1,12 +1,14 @@
 import { Flex } from "reflexbox/styled-components";
 import styled from "styled-components";
 
+import { Colors } from "../consts";
+
 const Divider = styled(Flex).attrs({ as: "hr" })`
   width: 100%;
   height: 1px;
   outline: none;
   border: none;
-  background-color: #e3e3e3;
+  background-color: ${Colors.Divider};
 `;
 
 export default Divider;

+ 0 - 58
client/components/Error.tsx

@@ -1,58 +0,0 @@
-import React, { FC } from 'react';
-import { connect } from 'react-redux';
-import styled, { css } from 'styled-components';
-import { prop } from 'styled-tools';
-
-import { fadeIn } from '../helpers/animations';
-
-interface Message {
-  bottom?: number;
-  left?: number;
-}
-
-interface Props extends Message {
-  error: any;
-  type: string;
-}
-
-const ErrorMessage = styled.p<Message>`
-  content: '';
-  position: absolute;
-  right: 36px;
-  bottom: ${prop('bottom', -64)}px;
-  display: block;
-  font-size: 14px;
-  color: red;
-  animation: ${fadeIn} 0.3s ease-out;
-
-  @media only screen and (max-width: 768px) {
-    right: 8px;
-    bottom: -40px;
-    font-size: 12px;
-  }
-
-  ${({ left }) =>
-    left > -1 &&
-    css`
-      right: auto;
-      left: ${left}px;
-    `};
-`;
-
-const Error: FC<Props> = ({ bottom, error, left, type }) => {
-  const message = error[type] && (
-    <ErrorMessage left={left} bottom={bottom}>
-      {error[type]}
-    </ErrorMessage>
-  );
-  return <div>{message}</div>;
-};
-
-Error.defaultProps = {
-  bottom: -64,
-  left: -1,
-};
-
-const mapStateToProps = ({ error }) => ({ error });
-
-export default connect(mapStateToProps)(Error);

+ 21 - 22
client/components/Extensions.tsx

@@ -1,19 +1,10 @@
-import React from 'react';
-import styled from 'styled-components';
-import { Flex } from 'reflexbox/styled-components';
-import SVG from 'react-inlinesvg'; // TODO: another solution
-
-const Section = styled(Flex).attrs({
-  width: 1,
-  flex: '0 0 auto',
-  flexWrap: ['wrap', 'wrap', 'nowrap'],
-  flexDirection: 'column',
-  alignItems: 'center',
-  m: 0,
-  p: ['48px 0 16px', '48px 0 16px', '90px 0 100px'],
-})`
-  background-color: #282828;
-`;
+import React from "react";
+import styled from "styled-components";
+import { Flex } from "reflexbox/styled-components";
+import SVG from "react-inlinesvg"; // TODO: another solution
+import { Colors } from "../consts";
+import { ColCenterH } from "./Layout";
+import Text, { H3 } from "./Text";
 
 const Title = styled.h3`
   font-size: 28px;
@@ -38,7 +29,7 @@ const Button = styled.button`
   justify-content: center;
   margin: 0 16px;
   padding: 12px 28px;
-  font-family: 'Nunito', sans-serif;
+  font-family: "Nunito", sans-serif;
   background-color: #eee;
   border: 1px solid #aaa;
   font-size: 14px;
@@ -89,7 +80,7 @@ const Icon = styled(SVG)`
     width: 18px;
     height: 18px;
     margin-right: 16px;
-    fill: ${props => props.color || '#333'};
+    fill: ${props => props.color || "#333"};
 
     @media only screen and (max-width: 768px) {
       width: 13px;
@@ -100,14 +91,22 @@ const Icon = styled(SVG)`
 `;
 
 const Extensions = () => (
-  <Section>
-    <Title>Browser extensions.</Title>
+  <ColCenterH
+    width={1}
+    flex="0 0 auto"
+    flexWrap={["wrap", "wrap", "nowrap"]}
+    py={[64, 96]}
+    backgroundColor={Colors.ExtensionsBg}
+  >
+    <H3 fontSize={[26, 28]} mb={5} color="white" light>
+      Browser extensions.
+    </H3>
     <Flex
       width={1200}
       maxWidth="100%"
       flex="1 1 auto"
       justifyContent="center"
-      flexWrap={['wrap', 'wrap', 'nowrap']}
+      flexWrap={["wrap", "wrap", "nowrap"]}
     >
       <Link
         href="https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd"
@@ -130,7 +129,7 @@ const Extensions = () => (
         </FirefoxButton>
       </Link>
     </Flex>
-  </Section>
+  </ColCenterH>
 );
 
 export default Extensions;

+ 18 - 37
client/components/Features.tsx

@@ -1,47 +1,28 @@
-import React from 'react';
-import styled from 'styled-components';
-import { Flex } from 'reflexbox/styled-components';
+import React from "react";
+import styled from "styled-components";
+import { Flex } from "reflexbox/styled-components";
 
-import FeaturesItem from './FeaturesItem';
-
-const Section = styled(Flex).attrs({
-  width: 1,
-  flex: '0 0 auto',
-  flexDirection: 'column',
-  alignItems: 'center',
-  m: 0,
-  p: ['64px 0 16px', '64px 0 16px', '64px 0 16px', '102px 0 110px'],
-  flexWrap: ['wrap', 'wrap', 'wrap', 'nowrap'],
-})`
-  position: relative;
-  background-color: #eaeaea;
-`;
-
-const Title = styled.h3`
-  font-size: 28px;
-  font-weight: 300;
-  margin: 0 0 72px;
-
-  @media only screen and (max-width: 768px) {
-    font-size: 24px;
-    margin-bottom: 56px;
-  }
-
-  @media only screen and (max-width: 448px) {
-    font-size: 20px;
-    margin-bottom: 40px;
-  }
-`;
+import FeaturesItem from "./FeaturesItem";
+import { ColCenterH } from "./Layout";
+import { Colors } from "../consts";
+import Text, { H3 } from "./Text";
 
 const Features = () => (
-  <Section>
-    <Title>Kutting edge features.</Title>
+  <ColCenterH
+    width={1}
+    flex="0 0 auto"
+    py={[64, 100]}
+    backgroundColor={Colors.FeaturesBg}
+  >
+    <H3 fontSize={[26, 28]} mb={72} light>
+      Kutting edge features.
+    </H3>
     <Flex
       width={1200}
       maxWidth="100%"
       flex="1 1 auto"
       justifyContent="center"
-      flexWrap={['wrap', 'wrap', 'wrap', 'nowrap']}
+      flexWrap={["wrap", "wrap", "wrap", "nowrap"]}
     >
       <FeaturesItem title="Managing links" icon="edit">
         Create, protect and delete your links and monitor them with detailed
@@ -57,7 +38,7 @@ const Features = () => (
         Completely open source and free. You can host it on your own server.
       </FeaturesItem>
     </Flex>
-  </Section>
+  </ColCenterH>
 );
 
 export default Features;

+ 11 - 11
client/components/FeaturesItem.tsx

@@ -1,8 +1,8 @@
-import React, { FC } from 'react';
-import styled from 'styled-components';
-import { Flex } from 'reflexbox/styled-components';
+import React, { FC } from "react";
+import styled from "styled-components";
+import { Flex } from "reflexbox/styled-components";
 
-import { fadeIn } from '../helpers/animations';
+import { fadeIn } from "../helpers/animations";
 
 interface Props {
   title: string;
@@ -10,11 +10,11 @@ interface Props {
 }
 
 const Block = styled(Flex).attrs({
-  maxWidth: ['100%', '100%', '50%', '25%'],
-  flexDirection: 'column',
-  alignItems: 'center',
-  p: '0 24px',
-  mb: [48, 48, 48, 0],
+  maxWidth: ["100%", "100%", "50%", "25%"],
+  flexDirection: "column",
+  alignItems: "center",
+  p: "0 24px",
+  mb: [48, 48, 48, 0]
 })`
   animation: ${fadeIn} 0.8s ease-out;
 
@@ -26,8 +26,8 @@ const Block = styled(Flex).attrs({
 const IconBox = styled(Flex).attrs({
   width: [40, 40, 48],
   height: [40, 40, 48],
-  alignItems: 'center',
-  justifyContent: 'center',
+  alignItems: "center",
+  justifyContent: "center"
 })`
   border-radius: 100%;
   box-sizing: border-box;

+ 36 - 60
client/components/Footer.tsx

@@ -1,87 +1,63 @@
-import React, { FC, useEffect } from 'react';
-import { connect } from 'react-redux';
-import styled from 'styled-components';
-import { Flex } from 'reflexbox/styled-components';
+import React, { FC, useEffect } from "react";
 
-import ReCaptcha from './ReCaptcha';
-import showRecaptcha from '../helpers/recaptcha';
-import { ifProp } from 'styled-tools';
+import showRecaptcha from "../helpers/recaptcha";
+import { useStoreState } from "../store";
+import { ColCenter } from "./Layout";
+import ReCaptcha from "./ReCaptcha";
+import ALink from "./ALink";
+import Text from "./Text";
 
-interface Props {
-  isAuthenticated: boolean;
-}
+const Footer: FC = () => {
+  const { isAuthenticated } = useStoreState(s => s.auth);
 
-const Wrapper = styled(Flex).attrs({
-  as: 'footer',
-  width: 1,
-  flexDirection: 'column',
-  justifyContent: 'center',
-  alignItems: 'center',
-})<Props>`
-  padding: 4px 0 ${ifProp('isAuthenticated', '8px', '24px')};
-  background-color: white;
-
-  a {
-    text-decoration: none;
-    color: #2196f3;
-  }
-`;
-
-const Text = styled.p`
-  font-size: 13px;
-  font-weight: 300;
-  color: #666;
-
-  @media only screen and (max-width: 768px) {
-    font-size: 11px;
-  }
-`;
-
-const Footer: FC<Props> = ({ isAuthenticated }) => {
   useEffect(() => {
     showRecaptcha();
   }, []);
 
   return (
-    <Wrapper isAuthenticated={isAuthenticated}>
+    <ColCenter
+      as="footer"
+      width={1}
+      backgroundColor="white"
+      p={isAuthenticated ? 2 : 24}
+    >
       {!isAuthenticated && <ReCaptcha />}
-      <Text>
-        Made with love by{' '}
-        <a href="//thedevs.network/" title="The Devs">
+      <Text fontSize={[12, 13]} py={2}>
+        Made with love by{" "}
+        <ALink href="//thedevs.network/" title="The Devs">
           The Devs
-        </a>
-        .{' | '}
-        <a
+        </ALink>
+        .{" | "}
+        <ALink
           href="https://github.com/thedevs-network/kutt"
           title="GitHub"
           target="_blank"
         >
           GitHub
-        </a>
-        {' | '}
-        <a href="/terms" title="Terms of Service">
+        </ALink>
+        {" | "}
+        <ALink href="/terms" title="Terms of Service">
           Terms of Service
-        </a>
-        {' | '}
-        <a href="/report" title="Report abuse">
+        </ALink>
+        {" | "}
+        <ALink href="/report" title="Report abuse">
           Report Abuse
-        </a>
+        </ALink>
         {process.env.CONTACT_EMAIL && (
           <>
-            {' | '}
-            <a href={`mailto:${process.env.CONTACT_EMAIL}`} title="Contact us">
+            {" | "}
+            <ALink
+              href={`mailto:${process.env.CONTACT_EMAIL}`}
+              title="Contact us"
+            >
               Contact us
-            </a>
+            </ALink>
           </>
         )}
         .
       </Text>
-    </Wrapper>
+    </ColCenter>
   );
 };
 
-const mapStateToProps = ({ auth: { isAuthenticated } }) => ({
-  isAuthenticated,
-});
-
-export default connect(mapStateToProps)(Footer);
+export default Footer;

+ 2 - 2
client/components/Header.tsx

@@ -50,7 +50,7 @@ const Header: FC = () => {
   const login = !isAuthenticated && (
     <Li>
       <Link href="/login">
-        <ALink href="/login" title="login / signup">
+        <ALink href="/login" title="login / signup" forButton>
           <Button>Login / Sign up</Button>
         </ALink>
       </Link>
@@ -68,7 +68,7 @@ const Header: FC = () => {
   const settings = isAuthenticated && (
     <Li>
       <Link href="/settings">
-        <ALink href="/settings" title="settings">
+        <ALink href="/settings" title="Settings" forButton>
           <Button>Settings</Button>
         </ALink>
       </Link>

+ 3 - 1
client/components/Icon/Icon.tsx

@@ -5,6 +5,7 @@ import React, { FC } from "react";
 
 import ChevronRight from "./ChevronRight";
 import ChevronLeft from "./ChevronLeft";
+import { Colors } from "../../consts";
 import Clipboard from "./Clipboard";
 import PieChart from "./PieChart";
 import Refresh from "./Refresh";
@@ -125,7 +126,8 @@ const CustomIcon: FC<React.ComponentProps<typeof Flex>> = styled(Flex)`
       border-radius: 100%;
       background-color: none !important;
       cursor: pointer;
-      box-sizing: content-box;
+      box-sizing: border-box;
+      box-shadow: 0 2px 1px ${Colors.IconShadow};
 
       :hover,
       :focus {

+ 1 - 1
client/components/Icon/Lock.tsx

@@ -10,7 +10,7 @@ function Lock() {
       stroke="#000"
       strokeLinecap="round"
       strokeLinejoin="round"
-      strokeWidth="3"
+      strokeWidth="2"
       viewBox="0 0 24 24"
     >
       <rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect>

+ 26 - 28
client/components/LinksTable.tsx

@@ -1,6 +1,7 @@
 import formatDistanceToNow from "date-fns/formatDistanceToNow";
 import { CopyToClipboard } from "react-copy-to-clipboard";
 import React, { FC, useState, useEffect } from "react";
+import { useFormState } from "react-use-form-state";
 import { Flex } from "reflexbox/styled-components";
 import styled, { css } from "styled-components";
 import QRCode from "qrcode.react";
@@ -8,18 +9,18 @@ import Link from "next/link";
 
 import { useStoreActions, useStoreState } from "../store";
 import { removeProtocol, withComma } from "../utils";
-import { useFormState } from "react-use-form-state";
 import { NavButton, Button } from "./Button";
 import { Col, RowCenter } from "./Layout";
 import { ifProp } from "styled-tools";
 import TextInput from "./TextInput";
-import Table from "./CustomTable";
+import Animation from "./Animation";
 import Tooltip from "./Tooltip";
+import Table from "./Table";
 import ALink from "./ALink";
 import Modal from "./Modal";
-import Text from "./Text";
+import Text, { H2, Span } from "./Text";
 import Icon from "./Icon";
-import Animation from "./Animation";
+import { Colors } from "../consts";
 
 const Tr = styled(Flex).attrs({ as: "tr", px: [12, 12, 2] })``;
 const Th = styled(Flex)``;
@@ -45,8 +46,8 @@ const Td = styled(Flex)<{ withFade?: boolean }>`
       tr:hover &:after {
         background: linear-gradient(
           to left,
-          hsl(200, 14%, 98%),
-          hsl(200, 14%, 98%),
+          ${Colors.TableRowHover},
+          ${Colors.TableRowHover},
           transparent
         );
       }
@@ -180,9 +181,9 @@ const LinksTable: FC = () => {
 
   return (
     <Col width={1200} maxWidth="95%" margin="40px 0 120px" my={6}>
-      <Text as="h2" fontWeight={300} mb={3}>
+      <H2 mb={3} light>
         Recent shortened links.
-      </Text>
+      </H2>
       <Table scrollWidth="700px">
         <thead>
           <Tr justifyContent="space-between">
@@ -216,7 +217,7 @@ const LinksTable: FC = () => {
           {!links.items.length ? (
             <Tr width={1} justifyContent="center">
               <Td flex="1 1 auto" justifyContent="center">
-                <Text as="p" fontWeight={300} fontSize={18}>
+                <Text fontSize={18} light>
                   {links.loading ? "Loading links..." : "No links to show."}
                 </Text>
               </Td>
@@ -239,14 +240,14 @@ const LinksTable: FC = () => {
                         alignItems="center"
                       >
                         <Icon
-                          size={[15, 24]}
+                          size={[23, 24]}
                           py={0}
                           px={0}
                           mr={2}
                           p="3px"
                           name="check"
                           strokeWidth="3"
-                          stroke="hsl(144, 50%, 60%)"
+                          stroke={Colors.CheckIcon}
                         />
                       </Animation>
                     ) : (
@@ -258,8 +259,8 @@ const LinksTable: FC = () => {
                           <Action
                             name="copy"
                             strokeWidth="2.5"
-                            stroke="hsl(144, 40%, 57%)"
-                            backgroundColor="hsl(144, 100%, 96%)"
+                            stroke={Colors.CopyIcon}
+                            backgroundColor={Colors.CopyIconBg}
                           />
                         </CopyToClipboard>
                       </Animation>
@@ -294,25 +295,25 @@ const LinksTable: FC = () => {
                       >
                         <Action
                           name="pieChart"
-                          stroke="hsl(260, 100%, 69%)"
+                          stroke={Colors.PieIcon}
                           strokeWidth="2.5"
-                          backgroundColor="hsl(260, 100%, 96%)"
+                          backgroundColor={Colors.PieIconBg}
                         />
                       </Link>
                     )}
                     <Action
                       name="qrcode"
                       stroke="none"
-                      fill="hsl(0, 0%, 35%)"
-                      backgroundColor="hsl(0, 0%, 94%)"
+                      fill={Colors.QrCodeIcon}
+                      backgroundColor={Colors.QrCodeIconBg}
                       onClick={() => setQRModal(index)}
                     />
                     <Action
                       mr={0}
                       name="trash"
-                      stroke="hsl(0, 100%, 69%)"
                       strokeWidth="2"
-                      backgroundColor="hsl(0, 100%, 96%)"
+                      stroke={Colors.TrashIcon}
+                      backgroundColor={Colors.TrashIconBg}
                       onClick={() => setDeleteModal(index)}
                     />
                   </Td>
@@ -344,20 +345,17 @@ const LinksTable: FC = () => {
       >
         {linkToDelete && (
           <>
-            <Text as="h2" fontWeight={700} mb={24} textAlign="center">
+            <H2 mb={24} textAlign="center" bold>
               Delete link?
-            </Text>
-            <Text as="p" textAlign="center">
+            </H2>
+            <Text textAlign="center">
               Are you sure do you want to delete the link{" "}
-              <Text as="span" fontWeight={700}>
-                "{removeProtocol(linkToDelete.shortLink)}"
-              </Text>
-              ?
+              <Span bold>"{removeProtocol(linkToDelete.shortLink)}"</Span>?
             </Text>
             <Flex justifyContent="center" mt={44}>
               {deleteLoading ? (
                 <>
-                  <Icon name="spinner" size={20} stroke="#888" />
+                  <Icon name="spinner" size={20} stroke={Colors.Spinner} />
                 </>
               ) : (
                 <>
@@ -368,7 +366,7 @@ const LinksTable: FC = () => {
                   >
                     Cancel
                   </Button>
-                  <Button color="blue" ml={3} onClick={onDelete}>
+                  <Button color="red" ml={3} onClick={onDelete}>
                     <Icon name="trash" stroke="white" mr={2} />
                     Delete
                   </Button>

+ 4 - 4
client/components/NeedToLogin.tsx

@@ -5,12 +5,13 @@ import { Flex } from "reflexbox/styled-components";
 
 import { Button } from "./Button";
 import { fadeIn } from "../helpers/animations";
+import { Col } from "./Layout";
 
 const Wrapper = styled(Flex).attrs({
   width: 1200,
   maxWidth: "98%",
   alignItems: "center",
-  margin: "16px 0 0",
+  margin: "150px 0 0",
   flexDirection: ["column", "column", "row"]
 })`
   animation: ${fadeIn} 0.8s ease-out;
@@ -56,8 +57,7 @@ const Image = styled.img`
 
 const NeedToLogin = () => (
   <Wrapper>
-    <Flex
-      flexDirection="column"
+    <Col
       alignItems={["center", "center", "flex-start"]}
       mt={-32}
       mb={[32, 32, 0]}
@@ -70,7 +70,7 @@ const NeedToLogin = () => (
           <Button>Login / Signup</Button>
         </a>
       </Link>
-    </Flex>
+    </Col>
     <Image src="/images/callout.png" />
   </Wrapper>
 );

+ 4 - 3
client/components/PageLoading.tsx

@@ -1,9 +1,10 @@
 import { Flex } from "reflexbox/styled-components";
 import React from "react";
 
+import { Colors } from "../consts";
 import Icon from "./Icon";
 
-const pageLoading = () => (
+const PageLoading = () => (
   <Flex
     flex="1 1 250px"
     alignItems="center"
@@ -11,8 +12,8 @@ const pageLoading = () => (
     justifyContent="center"
     margin="0 0 48px"
   >
-    <Icon name="spinner" size={24} stroke="#888" />
+    <Icon name="spinner" size={24} stroke={Colors.Spinner} />
   </Flex>
 );
 
-export default pageLoading;
+export default PageLoading;

+ 9 - 9
client/components/Settings/SettingsApi.tsx

@@ -7,7 +7,8 @@ import { useStoreState, useStoreActions } from "../../store";
 import { Button } from "../Button";
 import ALink from "../ALink";
 import Icon from "../Icon";
-import Text from "../Text";
+import Text, { H2 } from "../Text";
+import { Col } from "../Layout";
 
 const ApiKey = styled(Text).attrs({
   mr: 3,
@@ -39,11 +40,11 @@ const SettingsApi: FC = () => {
   };
 
   return (
-    <Flex flexDirection="column" alignItems="flex-start">
-      <Text as="h2" fontWeight={700} mb={4}>
+    <Col alignItems="flex-start">
+      <H2 mb={4} bold>
         API
-      </Text>
-      <Text as="p" mb={4}>
+      </H2>
+      <Text mb={4}>
         In additional to this website, you can use the API to create, delete and
         get shortend URLs. If
         {" you're"} not familiar with API, {"don't"} generate the key. DO NOT
@@ -57,10 +58,9 @@ const SettingsApi: FC = () => {
         </ALink>
       </Text>
       {apikey && (
-        <Flex flexDirection="column" style={{ position: "relative" }} my={3}>
+        <Col style={{ position: "relative" }} my={3}>
           {copied && (
             <Text
-              as="p"
               color="green"
               fontSize={14}
               style={{ position: "absolute", top: -24 }}
@@ -82,13 +82,13 @@ const SettingsApi: FC = () => {
               </Button>
             </CopyToClipboard>
           </Flex>
-        </Flex>
+        </Col>
       )}
       <Button color="purple" onClick={onSubmit} disabled={loading}>
         <Icon name={loading ? "spinner" : "zap"} mr={2} stroke="white" />
         {loading ? "Generating..." : apikey ? "Regenerate" : "Generate"} key
       </Button>
-    </Flex>
+    </Col>
   );
 };
 

+ 8 - 12
client/components/Settings/SettingsBan.tsx

@@ -10,7 +10,8 @@ import Checkbox from "../Checkbox";
 import { API } from "../../consts";
 import { Button } from "../Button";
 import Icon from "../Icon";
-import Text from "../Text";
+import Text, { H2 } from "../Text";
+import { Col } from "../Layout";
 
 interface BanForm {
   id: string;
@@ -43,16 +44,11 @@ const SettingsBan: FC = () => {
   };
 
   return (
-    <Flex flexDirection="column">
-      <Text as="h2" fontWeight={700} mb={4}>
+    <Col>
+      <H2 mb={4} bold>
         Ban link
-      </Text>
-      <Flex
-        as="form"
-        flexDirection="column"
-        onSubmit={onSubmit}
-        alignItems="flex-start"
-      >
+      </H2>
+      <Col as="form" onSubmit={onSubmit} alignItems="flex-start">
         <Flex mb={24} alignItems="center">
           <TextInput
             {...text("id")}
@@ -85,8 +81,8 @@ const SettingsBan: FC = () => {
         <Text color={message.color} mt={3}>
           {message.text}
         </Text>
-      </Flex>
-    </Flex>
+      </Col>
+    </Col>
   );
 };
 

+ 25 - 32
client/components/Settings/SettingsDomain.tsx

@@ -6,12 +6,14 @@ import { useStoreState, useStoreActions } from "../../store";
 import { useFormState } from "react-use-form-state";
 import { Domain } from "../../store/settings";
 import { useMessage } from "../../hooks";
+import { Colors } from "../../consts";
 import TextInput from "../TextInput";
-import Table from "../CustomTable";
 import { Button } from "../Button";
+import Table from "../Table";
 import Modal from "../Modal";
 import Icon from "../Icon";
-import Text from "../Text";
+import Text, { H2, Span } from "../Text";
+import { Col } from "../Layout";
 
 const Th = styled(Flex).attrs({ as: "th", py: 3, px: 3 })`
   font-size: 15px;
@@ -64,11 +66,11 @@ const SettingsDomain: FC = () => {
   };
 
   return (
-    <Flex alignItems="flex-start" flexDirection="column">
-      <Text as="h2" fontWeight={700} mb={4}>
+    <Col alignItems="flex-start">
+      <H2 mb={4} bold>
         Custom domain
-      </Text>
-      <Text as="p" mb={3}>
+      </H2>
+      <Text mb={3}>
         You can set a custom domain for your short URLs, so instead of{" "}
         <b>kutt.it/shorturl</b> you can have <b>example.com/shorturl.</b>
       </Text>
@@ -94,9 +96,9 @@ const SettingsDomain: FC = () => {
                   <Icon
                     as="button"
                     name="trash"
-                    stroke="hsl(0, 100%, 69%)"
+                    stroke={Colors.TrashIcon}
                     strokeWidth="2.5"
-                    backgroundColor="hsl(0, 100%, 96%)"
+                    backgroundColor={Colors.TrashIconBg}
                     py={0}
                     px={0}
                     size={[23, 24]}
@@ -112,22 +114,16 @@ const SettingsDomain: FC = () => {
           </tbody>
         </Table>
       ) : (
-        <Flex
+        <Col
           alignItems="flex-start"
-          flexDirection="column"
           onSubmit={onSubmit}
           width={1}
           as="form"
           my={4}
         >
           <Flex width={1}>
-            <Flex flexDirection="column" mr={2} flex="1 1 auto">
-              <Text
-                {...label("customDomain")}
-                as="label"
-                fontWeight={700}
-                mb={3}
-              >
+            <Col mr={2} flex="1 1 auto">
+              <Text {...label("customDomain")} as="label" mb={3} bold>
                 Domain
               </Text>
               <TextInput
@@ -138,9 +134,9 @@ const SettingsDomain: FC = () => {
                 pr={24}
                 required
               />
-            </Flex>
-            <Flex flexDirection="column" ml={2} flex="1 1 auto">
-              <Text {...label("homepage")} as="label" fontWeight={700} mb={3}>
+            </Col>
+            <Col ml={2} flex="1 1 auto">
+              <Text {...label("homepage")} as="label" mb={3} bold>
                 Homepage (optional)
               </Text>
               <TextInput
@@ -151,37 +147,34 @@ const SettingsDomain: FC = () => {
                 pl={24}
                 pr={24}
               />
-            </Flex>
+            </Col>
           </Flex>
           <Button type="submit" color="purple" mt={3} disabled={loading}>
             <Icon name={loading ? "spinner" : "plus"} mr={2} stroke="white" />
             {loading ? "Setting..." : "Set domain"}
           </Button>
-        </Flex>
+        </Col>
       )}
       <Text color={message.color}>{message.text}</Text>
       <Modal id="delete-custom-domain" show={modal} closeHandler={closeModal}>
-        <Text as="h2" fontWeight={700} mb={24} textAlign="center">
+        <H2 mb={24} textAlign="center" bold>
           Delete domain?
-        </Text>
-        <Text as="p" textAlign="center">
+        </H2>
+        <Text textAlign="center">
           Are you sure do you want to delete the domain{" "}
-          <Text as="span" fontWeight={700}>
-            "{domainToDelete && domainToDelete.customDomain}"
-          </Text>
-          ?
+          <Span bold>"{domainToDelete && domainToDelete.customDomain}"</Span>?
         </Text>
         <Flex justifyContent="center" mt={44}>
           {deleteLoading ? (
             <>
-              <Icon name="spinner" size={20} stroke="#888" />
+              <Icon name="spinner" size={20} stroke={Colors.Spinner} />
             </>
           ) : (
             <>
               <Button color="gray" mr={3} onClick={closeModal}>
                 Cancel
               </Button>
-              <Button color="blue" ml={3} onClick={onDelete}>
+              <Button color="red" ml={3} onClick={onDelete}>
                 <Icon name="trash" stroke="white" mr={2} />
                 Delete
               </Button>
@@ -189,7 +182,7 @@ const SettingsDomain: FC = () => {
           )}
         </Flex>
       </Modal>
-    </Flex>
+    </Col>
   );
 };
 

+ 6 - 5
client/components/Settings/SettingsPassword.tsx

@@ -9,7 +9,8 @@ import TextInput from "../TextInput";
 import { API } from "../../consts";
 import { Button } from "../Button";
 import Icon from "../Icon";
-import Text from "../Text";
+import Text, { H2 } from "../Text";
+import { Col } from "../Layout";
 
 const SettingsPassword: FC = () => {
   const [loading, setLoading] = useState(false);
@@ -39,10 +40,10 @@ const SettingsPassword: FC = () => {
   };
 
   return (
-    <Flex flexDirection="column" alignItems="flex-start">
-      <Text as="h2" fontWeight={700} mb={4}>
+    <Col alignItems="flex-start">
+      <H2 mb={4} bold>
         Change password
-      </Text>
+      </H2>
       <Text mb={4}>Enter a new password to change your current password.</Text>
       <Flex as="form" onSubmit={onSubmit}>
         <TextInput
@@ -71,7 +72,7 @@ const SettingsPassword: FC = () => {
       <Text color={message.color} mt={3} fontSize={15}>
         {message.text}
       </Text>
-    </Flex>
+    </Col>
   );
 };
 

+ 59 - 55
client/components/Shortener.tsx

@@ -2,7 +2,6 @@ import { CopyToClipboard } from "react-copy-to-clipboard";
 import { Flex } from "reflexbox/styled-components";
 import React, { useState } from "react";
 import styled from "styled-components";
-import QRCode from "qrcode.react";
 
 import { useStoreActions, useStoreState } from "../store";
 import { Col, RowCenterH, RowCenter } from "./Layout";
@@ -12,11 +11,9 @@ import { Link } from "../store/links";
 import { useMessage } from "../hooks";
 import TextInput from "./TextInput";
 import Animation from "./Animation";
+import { Colors } from "../consts";
 import Checkbox from "./Checkbox";
-import { Button } from "./Button";
-import Tooltip from "./Tooltip";
-import Modal from "./Modal";
-import Text from "./Text";
+import Text, { H1, Span } from "./Text";
 import Icon from "./Icon";
 
 const SubmitIconWrapper = styled.div`
@@ -40,13 +37,13 @@ const SubmitIconWrapper = styled.div`
   }
 `;
 
-const ShortenedLink = styled(Text)`
-  border-bottom: 2px dotted #aaa;
+const ShortenedLink = styled(H1)`
+  cursor: "pointer";
+  border-bottom: 1px dotted ${Colors.StatsTotalUnderline};
   cursor: pointer;
-  transition: all 0.2s ease;
 
   :hover {
-    opacity: 0.5;
+    opacity: 0.8;
   }
 `;
 
@@ -65,7 +62,7 @@ const Shortener = () => {
   const [message, setMessage] = useMessage(3000);
   const [loading, setLoading] = useState(false);
   const [qrModal, setQRModal] = useState(false);
-  const [copied, setCopied] = useMessage(3000);
+  const [copied, setCopied] = useState(false);
   const [formState, { raw, password, text, label }] = useFormState<Form>(null, {
     withIds: true,
     onChange(e, stateValues, nextStateValues) {
@@ -79,7 +76,7 @@ const Shortener = () => {
   const onSubmit = async e => {
     e.preventDefault();
     if (loading) return;
-    setCopied("");
+    setCopied(false);
     setLoading(true);
     try {
       const link = await submit(formState.values);
@@ -94,20 +91,21 @@ const Shortener = () => {
   };
 
   const title = !link && (
-    <Text as="h1" fontWeight={300}>
+    <H1 light>
       Kutt your links{" "}
-      <Text
-        as="span"
-        fontWeight={300}
-        style={{ borderBottom: "2px dotted #999" }}
-      >
+      <Span style={{ borderBottom: "2px dotted #999" }} light>
         shorter
-      </Text>
+      </Span>
       .
-    </Text>
+    </H1>
   );
 
-  const onCopy = () => setCopied("Copied to clipboard.", "green");
+  const onCopy = () => {
+    setCopied(true);
+    setTimeout(() => {
+      setCopied(false);
+    }, 1500);
+  };
 
   const result = link && (
     <Animation
@@ -116,28 +114,42 @@ const Shortener = () => {
       duration="0.4s"
       style={{ position: "relative" }}
     >
+      {copied ? (
+        <Animation offset="10px" duration="0.2s" alignItems="center">
+          <Icon
+            size={[35]}
+            py={0}
+            px={0}
+            mr={3}
+            p="5px"
+            name="check"
+            strokeWidth="3"
+            stroke={Colors.CheckIcon}
+          />
+        </Animation>
+      ) : (
+        <Animation offset="-10px" duration="0.2s">
+          <CopyToClipboard text={link.shortLink} onCopy={onCopy}>
+            <Icon
+              as="button"
+              py={0}
+              px={0}
+              mr={3}
+              size={[35]}
+              p={["7px"]}
+              name="copy"
+              strokeWidth="2.5"
+              stroke={Colors.CopyIcon}
+              backgroundColor={Colors.CopyIconBg}
+            />
+          </CopyToClipboard>
+        </Animation>
+      )}
       <CopyToClipboard text={link.shortLink} onCopy={onCopy}>
-        <ShortenedLink as="h1" fontWeight={300} mr={3} mb={1} pb="2px">
+        <ShortenedLink fontSize={[30]} pb="2px" light>
           {removeProtocol(link.shortLink)}
         </ShortenedLink>
       </CopyToClipboard>
-      <CopyToClipboard text={link.shortLink} onCopy={onCopy}>
-        <Button>
-          <Icon name="clipboard" stroke="white" mr={2} />
-          Copy
-        </Button>
-      </CopyToClipboard>
-      {copied && (
-        <Animation
-          as={Text}
-          offset="10px"
-          color={copied.color}
-          fontSize={15}
-          style={{ position: "absolute", left: 0, top: -24 }}
-        >
-          {copied.text}
-        </Animation>
-      )}
     </Animation>
   );
 
@@ -165,13 +177,14 @@ const Shortener = () => {
           width={1}
           height={[72]}
           autoFocus
+          data-lpignore
         />
         <SubmitIconWrapper onClick={onSubmit}>
           <Icon
             name={loading ? "spinner" : "send"}
             size={28}
             fill={loading ? "none" : "#aaa"}
-            stroke={loading ? "#888" : "none"}
+            stroke={loading ? Colors.Spinner : "none"}
             mb={1}
             mr={1}
           />
@@ -198,17 +211,12 @@ const Shortener = () => {
         checked={formState.values.showAdvanced}
         label="Show advanced options"
         mt={24}
+        alignSelf="flex-start"
       />
       {formState.values.showAdvanced && (
         <Flex mt={4}>
           <Col>
-            <Text
-              as="label"
-              {...label("customurl")}
-              fontWeight={700}
-              fontSize={15}
-              mb={2}
-            >
+            <Text as="label" {...label("customurl")} fontSize={15} mb={2} bold>
               {(domain || {}).customDomain ||
                 (typeof window !== "undefined" && window.location.hostname)}
               /
@@ -216,32 +224,28 @@ const Shortener = () => {
             <TextInput
               {...text("customurl")}
               placeholder="Custom address"
+              data-lpignore
               pl={24}
               pr={24}
               placeholderSize={[13, 14, 14, 14]}
               fontSize={[14, 15]}
-              height={48}
+              height={44}
               width={240}
             />
           </Col>
           <Col ml={4}>
-            <Text
-              as="label"
-              {...label("password")}
-              fontWeight={700}
-              fontSize={15}
-              mb={2}
-            >
+            <Text as="label" {...label("password")} fontSize={15} mb={2} bold>
               Password:
             </Text>
             <TextInput
               {...password("password")}
               placeholder="Password"
+              data-lpignore
               pl={24}
               pr={24}
               placeholderSize={[13, 14, 14, 14]}
               fontSize={[14, 15]}
-              height={48}
+              height={44}
               width={240}
             />
           </Col>

+ 0 - 146
client/components/Stats/Stats.tsx

@@ -1,146 +0,0 @@
-import React, { Component, FC, useState, useEffect } from "react";
-import { bindActionCreators } from "redux";
-import { connect } from "react-redux";
-import Router from "next/router";
-import styled from "styled-components";
-import axios from "axios";
-import cookie from "js-cookie";
-import { Box, Flex } from "reflexbox/styled-components";
-
-import StatsError from "./StatsError";
-import StatsHead from "./StatsHead";
-import StatsCharts from "./StatsCharts";
-import PageLoading from "../PageLoading";
-import { Button } from "../Button";
-
-interface Props {
-  isAuthenticated: boolean;
-  domain: string;
-  id: string;
-}
-
-const Title = styled.h2`
-  font-size: 24px;
-  font-weight: 300;
-
-  a {
-    color: #2196f3;
-    text-decoration: none;
-    border-bottom: 1px dotted transparent;
-
-    :hover {
-      border-bottom-color: #2196f3;
-    }
-  }
-
-  @media only screen and (max-width: 768px) {
-    font-size: 18px;
-  }
-`;
-
-const TitleTarget = styled.p`
-  font-size: 14px;
-  text-align: right;
-  color: #333;
-
-  @media only screen and (max-width: 768px) {
-    font-size: 11px;
-  }
-`;
-
-const Content = styled(Flex).attrs({
-  flex: "1 1 auto",
-  flexDirection: "column"
-})`
-  background-color: white;
-  border-radius: 12px;
-  box-shadow: 0 6px 30px rgba(50, 50, 50, 0.2);
-`;
-
-const Stats: FC<Props> = ({ domain, id, isAuthenticated }) => {
-  const [loading, setLoading] = useState(true);
-  const [error, setError] = useState(false);
-  const [stats, setStats] = useState();
-  const [period, setPeriod] = useState();
-
-  useEffect(() => {
-    if (id) return null;
-    axios
-      .get(`/api/url/stats?id=${id}&domain=${domain}`, {
-        headers: { Authorization: cookie.get("token") }
-      })
-      .then(({ data }) => {
-        setLoading(false);
-        setError(!data);
-        setStats(data);
-      })
-      .catch(() => {
-        setLoading(false);
-        setError(true);
-      });
-  }, []);
-
-  const changePeriod = e => setPeriod(e.currentTarget.dataset.period);
-
-  function goToHomepage(e) {
-    e.preventDefault();
-    Router.push("/");
-  }
-
-  if (!isAuthenticated)
-    return <StatsError text="You need to login to view stats." />;
-
-  if (!id || error) return <StatsError />;
-
-  if (loading) return <PageLoading />;
-
-  return (
-    <Flex
-      width={1200}
-      maxWidth="95%"
-      flexDirection="column"
-      alignItems="stretch"
-      m="40px 0"
-    >
-      <Flex justifyContent="space-between" alignItems="center">
-        <Title>
-          Stats for:{" "}
-          <a href={stats.shortLink} title="Short link">
-            {stats.shortLink.replace(/https?:\/\//, "")}
-          </a>
-        </Title>
-        <TitleTarget>
-          {stats.target.length > 80
-            ? `${stats.target
-                .split("")
-                .slice(0, 80)
-                .join("")}...`
-            : stats.target}
-        </TitleTarget>
-      </Flex>
-      <Content>
-        <StatsHead
-          total={stats.total}
-          period={period}
-          changePeriod={changePeriod}
-        />
-        <StatsCharts
-          stats={stats[period]}
-          updatedAt={stats.updatedAt}
-          period={period}
-        />
-      </Content>
-      <Box alignSelf="center" my={64}>
-        <Button icon="arrow-left" onClick={goToHomepage}>
-          Back to homepage
-        </Button>
-      </Box>
-    </Flex>
-  );
-};
-
-const mapStateToProps = ({ auth: { isAuthenticated } }) => ({
-  isAuthenticated
-});
-
-export default connect(mapStateToProps, null)(Stats);

+ 0 - 103
client/components/Stats/StatsCharts/StatsCharts.tsx

@@ -1,103 +0,0 @@
-import React, { FC } from 'react';
-import styled from 'styled-components';
-
-import Area from './Area';
-import Pie from './Pie';
-import Bar from './Bar';
-
-interface Props {
-  updatedAt: string;
-  period: string;
-  stats: {
-    stats: any; // TODO: types
-    views: number[];
-  };
-}
-
-const ChartsWrapper = styled.div`
-  display: flex;
-  flex-direction: column;
-  align-items: stretch;
-  padding: 32px;
-
-  @media only screen and (max-width: 768px) {
-    padding: 16px 16px 32px 16px;
-  }
-`;
-
-const Row = styled.div`
-  display: flex;
-  border-bottom: 1px dotted #aaa;
-  padding: 0 0 40px 0;
-  margin: 0 0 32px 0;
-
-  :last-child {
-    border: none;
-    margin: 0;
-  }
-
-  @media only screen and (max-width: 768px) {
-    flex-direction: column;
-    padding-bottom: 0;
-    margin-bottom: 0;
-    border-bottom: none;
-
-    > *:not(:last-child) {
-      padding-bottom: 24px;
-      margin-bottom: 16px;
-      border-bottom: 1px dotted #aaa;
-    }
-  }
-`;
-
-const StatsCharts: FC<Props> = ({ stats, period, updatedAt }) => {
-  const periodText = period.includes('last')
-    ? `the last ${period.replace('last', '').toLocaleLowerCase()}`
-    : 'all time';
-  const hasView = stats.views.some(view => view > 0);
-  return (
-    <ChartsWrapper>
-      <Row>
-        <Area
-          data={stats.views}
-          period={period}
-          updatedAt={updatedAt}
-          periodText={periodText}
-        />
-      </Row>
-      {hasView
-        ? [
-            <Row key="second-row">
-              <Pie
-                data={stats.stats.referrer}
-                updatedAt={updatedAt}
-                title="Referrals"
-              />
-              <Bar
-                data={stats.stats.browser}
-                updatedAt={updatedAt}
-                title="Browsers"
-              />
-            </Row>,
-            <Row key="third-row">
-              <Pie
-                data={stats.stats.country}
-                updatedAt={updatedAt}
-                title="Country"
-              />
-              <Bar
-                data={stats.stats.os.map(o => ({
-                  ...o,
-                  name: o.name === 'Mac Os X' ? 'macOS' : o.name,
-                }))}
-                updatedAt={updatedAt}
-                title="OS"
-              />
-            </Row>,
-          ]
-        : null}
-    </ChartsWrapper>
-  );
-};
-
-export default StatsCharts;

+ 0 - 1
client/components/Stats/StatsCharts/index.tsx

@@ -1 +0,0 @@
-export { default } from './StatsCharts';

+ 0 - 75
client/components/Stats/StatsCharts/withTitle.tsx

@@ -1,75 +0,0 @@
-import React, { FC } from 'react';
-import styled from 'styled-components';
-import formatDate from 'date-fns/format';
-import { Flex } from 'reflexbox/styled-components';
-
-interface Props {
-  data: number | any; // TODO: types
-  period?: string;
-  periodText?: string;
-  title?: string;
-  updatedAt: string;
-}
-
-const Title = styled.h3`
-  margin-bottom: 12px;
-  font-size: 24px;
-  font-weight: 300;
-
-  @media only screen and (max-width: 768px) {
-    font-size: 18px;
-  }
-`;
-
-const SubTitle = styled.span`
-  margin-bottom: 32px;
-  font-size: 13px;
-  font-weight: 300;
-  color: #aaa;
-
-  @media only screen and (max-width: 768px) {
-    font-size: 11px;
-  }
-`;
-
-const Count = styled.span`
-  font-weight: bold;
-  border-bottom: 1px dotted #999;
-`;
-
-const withTitle = (ChartComponent: FC<any>) => {
-  function WithTitle(props: Props) {
-    return (
-      <Flex
-        flexGrow={1}
-        flexShrink={1}
-        flexBasis={['100%', '100%', '50%']}
-        flexDirection="column"
-      >
-        <Title>
-          {props.periodText && (
-            <Count>{props.data.reduce((sum, view) => sum + view, 0)}</Count>
-          )}
-          {props.periodText
-            ? ` tracked clicks in ${props.periodText}`
-            : props.title}
-          .
-        </Title>
-        {props.periodText && props.updatedAt && (
-          <SubTitle>
-            Last update in{' '}
-            {formatDate(new Date(props.updatedAt), 'dddd, hh:mm aa')}.
-          </SubTitle>
-        )}
-        <ChartComponent {...props} />
-      </Flex>
-    );
-  }
-  WithTitle.defaultProps = {
-    title: '',
-    periodText: '',
-  };
-  return WithTitle;
-};
-
-export default withTitle;

+ 0 - 40
client/components/Stats/StatsError.tsx

@@ -1,40 +0,0 @@
-import React, { FC } from 'react';
-import styled from 'styled-components';
-import { Flex } from 'reflexbox/styled-components';
-
-interface Props {
-  text?: string;
-}
-
-const ErrorMessage = styled.h3`
-  font-size: 24px;
-  font-weight: 300;
-
-  @media only screen and (max-width: 768px) {
-    font-size: 18px;
-  }
-`;
-
-const Icon = styled.img`
-  width: 24px;
-  height: 24px;
-  margin: 6px 12px 0 0;
-
-  @media only screen and (max-width: 768px) {
-    width: 18px;
-    height: 18px;
-  }
-`;
-
-const StatsError: FC<Props> = ({ text }) => (
-  <Flex justifyContent="center" alignItems="center">
-    <Icon src="/images/x.svg" />
-    <ErrorMessage>{text || 'Could not get the short URL stats.'}</ErrorMessage>
-  </Flex>
-);
-
-StatsError.defaultProps = {
-  text: '',
-};
-
-export default StatsError;

+ 0 - 101
client/components/Stats/StatsHead.tsx

@@ -1,101 +0,0 @@
-import React, { FC } from "react";
-import styled, { css } from "styled-components";
-import { ifProp } from "styled-tools";
-import { Flex } from "reflexbox/styled-components";
-
-interface Props {
-  changePeriod: any; // TODO: types
-  period: string;
-  total: number;
-}
-
-const Wrapper = styled(Flex).attrs({
-  flex: "1 1 auto",
-  justifyContent: "center",
-  alignItems: "center",
-  py: [16, 16, 25],
-  px: 32
-})`
-  background-color: #f1f1f1;
-  border-top-left-radius: 12px;
-  border-top-right-radius: 12px;
-`;
-
-const TotalText = styled.p`
-  margin: 0;
-  padding: 0;
-
-  span {
-    font-weight: bold;
-    border-bottom: 1px dotted #999;
-  }
-
-  @media only screen and (max-width: 768px) {
-    font-size: 13px;
-  }
-`;
-
-const Button = styled.button<{ active: boolean }>`
-  display: flex;
-  padding: 6px 12px;
-  margin: 0 4px;
-  border: none;
-  font-size: 12px;
-  border-radius: 4px;
-  box-shadow: 0 0px 10px rgba(100, 100, 100, 0.1);
-  background-color: white;
-  cursor: pointer;
-  transition: all 0.2s ease-out;
-  box-sizing: border-box;
-
-  :last-child {
-    margin-right: 0;
-  }
-
-  ${ifProp(
-    { active: false },
-    css`
-      border: 1px solid #ddd;
-      background-color: #f5f5f5;
-      box-shadow: 0 2px 6px rgba(150, 150, 150, 0.2);
-
-      :hover {
-        border-color: 1px solid #ccc;
-        background-color: white;
-      }
-    `
-  )}
-
-  @media only screen and (max-width: 768px) {
-    padding: 4px 8px;
-    margin: 0 2px;
-    font-size: 11px;
-  }
-`;
-
-const StatsHead: FC<Props> = ({ changePeriod, period, total }) => {
-  const buttonWithPeriod = (periodText, text) => (
-    <Button
-      active={period === periodText}
-      data-period={periodText}
-      onClick={changePeriod}
-    >
-      {text}
-    </Button>
-  );
-  return (
-    <Wrapper>
-      <TotalText>
-        Total clicks: <span>{total}</span>
-      </TotalText>
-      <Flex>
-        {buttonWithPeriod("allTime", "All Time")}
-        {buttonWithPeriod("lastMonth", "Month")}
-        {buttonWithPeriod("lastWeek", "Week")}
-        {buttonWithPeriod("lastDay", "Day")}
-      </Flex>
-    </Wrapper>
-  );
-};
-
-export default StatsHead;

+ 0 - 1
client/components/Stats/index.tsx

@@ -1 +0,0 @@
-export { default } from './Stats';

+ 8 - 6
client/components/CustomTable.ts → client/components/Table.ts

@@ -2,10 +2,12 @@ import { Flex } from "reflexbox/styled-components";
 import styled, { css } from "styled-components";
 import { ifProp, prop } from "styled-tools";
 
+import { Colors } from "../consts";
+
 const Table = styled(Flex)<{ scrollWidth?: string }>`
   background-color: white;
   border-radius: 12px;
-  box-shadow: 0 6px 15px hsla(200, 20%, 70%, 0.3);
+  box-shadow: 0 6px 15px ${Colors.TableShadow};
   text-align: center;
   overflow: scroll;
 
@@ -26,7 +28,7 @@ const Table = styled(Flex)<{ scrollWidth?: string }>`
   }
 
   tr {
-    border-bottom: 1px solid hsl(200, 14%, 94%);
+    border-bottom: 1px solid ${Colors.TableHeadBorder};
   }
   tbody {
     border-bottom-right-radius: 12px;
@@ -37,19 +39,19 @@ const Table = styled(Flex)<{ scrollWidth?: string }>`
     border: none;
   }
   tbody tr:hover {
-    background-color: hsl(200, 14%, 98%);
+    background-color: ${Colors.TableRowHover};
   }
   thead {
-    background-color: hsl(200, 14%, 96%);
+    background-color: ${Colors.TableHeadBg};
     border-top-right-radius: 12px;
     border-top-left-radius: 12px;
     font-weight: bold;
     tr {
-      border-bottom: 1px solid hsl(200, 14%, 90%);
+      border-bottom: 1px solid ${Colors.TableBorder};
     }
   }
   tfoot {
-    background-color: hsl(200, 14%, 96%);
+    background-color: ${Colors.TableHeadBg};
     border-bottom-right-radius: 12px;
     border-bottom-left-radius: 12px;
   }

+ 0 - 149
client/components/Table/TBody/TBody.tsx

@@ -1,149 +0,0 @@
-import React, { FC } from 'react';
-import { connect } from 'react-redux';
-import styled, { css } from 'styled-components';
-import distanceInWordsToNow from 'date-fns/formatDistanceToNow';
-import { ifProp } from 'styled-tools';
-import { Flex } from 'reflexbox/styled-components';
-
-import TBodyShortUrl from './TBodyShortUrl';
-import TBodyCount from './TBodyCount';
-
-interface Props {
-  urls: Array<{
-    id: string;
-    count: number;
-    created_at: string;
-    password: boolean;
-    target: string;
-  }>;
-  copiedIndex: number;
-  showModal: any; // TODO: types
-  tableLoading: boolean;
-  handleCopy: any;
-}
-
-const TBody = styled(Flex).attrs({
-  as: 'tbody',
-  flex: '1 1 auto',
-  flexDirection: 'column',
-})<{ loading: boolean }>`
-  ${ifProp(
-    'loading',
-    css`
-      opacity: 0.2;
-    `
-  )}
-
-  tr:hover {
-    background-color: #f8f8f8;
-
-    td:after {
-      background: linear-gradient(to left, #f8f8f8, #f8f8f8, transparent);
-    }
-  }
-`;
-
-const Td = styled(Flex).attrs({
-  as: 'td',
-  flexBasis: 0,
-})<{ flex?: string; withFade?: boolean; date?: boolean }>`
-  white-space: nowrap;
-  overflow: hidden;
-
-  ${ifProp(
-    'withFade',
-    css`
-    :after {
-      content: '';
-      position: absolute;
-      right: 0;
-      top: 0;
-      height: 100%;
-      width: 56px;
-      background: linear-gradient(to left, white, white, transparent);
-  `
-  )}
-
-  :last-child {
-    justify-content: space-between;
-  }
-
-  a {
-    color: #2196f3;
-    text-decoration: none;
-    box-sizing: border-box;
-    border-bottom: 1px dotted transparent;
-    transition: all 0.2s ease-out;
-
-    :hover {
-      border-bottom-color: #2196f3;
-    }
-  }
-
-  ${ifProp(
-    'date',
-    css`
-      font-size: 15px;
-    `
-  )}
-
-  @media only screen and (max-width: 768px) {
-    flex: 1;
-    :nth-child(2) {
-      display: none;
-    }
-  }
-
-  @media only screen and (max-width: 510px) {
-    :nth-child(1) {
-      display: none;
-    }
-  }
-`;
-
-const TableBody: FC<Props> = ({
-  copiedIndex,
-  handleCopy,
-  tableLoading,
-  showModal,
-  urls,
-}) => {
-  const showList = (url, index) => (
-    <tr key={`tbody-${index}`}>
-      <Td flex="2 2 0" withFade>
-        <a href={url.target}>{url.target}</a>
-      </Td>
-      <Td flex="1 1 0" date>
-        {`${distanceInWordsToNow(new Date(url.created_at))} ago`}
-      </Td>
-      <Td flex="1 1 0" withFade>
-        <TBodyShortUrl
-          index={index}
-          copiedIndex={copiedIndex}
-          handleCopy={handleCopy}
-          url={url}
-        />
-      </Td>
-      <Td flex="1 1 0">
-        <TBodyCount url={url} showModal={showModal(url)} />
-      </Td>
-    </tr>
-  );
-  return (
-    <TBody loading={tableLoading}>
-      {urls.length ? (
-        urls.map(showList)
-      ) : (
-        <tr>
-          <Td>Nothing to show.</Td>
-        </tr>
-      )}
-    </TBody>
-  );
-};
-
-const mapStateToProps = ({ loading: { table: tableLoading } }) => ({
-  tableLoading,
-});
-
-export default connect(mapStateToProps)(TableBody);

+ 0 - 65
client/components/Table/TBody/TBodyButton.tsx

@@ -1,65 +0,0 @@
-import React, { FC } from 'react';
-import styled, { css } from 'styled-components';
-
-interface Props {
-  withText?: boolean;
-}
-
-const Button = styled.button<Props>`
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  width: 26px;
-  height: 26px;
-  margin: 0 12px 0 2px;
-  padding: 0;
-  border: none;
-  outline: none;
-  border-radius: 100%;
-  box-shadow: 0 2px 4px rgba(100, 100, 100, 0.1);
-  background-color: #dedede;
-  cursor: pointer;
-  transition: all 0.2s ease-out;
-
-  @media only screen and (max-width: 768px) {
-    height: 22px;
-    width: 22px;
-    margin: 0 8px 0 2px;
-
-    img {
-      width: 10px;
-      height: 10px;
-    }
-  }
-
-  ${({ withText }) =>
-    withText &&
-    css`
-      width: auto;
-      padding: 0 12px;
-      border-radius: 100px;
-
-      img {
-        margin: 4px 6px 0 0;
-      }
-
-      @media only screen and (max-width: 768px) {
-        width: auto;
-      }
-    `};
-
-  :active,
-  :focus {
-    outline: none;
-  }
-
-  :hover {
-    transform: translateY(-2px);
-  }
-`;
-
-Button.defaultProps = {
-  withText: null,
-};
-
-export default Button;

+ 0 - 80
client/components/Table/TBody/TBodyCount.tsx

@@ -1,80 +0,0 @@
-import React, { FC, useState } from "react";
-import Router from "next/router";
-import styled from "styled-components";
-import URL from "url";
-import QRCode from "qrcode.react";
-import { Flex } from "reflexbox/styled-components";
-
-import TBodyButton from "./TBodyButton";
-import Modal from "../../Modal";
-
-interface Props {
-  showModal: any;
-  url: {
-    count: number;
-    domain: string;
-    id: string;
-    password: boolean;
-    shortLink: string;
-    visit_count: number;
-  };
-}
-
-const Actions = styled(Flex).attrs({
-  justifyContent: "center",
-  alignItems: "center"
-})`
-  button {
-    margin: 0 2px 0 12px;
-  }
-`;
-
-const Icon = styled.img`
-  width: 12px;
-  height: 12px;
-`;
-
-const TBodyCount: FC<Props> = ({ url }) => {
-  const [showModal, setShowModal] = useState(false);
-  const toggleQrCodeModal = () => setShowModal(current => !current);
-
-  function goTo(e) {
-    e.preventDefault();
-    const { id, domain } = url;
-    Router.push(`/stats?id=${id}${domain ? `&domain=${domain}` : ""}`);
-  }
-
-  const showQrCode = window.innerWidth > 640;
-
-  return (
-    <Flex flex="1 1 auto" justifyContent="space-between" alignItems="center">
-      {url.visit_count || 0}
-      <Actions>
-        {url.password && <Icon src="/images/lock.svg" />}
-        {url.visit_count > 0 && (
-          <TBodyButton withText onClick={goTo}>
-            <Icon src="/images/chart.svg" />
-            Stats
-          </TBodyButton>
-        )}
-        {showQrCode && (
-          <TBodyButton onClick={toggleQrCodeModal}>
-            <Icon src="/images/qrcode.svg" />
-          </TBodyButton>
-        )}
-        <TBodyButton
-          data-id={url.id}
-          data-host={URL.parse(url.shortLink).hostname}
-          onClick={toggleQrCodeModal} // FIXME: what does this do?
-        >
-          <Icon src="/images/trash.svg" />
-        </TBodyButton>
-      </Actions>
-      <Modal show={showModal}>
-        <QRCode value={url.shortLink} size={196} />
-      </Modal>
-    </Flex>
-  );
-};
-
-export default TBodyCount;

+ 0 - 46
client/components/Table/TBody/TBodyShortUrl.tsx

@@ -1,46 +0,0 @@
-import React, { FC } from 'react';
-import styled from 'styled-components';
-import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { Flex } from 'reflexbox/styled-components';
-
-import TBodyButton from './TBodyButton';
-
-interface Props {
-  copiedIndex: number;
-  handleCopy: any; // TODO: types
-  index: number;
-  url: {
-    id: string;
-    shortLink: string;
-  };
-}
-
-const CopyText = styled.div`
-  position: absolute;
-  top: 0;
-  left: 40px;
-  font-size: 11px;
-  color: green;
-`;
-
-const Icon = styled.img`
-  width: 12px;
-  height: 12px;
-`;
-
-const TBodyShortUrl: FC<Props> = ({ index, copiedIndex, handleCopy, url }) => (
-  <Flex alignItems="center">
-    {copiedIndex === index && <CopyText>Copied to clipboard!</CopyText>}
-    <CopyToClipboard onCopy={() => handleCopy(index)} text={`${url.shortLink}`}>
-      <TBodyButton>
-        <Icon src="/images/copy.svg" />
-      </TBodyButton>
-    </CopyToClipboard>
-    <a href={`${url.shortLink}`}>{`${url.shortLink.replace(
-      /^https?:\/\//,
-      ''
-    )}`}</a>
-  </Flex>
-);
-
-export default TBodyShortUrl;

+ 0 - 1
client/components/Table/TBody/index.tsx

@@ -1 +0,0 @@
-export { default } from './TBody';

+ 0 - 53
client/components/Table/THead/THead.tsx

@@ -1,53 +0,0 @@
-import React, { FC } from "react";
-import styled, { css } from "styled-components";
-import { Flex } from "reflexbox";
-
-import TableOptions from "../../TableOptions";
-
-const THead = styled(Flex).attrs({
-  as: "thead",
-  flexDirection: "column",
-  flex: "1 1 auto"
-})`
-  background-color: #f1f1f1;
-  border-top-right-radius: 12px;
-  border-top-left-radius: 12px;
-
-  tr {
-    border-bottom: 1px solid #dedede;
-  }
-`;
-
-const Th = styled(Flex).attrs({
-  as: "th",
-  justifyContent: "start",
-  alignItems: "center",
-  flexBasis: 0
-})`
-  @media only screen and (max-width: 768px) {
-    flex: 1;
-    :nth-child(2) {
-      display: none;
-    }
-  }
-
-  @media only screen and (max-width: 510px) {
-    :nth-child(1) {
-      display: none;
-    }
-  }
-`;
-
-const TableHead: FC = () => (
-  <THead>
-    <TableOptions />
-    <tr>
-      <Th>Original URL</Th>
-      <Th>Created</Th>
-      <Th>Short URL</Th>
-      <Th>Clicks</Th>
-    </tr>
-  </THead>
-);
-
-export default TableHead;

+ 0 - 1
client/components/Table/THead/index.tsx

@@ -1 +0,0 @@
-export { default } from './THead';

+ 0 - 65
client/components/TableNav.tsx

@@ -1,65 +0,0 @@
-import React, { FC } from 'react';
-import styled, { css } from 'styled-components';
-import { ifProp } from 'styled-tools';
-import { Flex } from 'reflexbox/styled-components';
-
-interface Props {
-  handleNav: any; // TODO: types
-  next: boolean;
-  prev: boolean;
-}
-
-const Nav = styled.button<{ disabled: boolean }>`
-  margin-left: 12px;
-  padding: 5px 8px 3px;
-  border-radius: 4px;
-  border: 1px solid #eee;
-  background-color: transparent;
-  box-shadow: 0 0px 10px rgba(100, 100, 100, 0.1);
-  transition: all 0.2s ease-out;
-
-  ${ifProp(
-    'disabled',
-    css`
-      background-color: white;
-      cursor: pointer;
-    `
-  )}
-
-  ${ifProp(
-    { disabled: false },
-    css`
-      :hover {
-        transform: translateY(-2px);
-        box-shadow: 0 5px 25px rgba(50, 50, 50, 0.1);
-      }
-    `
-  )}
-
-  @media only screen and (max-width: 768px) {
-    padding: 4px 6px 2px;
-  }
-`;
-
-const Icon = styled.img`
-  width: 14px;
-  height: 14px;
-
-  @media only screen and (max-width: 768px) {
-    width: 12px;
-    height: 12px;
-  }
-`;
-
-const TableNav: FC<Props> = ({ handleNav, next, prev }) => (
-  <Flex alignItems="center">
-    <Nav disabled={!prev} onClick={handleNav(-1)}>
-      <Icon src="/images/nav-left.svg" />
-    </Nav>
-    <Nav disabled={!next} onClick={handleNav(1)}>
-      <Icon src="/images/nav-right.svg" />
-    </Nav>
-  </Flex>
-);
-
-export default TableNav;

+ 0 - 168
client/components/TableOptions.tsx

@@ -1,168 +0,0 @@
-import React, { FC, useState } from "react";
-import { bindActionCreators } from "redux";
-import { connect } from "react-redux";
-import styled, { css } from "styled-components";
-import { ifProp } from "styled-tools";
-import { Flex } from "reflexbox/styled-components";
-
-import TableNav from "./TableNav";
-import TextInput from "./TextInput";
-import { getUrlsList } from "../actions";
-
-interface Props {
-  getUrlsList: any; // TODO: types
-  nosearch?: boolean;
-  url: {
-    page: number;
-    count: number;
-    countAll: number;
-  };
-}
-
-const Tr = styled(Flex).attrs({ as: "tr", alignItems: "center" })`
-  thead & {
-    border-bottom: 1px solid #ddd !important;
-  }
-`;
-
-const Th = styled(Flex).attrs({ as: "th", alignItems: "center" })``;
-
-const Divider = styled.div`
-  margin: 0 16px 0 24px;
-  width: 1px;
-  height: 20px;
-  background-color: #ccc;
-
-  @media only screen and (max-width: 768px) {
-    margin: 0 4px 0 12px;
-  }
-
-  @media only screen and (max-width: 510px) {
-    display: none;
-  }
-`;
-
-const Ul = styled(Flex).attrs({ as: "ul" })`
-  margin: 0;
-  padding: 0;
-  list-style: none;
-
-  @media only screen and (max-width: 510px) {
-    display: none;
-  }
-`;
-
-const Li = styled(Flex).attrs({ as: "li" })`
-  li {
-    margin: 0 0 0 12px;
-    list-style: none;
-
-    @media only screen and (max-width: 768px) {
-      margin-left: 8px;
-    }
-  }
-`;
-
-const Button = styled.button<{ active: boolean }>`
-  display: flex;
-  padding: 4px 8px;
-  border: none;
-  font-size: 12px;
-  border-radius: 4px;
-  box-shadow: 0 0px 10px rgba(100, 100, 100, 0.1);
-  background-color: white;
-  cursor: pointer;
-  transition: all 0.2s ease-out;
-  box-sizing: border-box;
-
-  ${ifProp(
-    { active: false },
-    css`
-      border: 1px solid #ddd;
-      background-color: #f5f5f5;
-      box-shadow: 0 0px 5px rgba(150, 150, 150, 0.1);
-
-      :hover {
-        border-color: 1px solid #ccc;
-        background-color: white;
-      }
-    `
-  )}
-  @media only screen and (max-width: 768px) {
-    font-size: 10px;
-  }
-`;
-
-const TableOptions: FC<Props> = ({ getUrlsList, nosearch, url }) => {
-  const [search, setSearch] = useState();
-  const [count, setCount] = useState();
-
-  function submitSearch(e) {
-    e.preventDefault();
-    getUrlsList({ search });
-  }
-
-  const handleCount = e => setCount(Number(e.target.textContent));
-
-  function handleNav(num) {
-    return e => {
-      const { active } = e.target.dataset;
-      if (active === "false") return null;
-      return getUrlsList({ page: url.page + num });
-    };
-  }
-
-  return (
-    <Tr>
-      <Th>
-        {!nosearch && (
-          <form onSubmit={submitSearch}>
-            <TextInput
-              as="input"
-              id="search"
-              name="search"
-              value={search}
-              placeholder="Search..."
-              onChange={e => setSearch(e.target.value)}
-              tiny
-            />
-          </form>
-        )}
-      </Th>
-      <Th>
-        <Flex alignItems="center">
-          <Ul>
-            {[10, 25, 50].map(c => (
-              <Li key={c}>
-                <Button active={count === c} onClick={handleCount}>
-                  {c}
-                </Button>
-              </Li>
-            ))}
-          </Ul>
-        </Flex>
-        <Divider />
-        <TableNav
-          handleNav={handleNav}
-          next={url.page * count < url.countAll}
-          prev={url.page > 1}
-        />
-      </Th>
-    </Tr>
-  );
-};
-
-TableOptions.defaultProps = {
-  nosearch: false
-};
-
-const mapStateToProps = ({ url }) => ({ url });
-
-const mapDispatchToProps = dispatch => ({
-  getUrlsList: bindActionCreators(getUrlsList, dispatch)
-});
-
-export default connect(
-  mapStateToProps,
-  mapDispatchToProps
-)(TableOptions);

+ 41 - 5
client/components/Text.tsx

@@ -1,12 +1,18 @@
-import styled, { css } from "styled-components";
+import { switchProp, ifNotProp, ifProp } from "styled-tools";
 import { Box } from "reflexbox/styled-components";
-import { switchProp, ifNotProp } from "styled-tools";
+import styled, { css } from "styled-components";
+
+import { Colors } from "../consts";
+import { FC, ComponentProps } from "react";
 
 interface Props {
-  weight?: 300 | 400 | 700;
   htmlFor?: string;
+  light?: boolean;
+  normal?: boolean;
+  bold?: boolean;
 }
 const Text = styled(Box)<Props>`
+  font-weight: 400;
   ${ifNotProp(
     "fontSize",
     css`
@@ -20,12 +26,42 @@ const Text = styled(Box)<Props>`
       })};
     `
   )}
+
+  ${ifProp(
+    "light",
+    css`
+      font-weight: 300;
+    `
+  )}
+
+  ${ifProp(
+    "normal",
+    css`
+      font-weight: 400;
+    `
+  )}
+
+  ${ifProp(
+    "bold",
+    css`
+      font-weight: 700;
+    `
+  )}
 `;
 
 Text.defaultProps = {
   as: "p",
-  fontWeight: 400,
-  color: "hsl(200, 35%, 25%)"
+  color: Colors.Text
 };
 
 export default Text;
+
+type TextProps = ComponentProps<typeof Text>;
+
+export const H1: FC<TextProps> = props => <Text as="h1" {...props} />;
+export const H2: FC<TextProps> = props => <Text as="h2" {...props} />;
+export const H3: FC<TextProps> = props => <Text as="h3" {...props} />;
+export const H4: FC<TextProps> = props => <Text as="h4" {...props} />;
+export const H5: FC<TextProps> = props => <Text as="h5" {...props} />;
+export const H6: FC<TextProps> = props => <Text as="h6" {...props} />;
+export const Span: FC<TextProps> = props => <Text as="span" {...props} />;

+ 29 - 1
client/consts/consts.ts

@@ -1,6 +1,7 @@
 export enum API {
   LOGIN = "/api/auth/login",
   SIGNUP = "/api/auth/signup",
+  RENEW = "/api/auth/renew",
   REPORT = "/api/url/report",
   RESET_PASSWORD = "/api/auth/resetpassword",
   CHANGE_PASSWORD = "/api/auth/changepassword",
@@ -10,5 +11,32 @@ export enum API {
   SETTINGS = "/api/auth/usersettings",
   SUBMIT = "/api/url/submit",
   GET_LINKS = "/api/url/geturls",
-  DELETE_LINK = "/api/url/deleteurl"
+  DELETE_LINK = "/api/url/deleteurl",
+  STATS = "/api/url/stats"
+}
+
+export enum Colors {
+  Text = "hsl(200, 35%, 25%)",
+  Bg = "hsl(206, 12%, 95%)",
+  Spinner = "hsl(200, 15%, 70%)",
+  FeaturesBg = "hsl(230, 15%, 92%)",
+  ExtensionsBg = "hsl(230, 15%, 20%)",
+  IconShadow = "hsla(200, 15%, 60%, 0.12)",
+  CopyIcon = "hsl(144, 40%, 57%)",
+  CopyIconBg = "hsl(144, 100%, 96%)",
+  CheckIcon = "hsl(144, 50%, 60%)",
+  TrashIcon = "hsl(0, 100%, 69%)",
+  TrashIconBg = "hsl(0, 100%, 96%)",
+  QrCodeIcon = "hsl(0, 0%, 35%)",
+  QrCodeIconBg = "hsl(0, 0%, 94%)",
+  PieIcon = "hsl(260, 100%, 69%)",
+  PieIconBg = "hsl(260, 100%, 96%)",
+  TableHeadBg = "hsl(200, 12%, 95%)",
+  TableHeadBorder = "hsl(200, 14%, 94%)",
+  TableBorder = "hsl(200, 14%, 90%)",
+  TableRowHover = "hsl(200, 14%, 98%)",
+  TableShadow = "hsla(200, 20%, 70%, 0.3)",
+  StatsLastUpdateText = "hsl(200, 14%, 60%)",
+  StatsTotalUnderline = "hsl(200, 35%, 65%)",
+  Divider = "hsl(200, 15%, 90%)"
 }

+ 3 - 3
client/helpers/analytics.js → client/helpers/analytics.ts

@@ -1,4 +1,4 @@
-import ReactGA from 'react-ga';
+import ReactGA from "react-ga";
 
 export const initGA = () => {
   ReactGA.initialize(process.env.GOOGLE_ANALYTICS);
@@ -9,13 +9,13 @@ export const logPageView = () => {
   ReactGA.pageview(window.location.pathname);
 };
 
-export const logEvent = (category = '', action = '') => {
+export const logEvent = (category = "", action = "") => {
   if (category && action) {
     ReactGA.event({ category, action });
   }
 };
 
-export const logException = (description = '', fatal = false) => {
+export const logException = (description = "", fatal = false) => {
   if (description) {
     ReactGA.exception({ description, fatal });
   }

+ 33 - 13
client/pages/_app.tsx

@@ -1,14 +1,17 @@
 import App, { AppContext } from "next/app";
 import { StoreProvider } from "easy-peasy";
-import Head from "next/head";
 import Router from "next/router";
-import React from "react";
-import { Provider } from "react-redux";
 import decode from "jwt-decode";
+import cookie from "js-cookie";
+import Head from "next/head";
+import React from "react";
 
-import withReduxStore from "../with-redux-store";
+import { initGA, logPageView } from "../helpers/analytics";
 import { initializeStore } from "../store";
 import { TokenPayload } from "../types";
+import AppWrapper from "../components/AppWrapper";
+
+const isProd = process.env.NODE_ENV === "production";
 
 // TODO: types
 class MyApp extends App<any> {
@@ -39,27 +42,44 @@ class MyApp extends App<any> {
   }
 
   componentDidMount() {
-    const { loading } = this.store.dispatch;
+    const { loading, auth } = this.store.dispatch;
+    const token = cookie.get("token");
+
+    if (token) {
+      auth.renew().catch(() => {
+        auth.logout();
+      });
+    }
+
+    if (!isProd) {
+      initGA();
+    }
+
     Router.events.on("routeChangeStart", () => loading.show());
-    Router.events.on("routeChangeComplete", () => loading.hide());
+    Router.events.on("routeChangeComplete", () => {
+      loading.hide();
+
+      if (isProd) {
+        logPageView();
+      }
+    });
     Router.events.on("routeChangeError", () => loading.hide());
   }
 
   render() {
-    const { Component, pageProps, reduxStore } = this.props;
+    const { Component, pageProps } = this.props;
+
     return (
       <>
         <Head>
           <title>Kutt.it | Modern Open Source URL shortener.</title>
         </Head>
-        <Provider store={reduxStore}>
-          <StoreProvider store={this.store}>
-            <Component {...pageProps} />
-          </StoreProvider>
-        </Provider>
+        <StoreProvider store={this.store}>
+          <Component {...pageProps} />
+        </StoreProvider>
       </>
     );
   }
 }
 
-export default withReduxStore(MyApp);
+export default MyApp;

+ 3 - 2
client/pages/_document.tsx

@@ -1,6 +1,7 @@
 import React from "react";
 import Document, { Head, Main, NextScript } from "next/document";
 import { ServerStyleSheet } from "styled-components";
+import { Colors } from "../consts";
 
 interface Props {
   styleTags: any;
@@ -79,10 +80,10 @@ class AppDocument extends Document<Props> {
         <body
           style={{
             margin: 0,
-            backgroundColor: "hsl(206, 12%, 95%)",
+            backgroundColor: Colors.Bg,
             font: '16px/1.45 "Nunito", sans-serif',
             overflowX: "hidden",
-            color: "hsl(200, 35%, 25%)"
+            color: Colors.Text
           }}
         >
           <Main />

+ 15 - 18
client/pages/banned.tsx

@@ -1,37 +1,34 @@
-import React from "react";
-import Link from "next/link";
 import { Flex } from "reflexbox/styled-components";
+import Link from "next/link";
+import React from "react";
 
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
+import { H2, H4, Span } from "../components/Text";
 import Footer from "../components/Footer";
-import Text from "../components/Text";
 import ALink from "../components/ALink";
+import { Col } from "../components/Layout";
 
 const BannedPage = () => {
   return (
-    <BodyWrapper>
-      <Flex flex="1 1 100%" flexDirection="column" alignItems="center">
-        <Text as="h2" textAlign="center" my={3} fontWeight={400}>
+    <AppWrapper>
+      <Col flex="1 1 100%" alignItems="center">
+        <H2 textAlign="center" my={3} normal>
           Link has been banned and removed because of{" "}
-          <Text
-            as="span"
-            fontWeight={700}
-            style={{ borderBottom: "1px dotted rgba(0, 0, 0, 0.4)" }}
-          >
+          <Span style={{ borderBottom: "1px dotted rgba(0, 0, 0, 0.4)" }} bold>
             malware or scam
-          </Text>
+          </Span>
           .
-        </Text>
-        <Text as="h4" textAlign="center" fontWeight={400}>
+        </H2>
+        <H4 textAlign="center" normal>
           If you noticed a malware/scam link shortened by Kutt,{" "}
           <Link href="/report">
             <ALink title="Send report">send us a report</ALink>
           </Link>
           .
-        </Text>
-      </Flex>
+        </H4>
+      </Col>
       <Footer />
-    </BodyWrapper>
+    </AppWrapper>
   );
 };
 

+ 3 - 3
client/pages/index.tsx

@@ -1,9 +1,9 @@
 import React from "react";
 
 import NeedToLogin from "../components/NeedToLogin";
-import BodyWrapper from "../components/BodyWrapper";
 import Extensions from "../components/Extensions";
 import LinksTable from "../components/LinksTable";
+import AppWrapper from "../components/AppWrapper";
 import Shortener from "../components/Shortener";
 import Features from "../components/Features";
 import Footer from "../components/Footer";
@@ -13,14 +13,14 @@ const Homepage = () => {
   const isAuthenticated = useStoreState(s => s.auth.isAuthenticated);
 
   return (
-    <BodyWrapper>
+    <AppWrapper>
       <Shortener />
       {!isAuthenticated && <NeedToLogin />}
       {isAuthenticated && <LinksTable />}
       <Features />
       <Extensions />
       <Footer />
-    </BodyWrapper>
+    </AppWrapper>
   );
 };
 

+ 13 - 11
client/pages/login.tsx

@@ -8,14 +8,14 @@ import { useFormState } from "react-use-form-state";
 import { Flex } from "reflexbox/styled-components";
 
 import { useStoreState, useStoreActions } from "../store";
-import BodyWrapper from "../components/BodyWrapper";
+import { ColCenterV } from "../components/Layout";
+import AppWrapper from "../components/AppWrapper";
 import { fadeIn } from "../helpers/animations";
-import { API } from "../consts";
 import TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
-import Text from "../components/Text";
 import ALink from "../components/ALink";
-import { ColCenterV } from "../components/Layout";
+import Text, { H2 } from "../components/Text";
+import { API } from "../consts";
 
 const LoginForm = styled(Flex).attrs({
   as: "form",
@@ -95,29 +95,31 @@ const LoginPage = () => {
   }
 
   return (
-    <BodyWrapper>
+    <AppWrapper>
       <ColCenterV flex="0 0 auto" mt={24} mb={64}>
         {verifying ? (
-          <Text fontWeight={300} as="h2" textAlign="center">
+          <H2 textAlign="center" light>
             A verification email has been sent to{" "}
             <Email>{formState.values.email}</Email>.
-          </Text>
+          </H2>
         ) : (
           <LoginForm id="login-form" onSubmit={onSubmit("login")}>
-            <Text {...label("email")} as="label" fontWeight={700} mb={2}>
+            <Text {...label("email")} as="label" mb={2} bold>
               Email address:
             </Text>
             <TextInput
               {...email("email")}
+              placeholder="Email address..."
               height={[56, 64, 72]}
               mb={[24, 32, 36]}
               autoFocus
             />
-            <Text {...label("password")} as="label" fontWeight={700} mb={2}>
+            <Text {...label("password")} as="label" mb={2} bold>
               Password (min chars: 8):
             </Text>
             <TextInput
               {...password("password")}
+              placeholder="Password..."
               height={[56, 64, 72]}
               mb={[24, 32, 36]}
             />
@@ -153,13 +155,13 @@ const LoginPage = () => {
                 Forgot your password?
               </ALink>
             </Link>
-            <Text color="red" fontWeight={400} mt={1}>
+            <Text color="red" mt={1} normal>
               {error}
             </Text>
           </LoginForm>
         )}
       </ColCenterV>
-    </BodyWrapper>
+    </AppWrapper>
   );
 };
 

+ 14 - 20
client/pages/report.tsx

@@ -3,12 +3,13 @@ import axios from "axios";
 import { useFormState } from "react-use-form-state";
 import { Flex } from "reflexbox/styled-components";
 
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
 import TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
-import Text from "../components/Text";
-import { API } from "../consts";
+import Text, { H2, Span } from "../components/Text";
 import { useMessage } from "../hooks";
+import { API } from "../consts";
+import { Col } from "../components/Layout";
 
 const ReportPage = () => {
   const [formState, { text }] = useFormState<{ url: string }>();
@@ -31,27 +32,20 @@ const ReportPage = () => {
   };
 
   return (
-    <BodyWrapper>
-      <Flex
-        width={600}
-        maxWidth="97%"
-        flexDirection="column"
-        alignItems="flex-start"
-      >
-        <Text as="h2" fontWeight={700} my={3}>
+    <AppWrapper>
+      <Col width={600} maxWidth="97%" alignItems="flex-start">
+        <H2 my={3} bold>
           Report abuse
-        </Text>
-        <Text as="p" mb={3}>
+        </H2>
+        <Text mb={3}>
           Report abuses, malware and phishing links to the below email address
           or use the form. We will take actions shortly.
         </Text>
-        <Text as="p" mb={4}>
+        <Text mb={4}>
           {(process.env.REPORT_EMAIL || "").replace("@", "[at]")}
         </Text>
-        <Text as="p" mb={3}>
-          <Text as="span" fontWeight={700}>
-            URL containing malware/scam:
-          </Text>
+        <Text mb={3}>
+          <Span bold>URL containing malware/scam:</Span>
         </Text>
         <Flex
           as="form"
@@ -82,8 +76,8 @@ const ReportPage = () => {
         <Text fontSize={14} mt={3} color={message.color}>
           {message.text}
         </Text>
-      </Flex>
-    </BodyWrapper>
+      </Col>
+    </AppWrapper>
   );
 };
 

+ 11 - 10
client/pages/reset-password.tsx

@@ -8,13 +8,14 @@ import cookie from "js-cookie";
 import axios from "axios";
 
 import { useStoreState, useStoreActions } from "../store";
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
 import TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
 import { TokenPayload } from "../types";
-import Text from "../components/Text";
+import Text, { H2 } from "../components/Text";
 import { useMessage } from "../hooks";
 import { API } from "../consts";
+import { Col } from "../components/Layout";
 
 interface Props {
   token?: string;
@@ -59,12 +60,12 @@ const ResetPassword: NextPage<Props> = ({ token }) => {
 
   // FIXME: make a container for width
   return (
-    <BodyWrapper>
-      <Flex width={600} maxWidth="97%" flexDirection="column">
-        <Text as="h2" fontWeight={700} my={3}>
+    <AppWrapper>
+      <Col width={600} maxWidth="97%">
+        <H2 my={3} bold>
           Reset password
-        </Text>
-        <Text as="p" mb={4}>
+        </H2>
+        <Text mb={4}>
           If you forgot you password you can use the form below to get reset
           password link.
         </Text>
@@ -94,11 +95,11 @@ const ResetPassword: NextPage<Props> = ({ token }) => {
             Reset password
           </Button>
         </Flex>
-        <Text fontSize={14} fontWeight={400} color={message.color} mt={2}>
+        <Text fontSize={14} color={message.color} mt={2} normal>
           {message.text}
         </Text>
-      </Flex>
-    </BodyWrapper>
+      </Col>
+    </AppWrapper>
   );
 };
 

+ 15 - 21
client/pages/settings.tsx

@@ -1,42 +1,36 @@
 import { Flex } from "reflexbox/styled-components";
-import { NextPage } from "next";
 import React, { useEffect } from "react";
+import { NextPage } from "next";
 
 import SettingsPassword from "../components/Settings/SettingsPassword";
 import SettingsDomain from "../components/Settings/SettingsDomain";
 import SettingsBan from "../components/Settings/SettingsBan";
 import SettingsApi from "../components/Settings/SettingsApi";
-import BodyWrapper from "../components/BodyWrapper";
+import { useStoreState, useStoreActions } from "../store";
+import AppWrapper from "../components/AppWrapper";
+import { H1, Span } from "../components/Text";
 import Divider from "../components/Divider";
 import Footer from "../components/Footer";
-import { useStoreState, useStoreActions } from "../store";
-import Text from "../components/Text";
+import { Col } from "../components/Layout";
 
-const SettingsPage: NextPage = () => {
+const SettingsPage: NextPage = props => {
   const { email, isAdmin } = useStoreState(s => s.auth);
   const getSettings = useStoreActions(s => s.settings.getSettings);
 
   useEffect(() => {
     getSettings();
-  }, []);
+  }, [false]);
 
   return (
-    <BodyWrapper>
-      <Flex
-        width={600}
-        maxWidth="90%"
-        flexDirection="column"
-        alignItems="flex-start"
-        pb={80}
-        mt={4}
-      >
-        <Text as="h1" alignItems="center" fontWeight={300} fontSize={[24, 28]}>
+    <AppWrapper>
+      <Col width={600} maxWidth="90%" alignItems="flex-start" pb={80} mt={4}>
+        <H1 alignItems="center" fontSize={[24, 28]} light>
           Welcome,{" "}
-          <Text as="span" pb="2px" style={{ borderBottom: "2px dotted #999" }}>
+          <Span pb="2px" style={{ borderBottom: "2px dotted #999" }}>
             {email}
-          </Text>
+          </Span>
           .
-        </Text>
+        </H1>
         <Divider my={[4, 48]} />
         {isAdmin && (
           <>
@@ -49,9 +43,9 @@ const SettingsPage: NextPage = () => {
         <SettingsPassword />
         <Divider my={[12, 24]} />
         <SettingsApi />
-      </Flex>
+      </Col>
       <Footer />
-    </BodyWrapper>
+    </AppWrapper>
   );
 };
 

+ 183 - 8
client/pages/stats.tsx

@@ -1,19 +1,194 @@
-import React from "react";
+import { Box, Flex } from "reflexbox/styled-components";
+import React, { useState, useEffect } from "react";
+import formatDate from "date-fns/format";
 import { NextPage } from "next";
+import Link from "next/link";
+import axios from "axios";
 
-import BodyWrapper from "../components/BodyWrapper";
-import Stats from "../components/Stats";
+import { getAxiosConfig, removeProtocol } from "../utils";
+import { Button, NavButton } from "../components/Button";
+import { Col, RowCenterV } from "../components/Layout";
+import { Area, Bar, Pie } from "../components/Charts";
+import PageLoading from "../components/PageLoading";
+import AppWrapper from "../components/AppWrapper";
+import Text, { H1, H2, H4, Span } from "../components/Text";
+import Divider from "../components/Divider";
+import { useStoreState } from "../store";
+import ALink from "../components/ALink";
+import { API, Colors } from "../consts";
 
 interface Props {
   domain?: string;
   id?: string;
 }
 
-const StatsPage: NextPage<Props> = ({ domain, id }) => (
-  <BodyWrapper>
-    <Stats domain={domain} id={id} />
-  </BodyWrapper>
-);
+const StatsPage: NextPage<Props> = ({ domain, id }) => {
+  const { isAuthenticated } = useStoreState(s => s.auth);
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(false);
+  const [data, setData] = useState();
+  const [period, setPeriod] = useState("lastDay");
+
+  const stats = data && data[period];
+
+  useEffect(() => {
+    if (!id) return;
+    axios
+      .get(`${API.STATS}?id=${id}&domain=${domain}`, getAxiosConfig())
+      .then(({ data }) => {
+        setLoading(false);
+        setError(!data);
+        setData(data);
+      })
+      .catch(() => {
+        setLoading(false);
+        setError(true);
+      });
+  }, []);
+
+  let errorMessage;
+
+  if (!isAuthenticated) {
+    // TODO: use icons
+    errorMessage = <H2>You need to login to view stats.</H2>;
+  }
+
+  if (!id || error) {
+    // TODO: use icons
+    errorMessage = <H2>Couldn't get stats.</H2>;
+  }
+
+  const loader = loading && <PageLoading />;
+
+  const total = stats && stats.views.reduce((sum, view) => sum + view, 0);
+  const periodText = period.includes("last")
+    ? `the last ${period.replace("last", "").toLocaleLowerCase()}`
+    : "all time";
+
+  return (
+    <AppWrapper>
+      {errorMessage ||
+        loader ||
+        (data && (
+          <Col width={1200} maxWidth="95%" alignItems="stretch" m="40px 0">
+            <Flex justifyContent="space-between" alignItems="center" mb={3}>
+              <H1 fontSize={[18, 20, 24]} light>
+                Stats for:{" "}
+                <ALink href={data.shortLink} title="Short link">
+                  {removeProtocol(data.shortLink)}
+                </ALink>
+              </H1>
+              <Text fontSize={[13, 14]} textAlign="right">
+                {data.target.length > 80
+                  ? `${data.target
+                      .split("")
+                      .slice(0, 80)
+                      .join("")}...`
+                  : data.target}
+              </Text>
+            </Flex>
+            <Col
+              backgroundColor="white"
+              style={{
+                borderRadius: 12,
+                boxShadow: "0 6px 15px hsla(200, 20%, 70%, 0.3)",
+                overflow: "hidden"
+              }}
+            >
+              <RowCenterV
+                flex="1 1 auto"
+                backgroundColor={Colors.TableHeadBg}
+                justifyContent="space-between"
+                py={[3, 3, 24]}
+                px={[3, 4]}
+              >
+                <H4>
+                  Total clicks: <Span bold>{data.total}</Span>
+                </H4>
+                <Flex>
+                  {[
+                    ["allTime", "All Time"],
+                    ["lastMonth", "Month"],
+                    ["lastWeek", "Week"],
+                    ["lastDay", "Day"]
+                  ].map(([p, n]) => (
+                    <NavButton
+                      ml={10}
+                      disabled={p === period}
+                      onClick={() => setPeriod(p as any)}
+                    >
+                      {n}
+                    </NavButton>
+                  ))}
+                </Flex>
+              </RowCenterV>
+              <Col p={[3, 4]}>
+                <H2 mb={2} light>
+                  <Span
+                    style={{
+                      borderBottom: `1px dotted ${Colors.StatsTotalUnderline}`
+                    }}
+                    bold
+                  >
+                    {total}
+                  </Span>{" "}
+                  tracked clicks in {periodText}.
+                </H2>
+                <Text fontSize={[13, 14]} color={Colors.StatsLastUpdateText}>
+                  Last update in{" "}
+                  {formatDate(new Date(data.updatedAt), "hh:mm aa")}
+                </Text>
+                <Flex width={1} mt={4}>
+                  <Area data={stats.views} period={period} />
+                </Flex>
+                {total > 0 && (
+                  <>
+                    <Divider my={4} />
+                    <Flex width={1}>
+                      <Col flex="1 1 0">
+                        <H2 mb={3} light>
+                          Referrals.
+                        </H2>
+                        <Pie data={stats.stats.referrer} />
+                      </Col>
+                      <Col flex="1 1 0">
+                        <H2 mb={3} light>
+                          Browsers.
+                        </H2>
+                        <Bar data={stats.stats.browser} />
+                      </Col>
+                    </Flex>
+                    <Divider my={4} />
+                    <Flex width={1}>
+                      <Col flex="1 1 0">
+                        <H2 mb={3} light>
+                          Country.
+                        </H2>
+                        <Pie data={stats.stats.country} />
+                      </Col>
+                      <Col flex="1 1 0">
+                        <H2 mb={3} light>
+                          OS.
+                        </H2>
+                        <Bar data={stats.stats.os} />
+                      </Col>
+                    </Flex>
+                  </>
+                )}
+              </Col>
+            </Col>
+            <Box alignSelf="center" my={64}>
+              <Link href="/">
+                <ALink href="/" title="Back to homepage" forButton>
+                  <Button icon="arrow-left">Back to homepage</Button>
+                </ALink>
+              </Link>
+            </Box>
+          </Col>
+        ))}
+    </AppWrapper>
+  );
+};
 
 StatsPage.getInitialProps = ({ query }) => {
   return Promise.resolve(query);

+ 6 - 10
client/pages/terms.tsx

@@ -1,17 +1,13 @@
 import React from "react";
 import { Flex } from "reflexbox/styled-components";
 
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
+import { Col } from "../components/Layout";
 
 const TermsPage = () => (
-  <BodyWrapper>
+  <AppWrapper>
     {/* TODO: better container */}
-    <Flex
-      width={600}
-      maxWidth="97%"
-      flexDirection="column"
-      alignItems="flex-start"
-    >
+    <Col width={600} maxWidth="97%" alignItems="flex-start">
       <h3>Kutt Terms of Service</h3>
       <p>
         By accessing the website at{" "}
@@ -51,8 +47,8 @@ const TermsPage = () => (
         without notice. By using this website you are agreeing to be bound by
         the then current version of these terms of service.
       </p>
-    </Flex>
-  </BodyWrapper>
+    </Col>
+  </AppWrapper>
 );
 
 export default TermsPage;

+ 12 - 13
client/pages/url-info.tsx

@@ -3,9 +3,10 @@ import styled from "styled-components";
 import { Flex } from "reflexbox/styled-components";
 import { NextPage } from "next";
 
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
 import Footer from "../components/Footer";
-import Text from "../components/Text";
+import { H2, H4 } from "../components/Text";
+import { Col } from "../components/Layout";
 
 interface Props {
   linkTarget?: string;
@@ -13,25 +14,23 @@ interface Props {
 
 const UrlInfoPage: NextPage<Props> = ({ linkTarget }) => {
   return (
-    <BodyWrapper>
+    <AppWrapper>
       {!linkTarget ? (
-        <Text as="h2" my={4} fontWeight={300}>
+        <H2 my={4} light>
           404 | Link could not be found.
-        </Text>
+        </H2>
       ) : (
         <>
-          <Flex flex="1 1 100%" flexDirection="column" alignItems="center">
-            <Text as="h2" my={3} fontWeight={300}>
+          <Col flex="1 1 100%" alignItems="center">
+            <H2 my={3} light>
               Target:
-            </Text>
-            <Text as="h4" fontWeight={700}>
-              {linkTarget}
-            </Text>
-          </Flex>
+            </H2>
+            <H4 bold>{linkTarget}</H4>
+          </Col>
           <Footer />
         </>
       )}
-    </BodyWrapper>
+    </AppWrapper>
   );
 };
 

+ 15 - 17
client/pages/url-password.tsx

@@ -1,14 +1,14 @@
+import { useFormState } from "react-use-form-state";
+import { Flex } from "reflexbox/styled-components";
 import React, { useState } from "react";
 import { NextPage } from "next";
-import styled from "styled-components";
 import axios from "axios";
-import { Flex } from "reflexbox/styled-components";
-import { useFormState } from "react-use-form-state";
 
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
 import TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
-import Text from "../components/Text";
+import Text, { H2 } from "../components/Text";
+import { Col } from "../components/Layout";
 
 interface Props {
   protectedLink?: string;
@@ -43,19 +43,17 @@ const UrlPasswordPage: NextPage<Props> = ({ protectedLink }) => {
   };
 
   return (
-    <BodyWrapper>
+    <AppWrapper>
       {!protectedLink ? (
-        <Text as="h2" my={4} fontWeight={300}>
+        <H2 my={4} light>
           404 | Link could not be found.
-        </Text>
+        </H2>
       ) : (
-        <Flex width={500} maxWidth="97%" flexDirection="column">
-          <Text as="h2" fontWeight={700} my={3}>
+        <Col width={500} maxWidth="97%">
+          <H2 my={3} bold>
             Protected link
-          </Text>
-          <Text as="p" mb={4}>
-            Enter the password to be redirected to the link.
-          </Text>
+          </H2>
+          <Text mb={4}>Enter the password to be redirected to the link.</Text>
           <Flex
             as="form"
             alignItems="center"
@@ -79,12 +77,12 @@ const UrlPasswordPage: NextPage<Props> = ({ protectedLink }) => {
               Go
             </Button>
           </Flex>
-          <Text fontSize={14} color="red" fontWeight={400} mt={3}>
+          <Text fontSize={14} color="red" mt={3} normal>
             {error}
           </Text>
-        </Flex>
+        </Col>
       )}
-    </BodyWrapper>
+    </AppWrapper>
   );
 };
 

+ 26 - 22
client/pages/verify.tsx

@@ -1,15 +1,16 @@
+import { Flex } from "reflexbox/styled-components";
 import React, { useEffect } from "react";
-import Router from "next/router";
 import styled from "styled-components";
-import cookie from "js-cookie";
-import { Flex } from "reflexbox/styled-components";
+import Router from "next/router";
 import decode from "jwt-decode";
+import cookie from "js-cookie";
 
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
 import { Button } from "../components/Button";
-import { NextPage } from "next";
-import { TokenPayload } from "../types";
 import { useStoreActions } from "../store";
+import { TokenPayload } from "../types";
+import { NextPage } from "next";
+import { Col } from "../components/Layout";
 
 interface Props {
   token?: string;
@@ -58,23 +59,26 @@ const Verify: NextPage<Props> = ({ token }) => {
     Router.push("/");
   };
 
-  const message = token ? (
-    <Flex flexDirection="column" alignItems="center">
-      <MessageWrapper>
-        <Icon src="/images/check.svg" />
-        <Message>Your account has been verified successfully!</Message>
-      </MessageWrapper>
-      <Button icon="arrow-left" onClick={goToHomepage}>
-        Back to homepage
-      </Button>
-    </Flex>
-  ) : (
-    <MessageWrapper>
-      <Icon src="/images/x.svg" />
-      <Message>Invalid verification.</Message>
-    </MessageWrapper>
+  return (
+    <AppWrapper>
+      {token ? (
+        <Col alignItems="center">
+          <MessageWrapper>
+            <Icon src="/images/check.svg" />
+            <Message>Your account has been verified successfully!</Message>
+          </MessageWrapper>
+          <Button icon="arrow-left" onClick={goToHomepage}>
+            Back to homepage
+          </Button>
+        </Col>
+      ) : (
+        <MessageWrapper>
+          <Icon src="/images/x.svg" />
+          <Message>Invalid verification.</Message>
+        </MessageWrapper>
+      )}
+    </AppWrapper>
   );
-  return <BodyWrapper norenew>{message}</BodyWrapper>;
 };
 
 Verify.getInitialProps = async ({ req }) => {

+ 0 - 91
client/reducers/__test__/auth.js

@@ -1,91 +0,0 @@
-import { expect } from 'chai';
-import deepFreeze from 'deep-freeze';
-
-import {
-  AUTH_USER,
-  AUTH_RENEW,
-  UNAUTH_USER,
-  SENT_VERIFICATION
-} from '../../actions/actionTypes';
-
-import reducer from '../auth';
-
-describe('auth reducer', () => {
-  const initialState = {
-    admin: false,
-    isAuthenticated: false,
-    sentVerification: false,
-    user: '',
-    renew: false,
-  };
-
-  beforeEach(() => {
-    deepFreeze(initialState);
-  });
-
-  it('should return the initial state', () => {
-    expect(reducer(undefined, {})).to.deep.equal(initialState);
-  });
-
-  it('should handle AUTH_USER', () => {
-    const jwt = {
-      domain: '',
-      exp: 1529137738725,
-      iat: 1529137738725,
-      iss: 'ApiAuth',
-      sub: 'test@user.com',
-    };
-
-    const user = 'test@user.com';
-
-    const state = reducer(initialState, {
-      type: AUTH_USER,
-      payload: jwt
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.isAuthenticated).to.be.true;
-    expect(state.user).to.be.equal(user);
-    expect(state.sentVerification).to.be.false;
-  });
-
-  it('should handle AUTH_RENEW', () => {
-    const state = reducer(initialState, {
-      type: AUTH_RENEW
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.renew).to.be.true;
-  });
-
-  it('should handle UNAUTH_USER', () => {
-    const state = reducer(initialState, {
-      type: UNAUTH_USER
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state).to.deep.equal(initialState);
-  });
-
-  it('should handle SENT_VERIFICATION', () => {
-    const user = 'test@user.com';
-
-    const state = reducer(initialState, {
-      type: SENT_VERIFICATION,
-      payload: user
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.sentVerification).to.be.true;
-    expect(state.user).to.be.equal(user);
-  });
-
-  it('should not handle other action types', () => {
-    const state = reducer(initialState, {
-      type: 'ANOTHER_ACTION'
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state).to.deep.equal(initialState);
-  });
-});

+ 0 - 134
client/reducers/__test__/error.js

@@ -1,134 +0,0 @@
-import { expect } from 'chai';
-import deepFreeze from 'deep-freeze';
-
-import {
-  SHORTENER_ERROR,
-  DOMAIN_ERROR,
-  SET_DOMAIN,
-  SHOW_DOMAIN_INPUT,
-  ADD_URL,
-  UPDATE_URL,
-  AUTH_ERROR,
-  AUTH_USER,
-  HIDE_PAGE_LOADING
-} from '../../actions/actionTypes';
-
-import reducer from '../error';
-
-describe('error reducer', () => {
-  const initialState = {
-    auth: '',
-    domain: '',
-    shortener: '',
-    urlOptions: ''
-  };
-
-  beforeEach(() => {
-    deepFreeze(initialState);
-  });
-
-  it('should return the initial state', () => {
-    expect(reducer(undefined, {})).to.deep.equal(initialState);
-  });
-
-  it('should handle SHORTENER_ERROR', () => {
-    const error = 'SHORTENER_ERROR';
-
-    const state = reducer(initialState, {
-      type: SHORTENER_ERROR,
-      payload: error
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.shortener).to.be.equal(error);
-  });
-
-  it('should handle DOMAIN_ERROR', () => {
-    const error = 'DOMAIN_ERROR';
-
-    const state = reducer(initialState, {
-      type: DOMAIN_ERROR,
-      payload: error
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.domain).to.be.equal(error);
-  });
-
-  it('should handle SET_DOMAIN', () => {
-    const state = reducer(initialState, {
-      type: SET_DOMAIN
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.domain).to.be.empty;
-  });
-
-  it('should handle SHOW_DOMAIN_INPUT', () => {
-    const state = reducer(initialState, {
-      type: SHOW_DOMAIN_INPUT
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.domain).to.be.empty;
-  });
-
-  it('should handle ADD_URL', () => {
-    const state = reducer(initialState, {
-      type: ADD_URL
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.shortener).to.be.empty;
-  });
-
-  it('should handle UPDATE_URL', () => {
-    const state = reducer(initialState, {
-      type: UPDATE_URL
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.urlOptions).to.be.empty;
-  });
-
-  it('should handle AUTH_ERROR', () => {
-    const error = 'AUTH_ERROR';
-
-    const state = reducer(initialState, {
-      type: AUTH_ERROR,
-      payload: error
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.auth).to.be.equal(error);
-  });
-
-  it('should handle AUTH_USER', () => {
-    const state = reducer(initialState, {
-      type: AUTH_USER
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.auth).to.be.empty;
-  });
-
-  it('should handle HIDE_PAGE_LOADING', () => {
-    const state = reducer(initialState, {
-      type: HIDE_PAGE_LOADING
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.auth).to.be.empty;
-    expect(state.shortener).to.be.empty;
-    expect(state.urlOptions).to.be.empty;
-  });
-
-  it('should not handle other action types', () => {
-    const state = reducer(initialState, {
-      type: 'ANOTHER_ACTION'
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state).to.deep.equal(initialState);
-  });
-});

+ 0 - 211
client/reducers/__test__/loading.js

@@ -1,211 +0,0 @@
-import { expect } from 'chai';
-import deepFreeze from 'deep-freeze';
-
-import {
-  SHOW_PAGE_LOADING,
-  HIDE_PAGE_LOADING,
-  TABLE_LOADING,
-  LOGIN_LOADING,
-  SIGNUP_LOADING,
-  SHORTENER_LOADING,
-  ADD_URL,
-  SHORTENER_ERROR,
-  LIST_URLS,
-  DELETE_URL,
-  AUTH_ERROR,
-  AUTH_USER,
-  DOMAIN_LOADING,
-  SET_DOMAIN,
-  DOMAIN_ERROR,
-  API_LOADING,
-  SET_APIKEY
-} from '../../actions/actionTypes';
-
-import reducer from '../loading';
-
-describe('loading reducer', () => {
-  const initialState = {
-    api: false,
-    domain: false,
-    shortener: false,
-    login: false,
-    page: false,
-    table: false,
-    signup: false
-  };
-
-  beforeEach(() => {
-    deepFreeze(initialState);
-  });
-
-  it('should return the initial state', () => {
-    expect(reducer(undefined, {})).to.deep.equal(initialState);
-  });
-
-  it('should handle SHOW_PAGE_LOADING', () => {
-    const state = reducer(initialState, {
-      type: SHOW_PAGE_LOADING
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.page).to.be.true;
-  });
-
-  it('should handle HIDE_PAGE_LOADING', () => {
-    const state = reducer(initialState, {
-      type: HIDE_PAGE_LOADING
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.shortener).to.be.false;
-    expect(state.login).to.be.false;
-    expect(state.page).to.be.false;
-    expect(state.signup).to.be.false;
-  });
-
-  it('should handle TABLE_LOADING', () => {
-    const state = reducer(initialState, {
-      type: TABLE_LOADING
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.table).to.be.true;
-  });
-
-  it('should handle LOGIN_LOADING', () => {
-    const state = reducer(initialState, {
-      type: LOGIN_LOADING
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.login).to.be.true;
-  });
-
-  it('should handle SIGNUP_LOADING', () => {
-    const state = reducer(initialState, {
-      type: SIGNUP_LOADING
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.signup).to.be.true;
-  });
-
-  it('should handle SHORTENER_LOADING', () => {
-    const state = reducer(initialState, {
-      type: SHORTENER_LOADING
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.shortener).to.be.true;
-  });
-
-  it('should handle ADD_URL', () => {
-    const state = reducer(initialState, {
-      type: ADD_URL
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.shortener).to.be.false;
-  });
-
-  it('should handle SHORTENER_ERROR', () => {
-    const state = reducer(initialState, {
-      type: SHORTENER_ERROR
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.shortener).to.be.false;
-  });
-
-  it('should handle LIST_URLS', () => {
-    const state = reducer(initialState, {
-      type: LIST_URLS
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.table).to.be.false;
-  });
-
-  it('should handle DELETE_URL', () => {
-    const state = reducer(initialState, {
-      type: DELETE_URL
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.table).to.be.false;
-  });
-
-  it('should handle AUTH_ERROR', () => {
-    const state = reducer(initialState, {
-      type: AUTH_ERROR
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.login).to.be.false;
-    expect(state.signup).to.be.false;
-  });
-
-  it('should handle AUTH_USER', () => {
-    const state = reducer(initialState, {
-      type: AUTH_USER
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.login).to.be.false;
-    expect(state.signup).to.be.false;
-  });
-
-  it('should handle DOMAIN_LOADING', () => {
-    const state = reducer(initialState, {
-      type: DOMAIN_LOADING
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.domain).to.be.true;
-  });
-
-  it('should handle SET_DOMAIN', () => {
-    const state = reducer(initialState, {
-      type: SET_DOMAIN
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.domain).to.be.false;
-  });
-
-  it('should handle DOMAIN_ERROR', () => {
-    const state = reducer(initialState, {
-      type: DOMAIN_ERROR
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.domain).to.be.false;
-  });
-
-  it('should handle API_LOADING', () => {
-    const state = reducer(initialState, {
-      type: API_LOADING
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.api).to.be.true;
-  });
-
-  it('should handle SET_APIKEY', () => {
-    const state = reducer(initialState, {
-      type: SET_APIKEY
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.api).to.be.false;
-  });
-
-  it('should not handle other action types', () => {
-    const state = reducer(initialState, {
-      type: 'ANOTHER_ACTION'
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state).to.deep.equal(initialState);
-  });
-});

+ 0 - 92
client/reducers/__test__/settings.js

@@ -1,92 +0,0 @@
-import { expect } from 'chai';
-import deepFreeze from 'deep-freeze';
-
-import {
-  SET_DOMAIN,
-  SET_APIKEY,
-  DELETE_DOMAIN,
-  SHOW_DOMAIN_INPUT,
-  UNAUTH_USER
-} from '../../actions/actionTypes';
-
-import reducer from '../settings';
-
-describe('settings reducer', () => {
-  const initialState = {
-    apikey: '',
-    customDomain: '',
-    homepage: '',
-    domainInput: true,
-  };
-
-  beforeEach(() => {
-    deepFreeze(initialState);
-  });
-
-  it('should return the initial state', () => {
-    expect(reducer(undefined, {})).to.deep.equal(initialState);
-  });
-
-  it('should handle SET_DOMAIN', () => {
-    const customDomain = 'example.com';
-    const homepage = '';
-
-    const state = reducer(initialState, {
-      type: SET_DOMAIN,
-      payload: { customDomain, homepage }
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.customDomain).to.be.equal(customDomain);
-    expect(state.domainInput).to.be.false;
-  });
-
-  it('should handle SET_APIKEY', () => {
-    const apikey = '1234567';
-
-    const state = reducer(initialState, {
-      type: SET_APIKEY,
-      payload: apikey
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.apikey).to.be.equal(apikey);
-  });
-
-  it('should handle DELETE_DOMAIN', () => {
-    const state = reducer(initialState, {
-      type: DELETE_DOMAIN
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.customDomain).to.be.empty;
-    expect(state.domainInput).to.be.true;
-  });
-
-  it('should handle SHOW_DOMAIN_INPUT', () => {
-    const state = reducer(initialState, {
-      type: SHOW_DOMAIN_INPUT
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state.domainInput).to.be.true;
-  });
-
-  it('should handle UNAUTH_USER', () => {
-    const state = reducer(initialState, {
-      type: UNAUTH_USER
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state).to.deep.equal(initialState);
-  });
-
-  it('should not handle other action types', () => {
-    const state = reducer(initialState, {
-      type: 'ANOTHER_ACTION'
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state).to.deep.equal(initialState);
-  });
-});

+ 0 - 197
client/reducers/__test__/url.js

@@ -1,197 +0,0 @@
-import { expect } from 'chai';
-import deepFreeze from 'deep-freeze';
-
-import {
-  ADD_URL,
-  UPDATE_URL_LIST,
-  LIST_URLS,
-  DELETE_URL,
-  UNAUTH_USER
-} from '../../actions/actionTypes';
-
-import reducer from '../url';
-
-describe('url reducer', () => {
-  const initialState = {
-    list: [],
-    isShortened: false,
-    count: 10,
-    countAll: 0,
-    page: 1,
-    search: ''
-  };
-
-  beforeEach(() => {
-    deepFreeze(initialState);
-  });
-
-  it('should return the initial state', () => {
-    expect(reducer(undefined, {})).to.deep.equal(initialState);
-  });
-
-  it('should handle ADD_URL', () => {
-    const item = {
-      createdAt: '2018-06-12T19:23:00.272Z',
-      id: 'YufjdS',
-      target: 'https://kutt.it/',
-      password: false,
-      reuse: false,
-      shortLink: 'https://kutt.it/YufjdS'
-    };
-
-    const state = reducer(initialState, {
-      type: ADD_URL,
-      payload: item
-    });
-
-    expect(state.list).to.be.an('array');
-    expect(state.list).to.have.lengthOf(1);
-    expect(state.list).to.include(item);
-    expect(state.isShortened).to.be.true;
-  });
-
-  it('should handle UPDATE_URL_LIST', () => {
-    const count = 10;
-    const page = 1;
-    const search = 'test url';
-
-    const allParamsState = reducer(initialState, {
-      type: UPDATE_URL_LIST,
-      payload: { count, page, search }
-    });
-
-    expect(allParamsState).not.to.be.undefined;
-    expect(allParamsState.count).to.be.equal(count);
-    expect(allParamsState.page).to.be.equal(page);
-    expect(allParamsState.search).to.be.equal(search);
-
-    const countState = reducer(initialState, {
-      type: UPDATE_URL_LIST,
-      payload: { count }
-    });
-
-    expect(countState).not.to.be.undefined;
-    expect(countState.count).to.be.equal(count);
-
-    const pageState = reducer(initialState, {
-      type: UPDATE_URL_LIST,
-      payload: { page }
-    });
-
-    expect(pageState).not.to.be.undefined;
-    expect(pageState.page).to.be.equal(page);
-
-    const searchState = reducer(initialState, {
-      type: UPDATE_URL_LIST,
-      payload: { search }
-    });
-
-    expect(searchState).not.to.be.undefined;
-    expect(searchState.search).to.be.equal(search);
-
-    const state = reducer(initialState, {
-      type: UPDATE_URL_LIST
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state).to.deep.equal(initialState);
-  });
-
-  it('should handle LIST_URLS', () => {
-    const list = [
-      {
-        createdAt: '2018-06-12T19:23:00.272Z',
-        id: 'YufjdS',
-        target: 'https://kutt.it/',
-        password: false,
-        reuse: false,
-        shortLink: 'https://kutt.it/YufjdS'
-      },
-      {
-        createdAt: '2018-06-12T19:51:56.435Z',
-        id: '1gCdbC',
-        target: 'https://kutt.it/',
-        password: false,
-        reuse: false,
-        shortLink: 'https://kutt.it/1gCdbC'
-      }
-    ];
-
-    const countAll = list.length;
-
-    const state = reducer(initialState, {
-      type: LIST_URLS,
-      payload: { list, countAll }
-    });
-
-    expect(state.list).to.be.an('array');
-    expect(state.list).to.have.lengthOf(2);
-    expect(state.list).to.not.include(list);
-    expect(state.countAll).to.be.equal(countAll);
-    expect(state.isShortened).to.be.false;
-  });
-
-  it('should handle DELETE_URL', () => {
-    const itemsState = {
-      list: [
-        {
-          createdAt: '2018-06-12T19:23:00.272Z',
-          id: 'YufjdS',
-          target: 'https://kutt.it/',
-          password: false,
-          reuse: false,
-          shortLink: 'https://kutt.it/YufjdS'
-        },
-        {
-          createdAt: '2018-06-12T19:51:56.435Z',
-          id: '1gCdbC',
-          target: 'https://kutt.it/',
-          password: false,
-          reuse: false,
-          shortLink: 'https://kutt.it/1gCdbC'
-        }
-      ],
-      isShortened: true,
-      count: 10,
-      countAll: 2,
-      page: 1,
-      search: ''
-    };
-
-    deepFreeze(itemsState);
-
-    const state = reducer(itemsState, {
-      type: DELETE_URL,
-      payload: 'YufjdS'
-    });
-
-    expect(state.list).to.be.an('array');
-    expect(state.list).to.have.lengthOf(1);
-    expect(state.list).to.not.include({
-      createdAt: '2018-06-12T19:23:00.272Z',
-      id: 'YufjdS',
-      target: 'https://kutt.it/',
-      password: false,
-      reuse: false,
-      shortLink: 'https://kutt.it/YufjdS'
-    });
-  });
-
-  it('should handle UNAUTH_USER', () => {
-    const state = reducer(initialState, {
-      type: UNAUTH_USER
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state).to.deep.equal(initialState);
-  });
-
-  it('should not handle other action types', () => {
-    const state = reducer(initialState, {
-      type: 'ANOTHER_ACTION'
-    });
-
-    expect(state).not.to.be.undefined;
-    expect(state).to.deep.equal(initialState);
-  });
-});

+ 0 - 32
client/reducers/auth.js

@@ -1,32 +0,0 @@
-import { AUTH_USER, AUTH_RENEW, UNAUTH_USER, SENT_VERIFICATION } from '../actions/actionTypes';
-
-const initialState = {
-  admin: false,
-  isAuthenticated: false,
-  sentVerification: false,
-  user: '',
-  renew: false,
-};
-
-const auth = (state = initialState, action) => {
-  switch (action.type) {
-    case AUTH_USER:
-      return {
-        ...state,
-        isAuthenticated: true,
-        user: action.payload.sub,
-        admin: action.payload.admin,
-        sentVerification: false,
-      };
-    case AUTH_RENEW:
-      return { ...state, renew: true };
-    case UNAUTH_USER:
-      return initialState;
-    case SENT_VERIFICATION:
-      return { ...state, sentVerification: true, user: action.payload };
-    default:
-      return state;
-  }
-};
-
-export default auth;

+ 0 - 49
client/reducers/error.js

@@ -1,49 +0,0 @@
-import {
-  SHORTENER_ERROR,
-  DOMAIN_ERROR,
-  SET_DOMAIN,
-  SHOW_DOMAIN_INPUT,
-  ADD_URL,
-  UPDATE_URL,
-  AUTH_ERROR,
-  AUTH_USER,
-  HIDE_PAGE_LOADING,
-} from '../actions/actionTypes';
-
-const initialState = {
-  auth: '',
-  domain: '',
-  shortener: '',
-  urlOptions: '',
-};
-
-const error = (state = initialState, action) => {
-  switch (action.type) {
-    case SHORTENER_ERROR:
-      return { ...state, shortener: action.payload };
-    case DOMAIN_ERROR:
-      return { ...state, domain: action.payload };
-    case SET_DOMAIN:
-    case SHOW_DOMAIN_INPUT:
-      return { ...state, domain: '' };
-    case ADD_URL:
-      return { ...state, shortener: '' };
-    case UPDATE_URL:
-      return { ...state, urlOptions: '' };
-    case AUTH_ERROR:
-      return { ...state, auth: action.payload };
-    case AUTH_USER:
-      return { ...state, auth: '' };
-    case HIDE_PAGE_LOADING:
-      return {
-        ...state,
-        auth: '',
-        shortener: '',
-        urlOptions: '',
-      };
-    default:
-      return state;
-  }
-};
-
-export default error;

+ 0 - 17
client/reducers/index.js

@@ -1,17 +0,0 @@
-import { combineReducers } from 'redux';
-
-import url from './url';
-import auth from './auth';
-import error from './error';
-import loading from './loading';
-import settings from './settings';
-
-const rootReducer = combineReducers({
-  url,
-  auth,
-  error,
-  loading,
-  settings,
-});
-
-export default rootReducer;

+ 0 - 73
client/reducers/loading.js

@@ -1,73 +0,0 @@
-import {
-  SHOW_PAGE_LOADING,
-  HIDE_PAGE_LOADING,
-  TABLE_LOADING,
-  LOGIN_LOADING,
-  SIGNUP_LOADING,
-  SHORTENER_LOADING,
-  ADD_URL,
-  SHORTENER_ERROR,
-  LIST_URLS,
-  DELETE_URL,
-  AUTH_ERROR,
-  AUTH_USER,
-  DOMAIN_LOADING,
-  SET_DOMAIN,
-  DOMAIN_ERROR,
-  API_LOADING,
-  SET_APIKEY,
-} from '../actions/actionTypes';
-
-const initialState = {
-  api: false,
-  domain: false,
-  shortener: false,
-  login: false,
-  page: false,
-  table: false,
-  signup: false,
-};
-
-const loading = (state = initialState, action) => {
-  switch (action.type) {
-    case SHOW_PAGE_LOADING:
-      return { ...state, page: true };
-    case HIDE_PAGE_LOADING:
-      return {
-        shortener: false,
-        login: false,
-        page: false,
-        signup: false,
-      };
-    case TABLE_LOADING:
-      return { ...state, table: true };
-    case LOGIN_LOADING:
-      return { ...state, login: true };
-    case SIGNUP_LOADING:
-      return { ...state, signup: true };
-    case SHORTENER_LOADING:
-      return { ...state, shortener: true };
-    case ADD_URL:
-    case SHORTENER_ERROR:
-      return { ...state, shortener: false };
-    case LIST_URLS:
-    case DELETE_URL:
-      return { ...state, table: false };
-    case AUTH_ERROR:
-    case AUTH_USER:
-      return { ...state, login: false, signup: false };
-    case DOMAIN_LOADING:
-      return { ...state, domain: true };
-    case SET_DOMAIN:
-    case DOMAIN_ERROR:
-      return { ...state, domain: false };
-    case API_LOADING:
-      return { ...state, api: true };
-    case SET_APIKEY:
-      return { ...state, api: false };
-    default:
-      return state;
-  }
-};
-
-export default loading;

+ 0 - 38
client/reducers/settings.js

@@ -1,38 +0,0 @@
-import {
-  SET_DOMAIN,
-  SET_APIKEY,
-  DELETE_DOMAIN,
-  SHOW_DOMAIN_INPUT,
-  UNAUTH_USER,
-} from '../actions/actionTypes';
-
-const initialState = {
-  apikey: '',
-  customDomain: '',
-  homepage: '',
-  domainInput: true,
-};
-
-const settings = (state = initialState, action) => {
-  switch (action.type) {
-    case SET_DOMAIN:
-      return {
-        ...state,
-        customDomain: action.payload.customDomain,
-        homepage: action.payload.homepage,
-        domainInput: false,
-      };
-    case SET_APIKEY:
-      return { ...state, apikey: action.payload };
-    case DELETE_DOMAIN:
-      return { ...state, customDomain: '', homepage: '', domainInput: true };
-    case SHOW_DOMAIN_INPUT:
-      return { ...state, domainInput: true };
-    case UNAUTH_USER:
-      return initialState;
-    default:
-      return state;
-  }
-};
-
-export default settings;

+ 0 - 49
client/reducers/url.js

@@ -1,49 +0,0 @@
-import {
-  ADD_URL,
-  UPDATE_URL_LIST,
-  LIST_URLS,
-  DELETE_URL,
-  UNAUTH_USER,
-} from '../actions/actionTypes';
-
-const initialState = {
-  list: [],
-  isShortened: false,
-  count: 10,
-  countAll: 0,
-  page: 1,
-  search: '',
-};
-
-const url = (state = initialState, action) => {
-  const { count, page, search } = action.payload || {};
-  const isSearch = typeof search !== 'undefined';
-  switch (action.type) {
-    case ADD_URL:
-      return {
-        ...state,
-        isShortened: true,
-        list: [action.payload, ...state.list],
-      };
-    case UPDATE_URL_LIST:
-      return Object.assign({}, state, count && { count }, page && { page }, isSearch && { search });
-    case LIST_URLS:
-      return {
-        ...state,
-        list: action.payload.list,
-        countAll: action.payload.countAll,
-        isShortened: false,
-      };
-    case DELETE_URL:
-      return {
-        ...state,
-        list: state.list.filter(item => item.id !== action.payload),
-      };
-    case UNAUTH_USER:
-      return initialState;
-    default:
-      return state;
-  }
-};
-
-export default url;

+ 0 - 6
client/redux-store/index.js

@@ -1,6 +0,0 @@
-/* eslint-disable global-require */
-if (process.env.NODE_ENV === 'production') {
-  module.exports = require('./store.prod');
-} else {
-  module.exports = require('./store.dev');
-}

+ 0 - 9
client/redux-store/store.dev.js

@@ -1,9 +0,0 @@
-import { createStore, applyMiddleware } from 'redux';
-import { composeWithDevTools } from 'redux-devtools-extension';
-import thunk from 'redux-thunk';
-import rootReducer from '../reducers';
-
-const store = initialState =>
-  createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(thunk)));
-
-export default store;

+ 0 - 7
client/redux-store/store.prod.js

@@ -1,7 +0,0 @@
-import { createStore, applyMiddleware } from 'redux';
-import thunk from 'redux-thunk';
-import rootReducer from '../reducers';
-
-const store = initialState => createStore(rootReducer, initialState, applyMiddleware(thunk));
-
-export default store;

+ 9 - 0
client/store/auth.ts

@@ -5,6 +5,7 @@ import axios from "axios";
 
 import { TokenPayload } from "../types";
 import { API } from "../consts";
+import { getAxiosConfig } from "../utils";
 
 export interface Auth {
   domain?: string;
@@ -14,6 +15,7 @@ export interface Auth {
   add: Action<Auth, TokenPayload>;
   logout: Action<Auth>;
   login: Thunk<Auth, { email: string; password: string }>;
+  renew: Thunk<Auth>;
 }
 
 export const auth: Auth = {
@@ -38,5 +40,12 @@ export const auth: Auth = {
     cookie.set("token", token, { expires: 7 });
     const tokenPayload: TokenPayload = decode(token);
     actions.add(tokenPayload);
+  }),
+  renew: thunk(async actions => {
+    const res = await axios.post(API.RENEW, null, getAxiosConfig());
+    const { token } = res.data;
+    cookie.set("token", token, { expires: 7 });
+    const tokenPayload: TokenPayload = decode(token);
+    actions.add(tokenPayload);
   })
 };

+ 0 - 50
client/with-redux-store.js

@@ -1,50 +0,0 @@
-/* eslint-disable */
-import React from "react";
-import initializeStore from "./redux-store";
-
-const isServer = typeof window === "undefined";
-const __NEXT_REDUX_STORE__ = "__NEXT_REDUX_STORE__";
-
-function getOrCreateStore(initialState) {
-  // Always make a new store if server, otherwise state is shared between requests
-  if (isServer) {
-    return initializeStore(initialState);
-  }
-
-  // Create store if unavailable on the client and set it on the window object
-  if (!window[__NEXT_REDUX_STORE__]) {
-    window[__NEXT_REDUX_STORE__] = initializeStore(initialState);
-  }
-  return window[__NEXT_REDUX_STORE__];
-}
-
-export default App =>
-  class AppWithRedux extends React.Component {
-    static async getInitialProps(appContext) {
-      // Get or Create the store with `undefined` as initialState
-      // This allows you to set a custom default initialState
-      const reduxStore = getOrCreateStore();
-
-      // Provide the store to getInitialProps of pages
-      appContext.ctx.reduxStore = reduxStore;
-
-      let appProps = {};
-      if (typeof App.getInitialProps === "function") {
-        appProps = await App.getInitialProps(appContext);
-      }
-
-      return {
-        ...appProps,
-        initialReduxState: reduxStore.getState()
-      };
-    }
-
-    constructor(props) {
-      super(props);
-      this.reduxStore = getOrCreateStore(props.initialReduxState);
-    }
-
-    render() {
-      return <App {...this.props} reduxStore={this.reduxStore} />;
-    }
-  };

BIN
dump.rdb