poeti8 hace 6 años
padre
commit
cf537629f2
Se han modificado 84 ficheros con 767 adiciones y 3614 borrados
  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 { Box, BoxProps } from "reflexbox/styled-components";
+import styled, { css } from "styled-components";
+import { ifProp } from "styled-tools";
 
 
 interface Props extends BoxProps {
 interface Props extends BoxProps {
   href?: string;
   href?: string;
   title?: string;
   title?: string;
   target?: string;
   target?: string;
   rel?: string;
   rel?: string;
+  forButton?: boolean;
 }
 }
 const ALink = styled(Box).attrs({
 const ALink = styled(Box).attrs({
   as: "a"
   as: "a"
@@ -16,13 +18,19 @@ const ALink = styled(Box).attrs({
   text-decoration: none;
   text-decoration: none;
   transition: all 0.2s ease-out;
   transition: all 0.2s ease-out;
 
 
-  :hover {
-    border-bottom-color: #2196f3;
-  }
+  ${ifProp(
+    { forButton: false },
+    css`
+      :hover {
+        border-bottom-color: #2196f3;
+      }
+    `
+  )}
 `;
 `;
 
 
 ALink.defaultProps = {
 ALink.defaultProps = {
-  pb: "1px"
+  pb: "1px",
+  forButton: false
 };
 };
 
 
 export default ALink;
 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";
 import { spin } from "../helpers/animations";
 
 
 interface Props extends BoxProps {
 interface Props extends BoxProps {
-  color?: "purple" | "gray" | "blue";
+  color?: "purple" | "gray" | "blue" | "red";
   disabled?: boolean;
   disabled?: boolean;
   icon?: string; // TODO: better typing
   icon?: string; // TODO: better typing
   isRound?: boolean;
   isRound?: boolean;
@@ -27,16 +27,19 @@ const StyledButton = styled(Flex)<Props>`
   word-break: keep-all;
   word-break: keep-all;
   color: ${switchProp(prop("color", "blue"), {
   color: ${switchProp(prop("color", "blue"), {
     blue: "white",
     blue: "white",
+    red: "white",
     purple: "white",
     purple: "white",
     gray: "#444"
     gray: "#444"
   })};
   })};
   background: ${switchProp(prop("color", "blue"), {
   background: ${switchProp(prop("color", "blue"), {
     blue: "linear-gradient(to right, #42a5f5, #2979ff)",
     blue: "linear-gradient(to right, #42a5f5, #2979ff)",
+    red: "linear-gradient(to right, #ee3b3b, #e11c1c)",
     purple: "linear-gradient(to right, #7e57c2, #6200ea)",
     purple: "linear-gradient(to right, #7e57c2, #6200ea)",
     gray: "linear-gradient(to right, #e0e0e0, #bdbdbd)"
     gray: "linear-gradient(to right, #e0e0e0, #bdbdbd)"
   })};
   })};
   box-shadow: ${switchProp(prop("color", "blue"), {
   box-shadow: ${switchProp(prop("color", "blue"), {
     blue: "0 5px 6px rgba(66, 165, 245, 0.5)",
     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)",
     purple: "0 5px 6px rgba(81, 45, 168, 0.5)",
     gray: "0 5px 6px rgba(160, 160, 160, 0.5)"
     gray: "0 5px 6px rgba(160, 160, 160, 0.5)"
   })};
   })};
@@ -51,26 +54,12 @@ const StyledButton = styled(Flex)<Props>`
     outline: none;
     outline: none;
     box-shadow: ${switchProp(prop("color", "blue"), {
     box-shadow: ${switchProp(prop("color", "blue"), {
       blue: "0 6px 15px rgba(66, 165, 245, 0.5)",
       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)",
       purple: "0 6px 15px rgba(81, 45, 168, 0.5)",
       gray: "0 6px 15px rgba(160, 160, 160, 0.5)"
       gray: "0 6px 15px rgba(160, 160, 160, 0.5)"
     })};
     })};
     transform: translateY(-2px) scale(1.02, 1.02);
     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)`
 const Icon = styled(SVG)`
@@ -162,7 +151,7 @@ export const NavButton = styled(Flex)<NavButtonProps>`
   ${ifProp(
   ${ifProp(
     "disabled",
     "disabled",
     css`
     css`
-      background-color: #f5f5f5;
+      background-color: #f6f6f6;
       box-shadow: 0 0px 5px rgba(150, 150, 150, 0.1);
       box-shadow: 0 0px 5px rgba(150, 150, 150, 0.1);
       cursor: default;
       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 {
 import {
   AreaChart,
   AreaChart,
   Area,
   Area,
@@ -10,10 +10,8 @@ import {
   YAxis,
   YAxis,
   CartesianGrid,
   CartesianGrid,
   ResponsiveContainer,
   ResponsiveContainer,
-  Tooltip,
-} from 'recharts';
-
-import withTitle from './withTitle';
+  Tooltip
+} from "recharts";
 
 
 interface Props {
 interface Props {
   data: number[];
   data: number[];
@@ -24,22 +22,22 @@ const ChartArea: FC<Props> = ({ data: rawData, period }) => {
   const now = new Date();
   const now = new Date();
   const getDate = index => {
   const getDate = index => {
     switch (period) {
     switch (period) {
-      case 'allTime':
+      case "allTime":
         return formatDate(
         return formatDate(
           subMonths(now, rawData.length - index - 1),
           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:
       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) => ({
   const data = rawData.map((view, index) => ({
     name: getDate(index),
     name: getDate(index),
-    views: view,
+    views: view
   }));
   }));
 
 
   return (
   return (
@@ -53,7 +51,7 @@ const ChartArea: FC<Props> = ({ data: rawData, period }) => {
           top: 16,
           top: 16,
           right: 0,
           right: 0,
           left: 0,
           left: 0,
-          bottom: 16,
+          bottom: 16
         }}
         }}
       >
       >
         <defs>
         <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 {
 import {
   BarChart,
   BarChart,
   Bar,
   Bar,
@@ -6,10 +6,8 @@ import {
   YAxis,
   YAxis,
   CartesianGrid,
   CartesianGrid,
   Tooltip,
   Tooltip,
-  ResponsiveContainer,
-} from 'recharts';
-
-import withTitle from './withTitle';
+  ResponsiveContainer
+} from "recharts";
 
 
 interface Props {
 interface Props {
   data: any[]; // TODO: types
   data: any[]; // TODO: types
@@ -27,7 +25,7 @@ const ChartBar: FC<Props> = ({ data }) => (
         top: 0,
         top: 0,
         right: 0,
         right: 0,
         left: 24,
         left: 24,
-        bottom: 0,
+        bottom: 0
       }}
       }}
     >
     >
       <XAxis type="number" dataKey="value" />
       <XAxis type="number" dataKey="value" />
@@ -39,4 +37,4 @@ const ChartBar: FC<Props> = ({ data }) => (
   </ResponsiveContainer>
   </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 {
 interface Props {
   data: any[]; // TODO: types
   data: any[]; // TODO: types
 }
 }
 
 
-const renderCustomLabel = ({ name }) => name;
-
 const ChartPie: FC<Props> = ({ data }) => (
 const ChartPie: FC<Props> = ({ data }) => (
   <ResponsiveContainer
   <ResponsiveContainer
     width="100%"
     width="100%"
@@ -18,7 +15,7 @@ const ChartPie: FC<Props> = ({ data }) => (
         top: window.innerWidth < 468 ? 56 : 0,
         top: window.innerWidth < 468 ? 56 : 0,
         right: window.innerWidth < 468 ? 56 : 0,
         right: window.innerWidth < 468 ? 56 : 0,
         bottom: window.innerWidth < 468 ? 56 : 0,
         bottom: window.innerWidth < 468 ? 56 : 0,
-        left: window.innerWidth < 468 ? 56 : 0,
+        left: window.innerWidth < 468 ? 56 : 0
       }}
       }}
     >
     >
       <Pie
       <Pie
@@ -26,11 +23,11 @@ const ChartPie: FC<Props> = ({ data }) => (
         dataKey="value"
         dataKey="value"
         innerRadius={window.innerWidth < 468 ? 20 : 80}
         innerRadius={window.innerWidth < 468 ? 20 : 80}
         fill="#B39DDB"
         fill="#B39DDB"
-        label={renderCustomLabel}
+        label={({ name }) => name}
       />
       />
       <Tooltip />
       <Tooltip />
     </PieChart>
     </PieChart>
   </ResponsiveContainer>
   </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 { ifProp } from "styled-tools";
 import { Flex, BoxProps } from "reflexbox/styled-components";
 import { Flex, BoxProps } from "reflexbox/styled-components";
 
 
-import Text from "./Text";
+import Text, { Span } from "./Text";
 
 
 interface InputProps {
 interface InputProps {
   checked: boolean;
   checked: boolean;
@@ -82,9 +82,9 @@ const Checkbox: FC<Props> = ({
     >
     >
       <Input name={name} id={id} checked={checked} />
       <Input name={name} id={id} checked={checked} />
       <Box checked={checked} width={width} height={height} />
       <Box checked={checked} width={width} height={height} />
-      <Text as="span" ml={12} color="#555">
+      <Span ml={12} color="#555">
         {label}
         {label}
-      </Text>
+      </Span>
     </Flex>
     </Flex>
   );
   );
 };
 };

+ 3 - 1
client/components/Divider.tsx

@@ -1,12 +1,14 @@
 import { Flex } from "reflexbox/styled-components";
 import { Flex } from "reflexbox/styled-components";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
+import { Colors } from "../consts";
+
 const Divider = styled(Flex).attrs({ as: "hr" })`
 const Divider = styled(Flex).attrs({ as: "hr" })`
   width: 100%;
   width: 100%;
   height: 1px;
   height: 1px;
   outline: none;
   outline: none;
   border: none;
   border: none;
-  background-color: #e3e3e3;
+  background-color: ${Colors.Divider};
 `;
 `;
 
 
 export default 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`
 const Title = styled.h3`
   font-size: 28px;
   font-size: 28px;
@@ -38,7 +29,7 @@ const Button = styled.button`
   justify-content: center;
   justify-content: center;
   margin: 0 16px;
   margin: 0 16px;
   padding: 12px 28px;
   padding: 12px 28px;
-  font-family: 'Nunito', sans-serif;
+  font-family: "Nunito", sans-serif;
   background-color: #eee;
   background-color: #eee;
   border: 1px solid #aaa;
   border: 1px solid #aaa;
   font-size: 14px;
   font-size: 14px;
@@ -89,7 +80,7 @@ const Icon = styled(SVG)`
     width: 18px;
     width: 18px;
     height: 18px;
     height: 18px;
     margin-right: 16px;
     margin-right: 16px;
-    fill: ${props => props.color || '#333'};
+    fill: ${props => props.color || "#333"};
 
 
     @media only screen and (max-width: 768px) {
     @media only screen and (max-width: 768px) {
       width: 13px;
       width: 13px;
@@ -100,14 +91,22 @@ const Icon = styled(SVG)`
 `;
 `;
 
 
 const Extensions = () => (
 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
     <Flex
       width={1200}
       width={1200}
       maxWidth="100%"
       maxWidth="100%"
       flex="1 1 auto"
       flex="1 1 auto"
       justifyContent="center"
       justifyContent="center"
-      flexWrap={['wrap', 'wrap', 'nowrap']}
+      flexWrap={["wrap", "wrap", "nowrap"]}
     >
     >
       <Link
       <Link
         href="https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd"
         href="https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd"
@@ -130,7 +129,7 @@ const Extensions = () => (
         </FirefoxButton>
         </FirefoxButton>
       </Link>
       </Link>
     </Flex>
     </Flex>
-  </Section>
+  </ColCenterH>
 );
 );
 
 
 export default Extensions;
 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 = () => (
 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
     <Flex
       width={1200}
       width={1200}
       maxWidth="100%"
       maxWidth="100%"
       flex="1 1 auto"
       flex="1 1 auto"
       justifyContent="center"
       justifyContent="center"
-      flexWrap={['wrap', 'wrap', 'wrap', 'nowrap']}
+      flexWrap={["wrap", "wrap", "wrap", "nowrap"]}
     >
     >
       <FeaturesItem title="Managing links" icon="edit">
       <FeaturesItem title="Managing links" icon="edit">
         Create, protect and delete your links and monitor them with detailed
         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.
         Completely open source and free. You can host it on your own server.
       </FeaturesItem>
       </FeaturesItem>
     </Flex>
     </Flex>
-  </Section>
+  </ColCenterH>
 );
 );
 
 
 export default Features;
 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 {
 interface Props {
   title: string;
   title: string;
@@ -10,11 +10,11 @@ interface Props {
 }
 }
 
 
 const Block = styled(Flex).attrs({
 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;
   animation: ${fadeIn} 0.8s ease-out;
 
 
@@ -26,8 +26,8 @@ const Block = styled(Flex).attrs({
 const IconBox = styled(Flex).attrs({
 const IconBox = styled(Flex).attrs({
   width: [40, 40, 48],
   width: [40, 40, 48],
   height: [40, 40, 48],
   height: [40, 40, 48],
-  alignItems: 'center',
-  justifyContent: 'center',
+  alignItems: "center",
+  justifyContent: "center"
 })`
 })`
   border-radius: 100%;
   border-radius: 100%;
   box-sizing: border-box;
   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(() => {
   useEffect(() => {
     showRecaptcha();
     showRecaptcha();
   }, []);
   }, []);
 
 
   return (
   return (
-    <Wrapper isAuthenticated={isAuthenticated}>
+    <ColCenter
+      as="footer"
+      width={1}
+      backgroundColor="white"
+      p={isAuthenticated ? 2 : 24}
+    >
       {!isAuthenticated && <ReCaptcha />}
       {!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
           The Devs
-        </a>
-        .{' | '}
-        <a
+        </ALink>
+        .{" | "}
+        <ALink
           href="https://github.com/thedevs-network/kutt"
           href="https://github.com/thedevs-network/kutt"
           title="GitHub"
           title="GitHub"
           target="_blank"
           target="_blank"
         >
         >
           GitHub
           GitHub
-        </a>
-        {' | '}
-        <a href="/terms" title="Terms of Service">
+        </ALink>
+        {" | "}
+        <ALink href="/terms" title="Terms of Service">
           Terms of Service
           Terms of Service
-        </a>
-        {' | '}
-        <a href="/report" title="Report abuse">
+        </ALink>
+        {" | "}
+        <ALink href="/report" title="Report abuse">
           Report Abuse
           Report Abuse
-        </a>
+        </ALink>
         {process.env.CONTACT_EMAIL && (
         {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
               Contact us
-            </a>
+            </ALink>
           </>
           </>
         )}
         )}
         .
         .
       </Text>
       </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 && (
   const login = !isAuthenticated && (
     <Li>
     <Li>
       <Link href="/login">
       <Link href="/login">
-        <ALink href="/login" title="login / signup">
+        <ALink href="/login" title="login / signup" forButton>
           <Button>Login / Sign up</Button>
           <Button>Login / Sign up</Button>
         </ALink>
         </ALink>
       </Link>
       </Link>
@@ -68,7 +68,7 @@ const Header: FC = () => {
   const settings = isAuthenticated && (
   const settings = isAuthenticated && (
     <Li>
     <Li>
       <Link href="/settings">
       <Link href="/settings">
-        <ALink href="/settings" title="settings">
+        <ALink href="/settings" title="Settings" forButton>
           <Button>Settings</Button>
           <Button>Settings</Button>
         </ALink>
         </ALink>
       </Link>
       </Link>

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

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

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

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

+ 4 - 4
client/components/NeedToLogin.tsx

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

+ 4 - 3
client/components/PageLoading.tsx

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

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

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

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

@@ -9,7 +9,8 @@ import TextInput from "../TextInput";
 import { API } from "../../consts";
 import { API } from "../../consts";
 import { Button } from "../Button";
 import { Button } from "../Button";
 import Icon from "../Icon";
 import Icon from "../Icon";
-import Text from "../Text";
+import Text, { H2 } from "../Text";
+import { Col } from "../Layout";
 
 
 const SettingsPassword: FC = () => {
 const SettingsPassword: FC = () => {
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
@@ -39,10 +40,10 @@ const SettingsPassword: FC = () => {
   };
   };
 
 
   return (
   return (
-    <Flex flexDirection="column" alignItems="flex-start">
-      <Text as="h2" fontWeight={700} mb={4}>
+    <Col alignItems="flex-start">
+      <H2 mb={4} bold>
         Change password
         Change password
-      </Text>
+      </H2>
       <Text mb={4}>Enter a new password to change your current password.</Text>
       <Text mb={4}>Enter a new password to change your current password.</Text>
       <Flex as="form" onSubmit={onSubmit}>
       <Flex as="form" onSubmit={onSubmit}>
         <TextInput
         <TextInput
@@ -71,7 +72,7 @@ const SettingsPassword: FC = () => {
       <Text color={message.color} mt={3} fontSize={15}>
       <Text color={message.color} mt={3} fontSize={15}>
         {message.text}
         {message.text}
       </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 { Flex } from "reflexbox/styled-components";
 import React, { useState } from "react";
 import React, { useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-import QRCode from "qrcode.react";
 
 
 import { useStoreActions, useStoreState } from "../store";
 import { useStoreActions, useStoreState } from "../store";
 import { Col, RowCenterH, RowCenter } from "./Layout";
 import { Col, RowCenterH, RowCenter } from "./Layout";
@@ -12,11 +11,9 @@ import { Link } from "../store/links";
 import { useMessage } from "../hooks";
 import { useMessage } from "../hooks";
 import TextInput from "./TextInput";
 import TextInput from "./TextInput";
 import Animation from "./Animation";
 import Animation from "./Animation";
+import { Colors } from "../consts";
 import Checkbox from "./Checkbox";
 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";
 import Icon from "./Icon";
 
 
 const SubmitIconWrapper = styled.div`
 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;
   cursor: pointer;
-  transition: all 0.2s ease;
 
 
   :hover {
   :hover {
-    opacity: 0.5;
+    opacity: 0.8;
   }
   }
 `;
 `;
 
 
@@ -65,7 +62,7 @@ const Shortener = () => {
   const [message, setMessage] = useMessage(3000);
   const [message, setMessage] = useMessage(3000);
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
   const [qrModal, setQRModal] = 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, {
   const [formState, { raw, password, text, label }] = useFormState<Form>(null, {
     withIds: true,
     withIds: true,
     onChange(e, stateValues, nextStateValues) {
     onChange(e, stateValues, nextStateValues) {
@@ -79,7 +76,7 @@ const Shortener = () => {
   const onSubmit = async e => {
   const onSubmit = async e => {
     e.preventDefault();
     e.preventDefault();
     if (loading) return;
     if (loading) return;
-    setCopied("");
+    setCopied(false);
     setLoading(true);
     setLoading(true);
     try {
     try {
       const link = await submit(formState.values);
       const link = await submit(formState.values);
@@ -94,20 +91,21 @@ const Shortener = () => {
   };
   };
 
 
   const title = !link && (
   const title = !link && (
-    <Text as="h1" fontWeight={300}>
+    <H1 light>
       Kutt your links{" "}
       Kutt your links{" "}
-      <Text
-        as="span"
-        fontWeight={300}
-        style={{ borderBottom: "2px dotted #999" }}
-      >
+      <Span style={{ borderBottom: "2px dotted #999" }} light>
         shorter
         shorter
-      </Text>
+      </Span>
       .
       .
-    </Text>
+    </H1>
   );
   );
 
 
-  const onCopy = () => setCopied("Copied to clipboard.", "green");
+  const onCopy = () => {
+    setCopied(true);
+    setTimeout(() => {
+      setCopied(false);
+    }, 1500);
+  };
 
 
   const result = link && (
   const result = link && (
     <Animation
     <Animation
@@ -116,28 +114,42 @@ const Shortener = () => {
       duration="0.4s"
       duration="0.4s"
       style={{ position: "relative" }}
       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}>
       <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)}
           {removeProtocol(link.shortLink)}
         </ShortenedLink>
         </ShortenedLink>
       </CopyToClipboard>
       </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>
     </Animation>
   );
   );
 
 
@@ -165,13 +177,14 @@ const Shortener = () => {
           width={1}
           width={1}
           height={[72]}
           height={[72]}
           autoFocus
           autoFocus
+          data-lpignore
         />
         />
         <SubmitIconWrapper onClick={onSubmit}>
         <SubmitIconWrapper onClick={onSubmit}>
           <Icon
           <Icon
             name={loading ? "spinner" : "send"}
             name={loading ? "spinner" : "send"}
             size={28}
             size={28}
             fill={loading ? "none" : "#aaa"}
             fill={loading ? "none" : "#aaa"}
-            stroke={loading ? "#888" : "none"}
+            stroke={loading ? Colors.Spinner : "none"}
             mb={1}
             mb={1}
             mr={1}
             mr={1}
           />
           />
@@ -198,17 +211,12 @@ const Shortener = () => {
         checked={formState.values.showAdvanced}
         checked={formState.values.showAdvanced}
         label="Show advanced options"
         label="Show advanced options"
         mt={24}
         mt={24}
+        alignSelf="flex-start"
       />
       />
       {formState.values.showAdvanced && (
       {formState.values.showAdvanced && (
         <Flex mt={4}>
         <Flex mt={4}>
           <Col>
           <Col>
-            <Text
-              as="label"
-              {...label("customurl")}
-              fontWeight={700}
-              fontSize={15}
-              mb={2}
-            >
+            <Text as="label" {...label("customurl")} fontSize={15} mb={2} bold>
               {(domain || {}).customDomain ||
               {(domain || {}).customDomain ||
                 (typeof window !== "undefined" && window.location.hostname)}
                 (typeof window !== "undefined" && window.location.hostname)}
               /
               /
@@ -216,32 +224,28 @@ const Shortener = () => {
             <TextInput
             <TextInput
               {...text("customurl")}
               {...text("customurl")}
               placeholder="Custom address"
               placeholder="Custom address"
+              data-lpignore
               pl={24}
               pl={24}
               pr={24}
               pr={24}
               placeholderSize={[13, 14, 14, 14]}
               placeholderSize={[13, 14, 14, 14]}
               fontSize={[14, 15]}
               fontSize={[14, 15]}
-              height={48}
+              height={44}
               width={240}
               width={240}
             />
             />
           </Col>
           </Col>
           <Col ml={4}>
           <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:
               Password:
             </Text>
             </Text>
             <TextInput
             <TextInput
               {...password("password")}
               {...password("password")}
               placeholder="Password"
               placeholder="Password"
+              data-lpignore
               pl={24}
               pl={24}
               pr={24}
               pr={24}
               placeholderSize={[13, 14, 14, 14]}
               placeholderSize={[13, 14, 14, 14]}
               fontSize={[14, 15]}
               fontSize={[14, 15]}
-              height={48}
+              height={44}
               width={240}
               width={240}
             />
             />
           </Col>
           </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 styled, { css } from "styled-components";
 import { ifProp, prop } from "styled-tools";
 import { ifProp, prop } from "styled-tools";
 
 
+import { Colors } from "../consts";
+
 const Table = styled(Flex)<{ scrollWidth?: string }>`
 const Table = styled(Flex)<{ scrollWidth?: string }>`
   background-color: white;
   background-color: white;
   border-radius: 12px;
   border-radius: 12px;
-  box-shadow: 0 6px 15px hsla(200, 20%, 70%, 0.3);
+  box-shadow: 0 6px 15px ${Colors.TableShadow};
   text-align: center;
   text-align: center;
   overflow: scroll;
   overflow: scroll;
 
 
@@ -26,7 +28,7 @@ const Table = styled(Flex)<{ scrollWidth?: string }>`
   }
   }
 
 
   tr {
   tr {
-    border-bottom: 1px solid hsl(200, 14%, 94%);
+    border-bottom: 1px solid ${Colors.TableHeadBorder};
   }
   }
   tbody {
   tbody {
     border-bottom-right-radius: 12px;
     border-bottom-right-radius: 12px;
@@ -37,19 +39,19 @@ const Table = styled(Flex)<{ scrollWidth?: string }>`
     border: none;
     border: none;
   }
   }
   tbody tr:hover {
   tbody tr:hover {
-    background-color: hsl(200, 14%, 98%);
+    background-color: ${Colors.TableRowHover};
   }
   }
   thead {
   thead {
-    background-color: hsl(200, 14%, 96%);
+    background-color: ${Colors.TableHeadBg};
     border-top-right-radius: 12px;
     border-top-right-radius: 12px;
     border-top-left-radius: 12px;
     border-top-left-radius: 12px;
     font-weight: bold;
     font-weight: bold;
     tr {
     tr {
-      border-bottom: 1px solid hsl(200, 14%, 90%);
+      border-bottom: 1px solid ${Colors.TableBorder};
     }
     }
   }
   }
   tfoot {
   tfoot {
-    background-color: hsl(200, 14%, 96%);
+    background-color: ${Colors.TableHeadBg};
     border-bottom-right-radius: 12px;
     border-bottom-right-radius: 12px;
     border-bottom-left-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 { 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 {
 interface Props {
-  weight?: 300 | 400 | 700;
   htmlFor?: string;
   htmlFor?: string;
+  light?: boolean;
+  normal?: boolean;
+  bold?: boolean;
 }
 }
 const Text = styled(Box)<Props>`
 const Text = styled(Box)<Props>`
+  font-weight: 400;
   ${ifNotProp(
   ${ifNotProp(
     "fontSize",
     "fontSize",
     css`
     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 = {
 Text.defaultProps = {
   as: "p",
   as: "p",
-  fontWeight: 400,
-  color: "hsl(200, 35%, 25%)"
+  color: Colors.Text
 };
 };
 
 
 export default 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 {
 export enum API {
   LOGIN = "/api/auth/login",
   LOGIN = "/api/auth/login",
   SIGNUP = "/api/auth/signup",
   SIGNUP = "/api/auth/signup",
+  RENEW = "/api/auth/renew",
   REPORT = "/api/url/report",
   REPORT = "/api/url/report",
   RESET_PASSWORD = "/api/auth/resetpassword",
   RESET_PASSWORD = "/api/auth/resetpassword",
   CHANGE_PASSWORD = "/api/auth/changepassword",
   CHANGE_PASSWORD = "/api/auth/changepassword",
@@ -10,5 +11,32 @@ export enum API {
   SETTINGS = "/api/auth/usersettings",
   SETTINGS = "/api/auth/usersettings",
   SUBMIT = "/api/url/submit",
   SUBMIT = "/api/url/submit",
   GET_LINKS = "/api/url/geturls",
   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 = () => {
 export const initGA = () => {
   ReactGA.initialize(process.env.GOOGLE_ANALYTICS);
   ReactGA.initialize(process.env.GOOGLE_ANALYTICS);
@@ -9,13 +9,13 @@ export const logPageView = () => {
   ReactGA.pageview(window.location.pathname);
   ReactGA.pageview(window.location.pathname);
 };
 };
 
 
-export const logEvent = (category = '', action = '') => {
+export const logEvent = (category = "", action = "") => {
   if (category && action) {
   if (category && action) {
     ReactGA.event({ category, action });
     ReactGA.event({ category, action });
   }
   }
 };
 };
 
 
-export const logException = (description = '', fatal = false) => {
+export const logException = (description = "", fatal = false) => {
   if (description) {
   if (description) {
     ReactGA.exception({ description, fatal });
     ReactGA.exception({ description, fatal });
   }
   }

+ 33 - 13
client/pages/_app.tsx

@@ -1,14 +1,17 @@
 import App, { AppContext } from "next/app";
 import App, { AppContext } from "next/app";
 import { StoreProvider } from "easy-peasy";
 import { StoreProvider } from "easy-peasy";
-import Head from "next/head";
 import Router from "next/router";
 import Router from "next/router";
-import React from "react";
-import { Provider } from "react-redux";
 import decode from "jwt-decode";
 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 { initializeStore } from "../store";
 import { TokenPayload } from "../types";
 import { TokenPayload } from "../types";
+import AppWrapper from "../components/AppWrapper";
+
+const isProd = process.env.NODE_ENV === "production";
 
 
 // TODO: types
 // TODO: types
 class MyApp extends App<any> {
 class MyApp extends App<any> {
@@ -39,27 +42,44 @@ class MyApp extends App<any> {
   }
   }
 
 
   componentDidMount() {
   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("routeChangeStart", () => loading.show());
-    Router.events.on("routeChangeComplete", () => loading.hide());
+    Router.events.on("routeChangeComplete", () => {
+      loading.hide();
+
+      if (isProd) {
+        logPageView();
+      }
+    });
     Router.events.on("routeChangeError", () => loading.hide());
     Router.events.on("routeChangeError", () => loading.hide());
   }
   }
 
 
   render() {
   render() {
-    const { Component, pageProps, reduxStore } = this.props;
+    const { Component, pageProps } = this.props;
+
     return (
     return (
       <>
       <>
         <Head>
         <Head>
           <title>Kutt.it | Modern Open Source URL shortener.</title>
           <title>Kutt.it | Modern Open Source URL shortener.</title>
         </Head>
         </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 React from "react";
 import Document, { Head, Main, NextScript } from "next/document";
 import Document, { Head, Main, NextScript } from "next/document";
 import { ServerStyleSheet } from "styled-components";
 import { ServerStyleSheet } from "styled-components";
+import { Colors } from "../consts";
 
 
 interface Props {
 interface Props {
   styleTags: any;
   styleTags: any;
@@ -79,10 +80,10 @@ class AppDocument extends Document<Props> {
         <body
         <body
           style={{
           style={{
             margin: 0,
             margin: 0,
-            backgroundColor: "hsl(206, 12%, 95%)",
+            backgroundColor: Colors.Bg,
             font: '16px/1.45 "Nunito", sans-serif',
             font: '16px/1.45 "Nunito", sans-serif',
             overflowX: "hidden",
             overflowX: "hidden",
-            color: "hsl(200, 35%, 25%)"
+            color: Colors.Text
           }}
           }}
         >
         >
           <Main />
           <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 { 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 Footer from "../components/Footer";
-import Text from "../components/Text";
 import ALink from "../components/ALink";
 import ALink from "../components/ALink";
+import { Col } from "../components/Layout";
 
 
 const BannedPage = () => {
 const BannedPage = () => {
   return (
   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{" "}
           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
             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,{" "}
           If you noticed a malware/scam link shortened by Kutt,{" "}
           <Link href="/report">
           <Link href="/report">
             <ALink title="Send report">send us a report</ALink>
             <ALink title="Send report">send us a report</ALink>
           </Link>
           </Link>
           .
           .
-        </Text>
-      </Flex>
+        </H4>
+      </Col>
       <Footer />
       <Footer />
-    </BodyWrapper>
+    </AppWrapper>
   );
   );
 };
 };
 
 

+ 3 - 3
client/pages/index.tsx

@@ -1,9 +1,9 @@
 import React from "react";
 import React from "react";
 
 
 import NeedToLogin from "../components/NeedToLogin";
 import NeedToLogin from "../components/NeedToLogin";
-import BodyWrapper from "../components/BodyWrapper";
 import Extensions from "../components/Extensions";
 import Extensions from "../components/Extensions";
 import LinksTable from "../components/LinksTable";
 import LinksTable from "../components/LinksTable";
+import AppWrapper from "../components/AppWrapper";
 import Shortener from "../components/Shortener";
 import Shortener from "../components/Shortener";
 import Features from "../components/Features";
 import Features from "../components/Features";
 import Footer from "../components/Footer";
 import Footer from "../components/Footer";
@@ -13,14 +13,14 @@ const Homepage = () => {
   const isAuthenticated = useStoreState(s => s.auth.isAuthenticated);
   const isAuthenticated = useStoreState(s => s.auth.isAuthenticated);
 
 
   return (
   return (
-    <BodyWrapper>
+    <AppWrapper>
       <Shortener />
       <Shortener />
       {!isAuthenticated && <NeedToLogin />}
       {!isAuthenticated && <NeedToLogin />}
       {isAuthenticated && <LinksTable />}
       {isAuthenticated && <LinksTable />}
       <Features />
       <Features />
       <Extensions />
       <Extensions />
       <Footer />
       <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 { Flex } from "reflexbox/styled-components";
 
 
 import { useStoreState, useStoreActions } from "../store";
 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 { fadeIn } from "../helpers/animations";
-import { API } from "../consts";
 import TextInput from "../components/TextInput";
 import TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
 import { Button } from "../components/Button";
-import Text from "../components/Text";
 import ALink from "../components/ALink";
 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({
 const LoginForm = styled(Flex).attrs({
   as: "form",
   as: "form",
@@ -95,29 +95,31 @@ const LoginPage = () => {
   }
   }
 
 
   return (
   return (
-    <BodyWrapper>
+    <AppWrapper>
       <ColCenterV flex="0 0 auto" mt={24} mb={64}>
       <ColCenterV flex="0 0 auto" mt={24} mb={64}>
         {verifying ? (
         {verifying ? (
-          <Text fontWeight={300} as="h2" textAlign="center">
+          <H2 textAlign="center" light>
             A verification email has been sent to{" "}
             A verification email has been sent to{" "}
             <Email>{formState.values.email}</Email>.
             <Email>{formState.values.email}</Email>.
-          </Text>
+          </H2>
         ) : (
         ) : (
           <LoginForm id="login-form" onSubmit={onSubmit("login")}>
           <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:
               Email address:
             </Text>
             </Text>
             <TextInput
             <TextInput
               {...email("email")}
               {...email("email")}
+              placeholder="Email address..."
               height={[56, 64, 72]}
               height={[56, 64, 72]}
               mb={[24, 32, 36]}
               mb={[24, 32, 36]}
               autoFocus
               autoFocus
             />
             />
-            <Text {...label("password")} as="label" fontWeight={700} mb={2}>
+            <Text {...label("password")} as="label" mb={2} bold>
               Password (min chars: 8):
               Password (min chars: 8):
             </Text>
             </Text>
             <TextInput
             <TextInput
               {...password("password")}
               {...password("password")}
+              placeholder="Password..."
               height={[56, 64, 72]}
               height={[56, 64, 72]}
               mb={[24, 32, 36]}
               mb={[24, 32, 36]}
             />
             />
@@ -153,13 +155,13 @@ const LoginPage = () => {
                 Forgot your password?
                 Forgot your password?
               </ALink>
               </ALink>
             </Link>
             </Link>
-            <Text color="red" fontWeight={400} mt={1}>
+            <Text color="red" mt={1} normal>
               {error}
               {error}
             </Text>
             </Text>
           </LoginForm>
           </LoginForm>
         )}
         )}
       </ColCenterV>
       </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 { useFormState } from "react-use-form-state";
 import { Flex } from "reflexbox/styled-components";
 import { Flex } from "reflexbox/styled-components";
 
 
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
 import TextInput from "../components/TextInput";
 import TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
 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 { useMessage } from "../hooks";
+import { API } from "../consts";
+import { Col } from "../components/Layout";
 
 
 const ReportPage = () => {
 const ReportPage = () => {
   const [formState, { text }] = useFormState<{ url: string }>();
   const [formState, { text }] = useFormState<{ url: string }>();
@@ -31,27 +32,20 @@ const ReportPage = () => {
   };
   };
 
 
   return (
   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
           Report abuse
-        </Text>
-        <Text as="p" mb={3}>
+        </H2>
+        <Text mb={3}>
           Report abuses, malware and phishing links to the below email address
           Report abuses, malware and phishing links to the below email address
           or use the form. We will take actions shortly.
           or use the form. We will take actions shortly.
         </Text>
         </Text>
-        <Text as="p" mb={4}>
+        <Text mb={4}>
           {(process.env.REPORT_EMAIL || "").replace("@", "[at]")}
           {(process.env.REPORT_EMAIL || "").replace("@", "[at]")}
         </Text>
         </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>
         </Text>
         <Flex
         <Flex
           as="form"
           as="form"
@@ -82,8 +76,8 @@ const ReportPage = () => {
         <Text fontSize={14} mt={3} color={message.color}>
         <Text fontSize={14} mt={3} color={message.color}>
           {message.text}
           {message.text}
         </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 axios from "axios";
 
 
 import { useStoreState, useStoreActions } from "../store";
 import { useStoreState, useStoreActions } from "../store";
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
 import TextInput from "../components/TextInput";
 import TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
 import { Button } from "../components/Button";
 import { TokenPayload } from "../types";
 import { TokenPayload } from "../types";
-import Text from "../components/Text";
+import Text, { H2 } from "../components/Text";
 import { useMessage } from "../hooks";
 import { useMessage } from "../hooks";
 import { API } from "../consts";
 import { API } from "../consts";
+import { Col } from "../components/Layout";
 
 
 interface Props {
 interface Props {
   token?: string;
   token?: string;
@@ -59,12 +60,12 @@ const ResetPassword: NextPage<Props> = ({ token }) => {
 
 
   // FIXME: make a container for width
   // FIXME: make a container for width
   return (
   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
           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
           If you forgot you password you can use the form below to get reset
           password link.
           password link.
         </Text>
         </Text>
@@ -94,11 +95,11 @@ const ResetPassword: NextPage<Props> = ({ token }) => {
             Reset password
             Reset password
           </Button>
           </Button>
         </Flex>
         </Flex>
-        <Text fontSize={14} fontWeight={400} color={message.color} mt={2}>
+        <Text fontSize={14} color={message.color} mt={2} normal>
           {message.text}
           {message.text}
         </Text>
         </Text>
-      </Flex>
-    </BodyWrapper>
+      </Col>
+    </AppWrapper>
   );
   );
 };
 };
 
 

+ 15 - 21
client/pages/settings.tsx

@@ -1,42 +1,36 @@
 import { Flex } from "reflexbox/styled-components";
 import { Flex } from "reflexbox/styled-components";
-import { NextPage } from "next";
 import React, { useEffect } from "react";
 import React, { useEffect } from "react";
+import { NextPage } from "next";
 
 
 import SettingsPassword from "../components/Settings/SettingsPassword";
 import SettingsPassword from "../components/Settings/SettingsPassword";
 import SettingsDomain from "../components/Settings/SettingsDomain";
 import SettingsDomain from "../components/Settings/SettingsDomain";
 import SettingsBan from "../components/Settings/SettingsBan";
 import SettingsBan from "../components/Settings/SettingsBan";
 import SettingsApi from "../components/Settings/SettingsApi";
 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 Divider from "../components/Divider";
 import Footer from "../components/Footer";
 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 { email, isAdmin } = useStoreState(s => s.auth);
   const getSettings = useStoreActions(s => s.settings.getSettings);
   const getSettings = useStoreActions(s => s.settings.getSettings);
 
 
   useEffect(() => {
   useEffect(() => {
     getSettings();
     getSettings();
-  }, []);
+  }, [false]);
 
 
   return (
   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,{" "}
           Welcome,{" "}
-          <Text as="span" pb="2px" style={{ borderBottom: "2px dotted #999" }}>
+          <Span pb="2px" style={{ borderBottom: "2px dotted #999" }}>
             {email}
             {email}
-          </Text>
+          </Span>
           .
           .
-        </Text>
+        </H1>
         <Divider my={[4, 48]} />
         <Divider my={[4, 48]} />
         {isAdmin && (
         {isAdmin && (
           <>
           <>
@@ -49,9 +43,9 @@ const SettingsPage: NextPage = () => {
         <SettingsPassword />
         <SettingsPassword />
         <Divider my={[12, 24]} />
         <Divider my={[12, 24]} />
         <SettingsApi />
         <SettingsApi />
-      </Flex>
+      </Col>
       <Footer />
       <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 { 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 {
 interface Props {
   domain?: string;
   domain?: string;
   id?: 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 }) => {
 StatsPage.getInitialProps = ({ query }) => {
   return Promise.resolve(query);
   return Promise.resolve(query);

+ 6 - 10
client/pages/terms.tsx

@@ -1,17 +1,13 @@
 import React from "react";
 import React from "react";
 import { Flex } from "reflexbox/styled-components";
 import { Flex } from "reflexbox/styled-components";
 
 
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
+import { Col } from "../components/Layout";
 
 
 const TermsPage = () => (
 const TermsPage = () => (
-  <BodyWrapper>
+  <AppWrapper>
     {/* TODO: better container */}
     {/* 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>
       <h3>Kutt Terms of Service</h3>
       <p>
       <p>
         By accessing the website at{" "}
         By accessing the website at{" "}
@@ -51,8 +47,8 @@ const TermsPage = () => (
         without notice. By using this website you are agreeing to be bound by
         without notice. By using this website you are agreeing to be bound by
         the then current version of these terms of service.
         the then current version of these terms of service.
       </p>
       </p>
-    </Flex>
-  </BodyWrapper>
+    </Col>
+  </AppWrapper>
 );
 );
 
 
 export default TermsPage;
 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 { Flex } from "reflexbox/styled-components";
 import { NextPage } from "next";
 import { NextPage } from "next";
 
 
-import BodyWrapper from "../components/BodyWrapper";
+import AppWrapper from "../components/AppWrapper";
 import Footer from "../components/Footer";
 import Footer from "../components/Footer";
-import Text from "../components/Text";
+import { H2, H4 } from "../components/Text";
+import { Col } from "../components/Layout";
 
 
 interface Props {
 interface Props {
   linkTarget?: string;
   linkTarget?: string;
@@ -13,25 +14,23 @@ interface Props {
 
 
 const UrlInfoPage: NextPage<Props> = ({ linkTarget }) => {
 const UrlInfoPage: NextPage<Props> = ({ linkTarget }) => {
   return (
   return (
-    <BodyWrapper>
+    <AppWrapper>
       {!linkTarget ? (
       {!linkTarget ? (
-        <Text as="h2" my={4} fontWeight={300}>
+        <H2 my={4} light>
           404 | Link could not be found.
           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:
               Target:
-            </Text>
-            <Text as="h4" fontWeight={700}>
-              {linkTarget}
-            </Text>
-          </Flex>
+            </H2>
+            <H4 bold>{linkTarget}</H4>
+          </Col>
           <Footer />
           <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 React, { useState } from "react";
 import { NextPage } from "next";
 import { NextPage } from "next";
-import styled from "styled-components";
 import axios from "axios";
 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 TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
 import { Button } from "../components/Button";
-import Text from "../components/Text";
+import Text, { H2 } from "../components/Text";
+import { Col } from "../components/Layout";
 
 
 interface Props {
 interface Props {
   protectedLink?: string;
   protectedLink?: string;
@@ -43,19 +43,17 @@ const UrlPasswordPage: NextPage<Props> = ({ protectedLink }) => {
   };
   };
 
 
   return (
   return (
-    <BodyWrapper>
+    <AppWrapper>
       {!protectedLink ? (
       {!protectedLink ? (
-        <Text as="h2" my={4} fontWeight={300}>
+        <H2 my={4} light>
           404 | Link could not be found.
           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
             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
           <Flex
             as="form"
             as="form"
             alignItems="center"
             alignItems="center"
@@ -79,12 +77,12 @@ const UrlPasswordPage: NextPage<Props> = ({ protectedLink }) => {
               Go
               Go
             </Button>
             </Button>
           </Flex>
           </Flex>
-          <Text fontSize={14} color="red" fontWeight={400} mt={3}>
+          <Text fontSize={14} color="red" mt={3} normal>
             {error}
             {error}
           </Text>
           </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 React, { useEffect } from "react";
-import Router from "next/router";
 import styled from "styled-components";
 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 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 { Button } from "../components/Button";
-import { NextPage } from "next";
-import { TokenPayload } from "../types";
 import { useStoreActions } from "../store";
 import { useStoreActions } from "../store";
+import { TokenPayload } from "../types";
+import { NextPage } from "next";
+import { Col } from "../components/Layout";
 
 
 interface Props {
 interface Props {
   token?: string;
   token?: string;
@@ -58,23 +59,26 @@ const Verify: NextPage<Props> = ({ token }) => {
     Router.push("/");
     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 }) => {
 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 { TokenPayload } from "../types";
 import { API } from "../consts";
 import { API } from "../consts";
+import { getAxiosConfig } from "../utils";
 
 
 export interface Auth {
 export interface Auth {
   domain?: string;
   domain?: string;
@@ -14,6 +15,7 @@ export interface Auth {
   add: Action<Auth, TokenPayload>;
   add: Action<Auth, TokenPayload>;
   logout: Action<Auth>;
   logout: Action<Auth>;
   login: Thunk<Auth, { email: string; password: string }>;
   login: Thunk<Auth, { email: string; password: string }>;
+  renew: Thunk<Auth>;
 }
 }
 
 
 export const auth: Auth = {
 export const auth: Auth = {
@@ -38,5 +40,12 @@ export const auth: Auth = {
     cookie.set("token", token, { expires: 7 });
     cookie.set("token", token, { expires: 7 });
     const tokenPayload: TokenPayload = decode(token);
     const tokenPayload: TokenPayload = decode(token);
     actions.add(tokenPayload);
     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