* refactor: (wip) * refactor: finish settings, add icons and stuff * 🐬 * 🐬 * 2.2.0pull/266/head
@ -1,4 +1,12 @@ | |||
{ | |||
"presets": ["next/babel", "@zeit/next-typescript/babel"], | |||
"plugins": [["styled-components", { "ssr": true, "displayName": true, "preprocess": false }]] | |||
"plugins": [ | |||
[ | |||
"styled-components", | |||
{ "ssr": true, "displayName": true, "preprocess": false } | |||
], | |||
"inline-react-svg", | |||
"@babel/plugin-proposal-optional-chaining", | |||
"@babel/plugin-proposal-nullish-coalescing-operator" | |||
] | |||
} |
@ -0,0 +1,8 @@ | |||
{ | |||
"useTabs": false, | |||
"tabWidth": 2, | |||
"trailingComma": "none", | |||
"singleQuote": false, | |||
"printWidth": 80, | |||
"endOfLine": "lf" | |||
} |
@ -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)); | |||
}); | |||
}); | |||
}); |
@ -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)); | |||
}); | |||
}); | |||
}); |
@ -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)); | |||
}); | |||
}); | |||
}); |
@ -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'; |
@ -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()); | |||
} | |||
}; |
@ -1,3 +0,0 @@ | |||
export * from './url'; | |||
export * from './settings'; | |||
export * from './auth'; |
@ -1,85 +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 = () => async dispatch => { | |||
try { | |||
await axios.delete('/api/url/customdomain', { | |||
headers: { Authorization: cookie.get('token') }, | |||
}); | |||
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); | |||
} | |||
}; |
@ -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)); | |||
} | |||
}; |
@ -0,0 +1,36 @@ | |||
import { Box, BoxProps } from "reflexbox/styled-components"; | |||
import styled, { css } from "styled-components"; | |||
import { ifProp } from "styled-tools"; | |||
interface Props extends BoxProps { | |||
href?: string; | |||
title?: string; | |||
target?: string; | |||
rel?: string; | |||
forButton?: boolean; | |||
} | |||
const ALink = styled(Box).attrs({ | |||
as: "a" | |||
})<Props>` | |||
cursor: pointer; | |||
color: #2196f3; | |||
border-bottom: 1px dotted transparent; | |||
text-decoration: none; | |||
transition: all 0.2s ease-out; | |||
${ifProp( | |||
{ forButton: false }, | |||
css` | |||
:hover { | |||
border-bottom-color: #2196f3; | |||
} | |||
` | |||
)} | |||
`; | |||
ALink.defaultProps = { | |||
pb: "1px", | |||
forButton: false | |||
}; | |||
export default ALink; |
@ -0,0 +1,17 @@ | |||
import { fadeInVertical } from "../helpers/animations"; | |||
import { Flex } from "reflexbox/styled-components"; | |||
import styled from "styled-components"; | |||
import { prop } from "styled-tools"; | |||
import { FC } from "react"; | |||
interface Props extends React.ComponentProps<typeof Flex> { | |||
offset: string; | |||
duration?: string; | |||
} | |||
const Animation: FC<Props> = styled(Flex)<Props>` | |||
animation: ${props => fadeInVertical(props.offset)} | |||
${prop("duration", "0.3s")} ease-out; | |||
`; | |||
export default Animation; |
@ -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; |
@ -1,97 +0,0 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import { bindActionCreators } from 'redux'; | |||
import { connect } from 'react-redux'; | |||
import styled from 'styled-components'; | |||
import cookie from 'js-cookie'; | |||
import Header from '../Header'; | |||
import PageLoading from '../PageLoading'; | |||
import { renewAuthUser, hidePageLoading } from '../../actions'; | |||
import { initGA, logPageView } from '../../helpers/analytics'; | |||
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 ContentWrapper = styled.div` | |||
min-height: 100vh; | |||
width: 100%; | |||
flex: 0 0 auto; | |||
display: flex; | |||
align-items: center; | |||
flex-direction: column; | |||
box-sizing: border-box; | |||
`; | |||
class BodyWrapper extends React.Component { | |||
componentDidMount() { | |||
if (process.env.GOOGLE_ANALYTICS) { | |||
if (!window.GA_INITIALIZED) { | |||
initGA(); | |||
window.GA_INITIALIZED = true; | |||
} | |||
logPageView(); | |||
} | |||
const token = cookie.get('token'); | |||
this.props.hidePageLoading(); | |||
if (!token || this.props.norenew) return null; | |||
return this.props.renewAuthUser(token); | |||
} | |||
render() { | |||
const { children, pageLoading } = this.props; | |||
const content = pageLoading ? <PageLoading /> : children; | |||
return ( | |||
<Wrapper> | |||
<ContentWrapper> | |||
<Header /> | |||
{content} | |||
</ContentWrapper> | |||
</Wrapper> | |||
); | |||
} | |||
} | |||
BodyWrapper.propTypes = { | |||
children: PropTypes.node.isRequired, | |||
hidePageLoading: PropTypes.func.isRequired, | |||
norenew: PropTypes.bool, | |||
pageLoading: PropTypes.bool.isRequired, | |||
renewAuthUser: PropTypes.func.isRequired, | |||
}; | |||
BodyWrapper.defaultProps = { | |||
norenew: false, | |||
}; | |||
const mapStateToProps = ({ loading: { page: pageLoading } }) => ({ pageLoading }); | |||
const mapDispatchToProps = dispatch => ({ | |||
hidePageLoading: bindActionCreators(hidePageLoading, dispatch), | |||
renewAuthUser: bindActionCreators(renewAuthUser, dispatch), | |||
}); | |||
export default connect( | |||
mapStateToProps, | |||
mapDispatchToProps | |||
)(BodyWrapper); |
@ -1 +0,0 @@ | |||
export { default } from './BodyWrapper'; |
@ -0,0 +1,176 @@ | |||
import React, { FC } from "react"; | |||
import styled, { css } from "styled-components"; | |||
import { switchProp, prop, ifProp } from "styled-tools"; | |||
import { Flex, BoxProps } from "reflexbox/styled-components"; | |||
// TODO: another solution for inline SVG | |||
import SVG from "react-inlinesvg"; | |||
import { spin } from "../helpers/animations"; | |||
interface Props extends BoxProps { | |||
color?: "purple" | "gray" | "blue" | "red"; | |||
disabled?: boolean; | |||
icon?: string; // TODO: better typing | |||
isRound?: boolean; | |||
onClick?: any; // TODO: better typing | |||
type?: "button" | "submit" | "reset"; | |||
} | |||
const StyledButton = styled(Flex)<Props>` | |||
position: relative; | |||
align-items: center; | |||
justify-content: center; | |||
font-weight: normal; | |||
text-align: center; | |||
line-height: 1; | |||
word-break: keep-all; | |||
color: ${switchProp(prop("color", "blue"), { | |||
blue: "white", | |||
red: "white", | |||
purple: "white", | |||
gray: "#444" | |||
})}; | |||
background: ${switchProp(prop("color", "blue"), { | |||
blue: "linear-gradient(to right, #42a5f5, #2979ff)", | |||
red: "linear-gradient(to right, #ee3b3b, #e11c1c)", | |||
purple: "linear-gradient(to right, #7e57c2, #6200ea)", | |||
gray: "linear-gradient(to right, #e0e0e0, #bdbdbd)" | |||
})}; | |||
box-shadow: ${switchProp(prop("color", "blue"), { | |||
blue: "0 5px 6px rgba(66, 165, 245, 0.5)", | |||
red: "0 5px 6px rgba(168, 45, 45, 0.5)", | |||
purple: "0 5px 6px rgba(81, 45, 168, 0.5)", | |||
gray: "0 5px 6px rgba(160, 160, 160, 0.5)" | |||
})}; | |||
border: none; | |||
border-radius: 100px; | |||
transition: all 0.4s ease-out; | |||
cursor: pointer; | |||
overflow: hidden; | |||
:hover, | |||
:focus { | |||
outline: none; | |||
box-shadow: ${switchProp(prop("color", "blue"), { | |||
blue: "0 6px 15px rgba(66, 165, 245, 0.5)", | |||
red: "0 6px 15px rgba(168, 45, 45, 0.5)", | |||
purple: "0 6px 15px rgba(81, 45, 168, 0.5)", | |||
gray: "0 6px 15px rgba(160, 160, 160, 0.5)" | |||
})}; | |||
transform: translateY(-2px) scale(1.02, 1.02); | |||
} | |||
`; | |||
const Icon = styled(SVG)` | |||
svg { | |||
width: 16px; | |||
height: 16px; | |||
margin-right: 12px; | |||
stroke: ${ifProp({ color: "gray" }, "#444", "#fff")}; | |||
${ifProp( | |||
{ icon: "loader" }, | |||
css` | |||
width: 20px; | |||
height: 20px; | |||
margin: 0; | |||
animation: ${spin} 1s linear infinite; | |||
` | |||
)} | |||
${ifProp( | |||
"isRound", | |||
css` | |||
width: 15px; | |||
height: 15px; | |||
margin: 0; | |||
` | |||
)} | |||
@media only screen and (max-width: 768px) { | |||
width: 12px; | |||
height: 12px; | |||
margin-right: 6px; | |||
} | |||
} | |||
`; | |||
export const Button: FC<Props> = props => { | |||
const SVGIcon = props.icon ? ( | |||
<Icon | |||
icon={props.icon} | |||
isRound={props.isRound} | |||
color={props.color} | |||
src={`/images/${props.icon}.svg`} | |||
/> | |||
) : ( | |||
"" | |||
); | |||
return ( | |||
<StyledButton {...props}> | |||
{SVGIcon} | |||
{props.icon !== "loader" && props.children} | |||
</StyledButton> | |||
); | |||
}; | |||
Button.defaultProps = { | |||
as: "button", | |||
width: "auto", | |||
flex: "0 0 auto", | |||
height: [32, 40], | |||
py: 0, | |||
px: [24, 32], | |||
fontSize: [12, 13], | |||
color: "blue", | |||
icon: "", | |||
isRound: false | |||
}; | |||
interface NavButtonProps extends BoxProps { | |||
disabled?: boolean; | |||
onClick?: any; // TODO: better typing | |||
type?: "button" | "submit" | "reset"; | |||
} | |||
export const NavButton = styled(Flex)<NavButtonProps>` | |||
display: flex; | |||
border: none; | |||
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; | |||
:hover { | |||
transform: translateY(-2px); | |||
} | |||
${ifProp( | |||
"disabled", | |||
css` | |||
background-color: #f6f6f6; | |||
box-shadow: 0 0px 5px rgba(150, 150, 150, 0.1); | |||
cursor: default; | |||
:hover { | |||
transform: none; | |||
} | |||
` | |||
)} | |||
`; | |||
NavButton.defaultProps = { | |||
as: "button", | |||
type: "button", | |||
flex: "0 0 auto", | |||
alignItems: "center", | |||
justifyContent: "center", | |||
width: "auto", | |||
height: [26, 28], | |||
py: 0, | |||
px: ["6px", "8px"], | |||
fontSize: [12] | |||
}; |
@ -1,155 +0,0 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import styled, { css } from 'styled-components'; | |||
import SVG from 'react-inlinesvg'; | |||
import { spin } from '../../helpers/animations'; | |||
const StyledButton = styled.button` | |||
position: relative; | |||
height: 40px; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
padding: 0px 32px; | |||
font-size: 13px; | |||
font-weight: normal; | |||
text-align: center; | |||
line-height: 1; | |||
word-break: keep-all; | |||
color: white; | |||
background: linear-gradient(to right, #42a5f5, #2979ff); | |||
box-shadow: 0 5px 6px rgba(66, 165, 245, 0.5); | |||
border: none; | |||
border-radius: 100px; | |||
transition: all 0.4s ease-out; | |||
cursor: pointer; | |||
overflow: hidden; | |||
:hover, | |||
:focus { | |||
outline: none; | |||
box-shadow: 0 6px 15px rgba(66, 165, 245, 0.5); | |||
transform: translateY(-2px) scale(1.02, 1.02); | |||
} | |||
a & { | |||
text-decoration: none; | |||
border: none; | |||
} | |||
@media only screen and (max-width: 448px) { | |||
height: 32px; | |||
padding: 0 24px; | |||
font-size: 12px; | |||
} | |||
${({ color }) => { | |||
if (color === 'purple') { | |||
return css` | |||
background: linear-gradient(to right, #7e57c2, #6200ea); | |||
box-shadow: 0 5px 6px rgba(81, 45, 168, 0.5); | |||
:focus, | |||
:hover { | |||
box-shadow: 0 6px 15px rgba(81, 45, 168, 0.5); | |||
} | |||
`; | |||
} | |||
if (color === 'gray') { | |||
return css` | |||
color: black; | |||
background: linear-gradient(to right, #e0e0e0, #bdbdbd); | |||
box-shadow: 0 5px 6px rgba(160, 160, 160, 0.5); | |||
:focus, | |||
:hover { | |||
box-shadow: 0 6px 15px rgba(160, 160, 160, 0.5); | |||
} | |||
`; | |||
} | |||
return null; | |||
}}; | |||
${({ big }) => | |||
big && | |||
css` | |||
height: 56px; | |||
@media only screen and (max-width: 448px) { | |||
height: 40px; | |||
} | |||
`}; | |||
`; | |||
const Icon = styled(SVG)` | |||
svg { | |||
width: 16px; | |||
height: 16px; | |||
margin-right: 12px; | |||
stroke: #fff; | |||
${({ type }) => | |||
type === 'loader' && | |||
css` | |||
width: 20px; | |||
height: 20px; | |||
margin: 0; | |||
animation: ${spin} 1s linear infinite; | |||
`}; | |||
${({ round }) => | |||
round && | |||
css` | |||
width: 15px; | |||
height: 15px; | |||
margin: 0; | |||
`}; | |||
${({ color }) => | |||
color === 'gray' && | |||
css` | |||
stroke: #444; | |||
`}; | |||
@media only screen and (max-width: 768px) { | |||
width: 12px; | |||
height: 12px; | |||
margin-right: 6px; | |||
} | |||
} | |||
`; | |||
const Button = props => { | |||
const SVGIcon = props.icon ? ( | |||
<Icon | |||
type={props.icon} | |||
round={props.round} | |||
color={props.color} | |||
src={`/images/${props.icon}.svg`} | |||
/> | |||
) : ( | |||
'' | |||
); | |||
return ( | |||
<StyledButton {...props}> | |||
{SVGIcon} | |||
{props.icon !== 'loader' && props.children} | |||
</StyledButton> | |||
); | |||
}; | |||
Button.propTypes = { | |||
children: PropTypes.node.isRequired, | |||
color: PropTypes.string, | |||
icon: PropTypes.string, | |||
round: PropTypes.bool, | |||
type: PropTypes.string, | |||
}; | |||
Button.defaultProps = { | |||
color: 'blue', | |||
icon: '', | |||
type: '', | |||
round: false, | |||
}; | |||
export default Button; |
@ -1 +0,0 @@ | |||
export { default } from './Button'; |
@ -0,0 +1,40 @@ | |||
import React, { FC } from "react"; | |||
import { | |||
BarChart, | |||
Bar, | |||
XAxis, | |||
YAxis, | |||
CartesianGrid, | |||
Tooltip, | |||
ResponsiveContainer | |||
} from "recharts"; | |||
interface Props { | |||
data: any[]; // TODO: types | |||
} | |||
const ChartBar: FC<Props> = ({ data }) => ( | |||
<ResponsiveContainer | |||
width="100%" | |||
height={window.innerWidth < 468 ? 240 : 320} | |||
> | |||
<BarChart | |||
data={data} | |||
layout="vertical" | |||
margin={{ | |||
top: 0, | |||
right: 0, | |||
left: 24, | |||
bottom: 0 | |||
}} | |||
> | |||
<XAxis type="number" dataKey="value" /> | |||
<YAxis type="category" dataKey="name" /> | |||
<CartesianGrid strokeDasharray="1 1" /> | |||
<Tooltip /> | |||
<Bar dataKey="value" fill="#B39DDB" /> | |||
</BarChart> | |||
</ResponsiveContainer> | |||
); | |||
export default ChartBar; |
@ -0,0 +1,33 @@ | |||
import { PieChart, Pie, Tooltip, ResponsiveContainer } from "recharts"; | |||
import React, { FC } from "react"; | |||
interface Props { | |||
data: any[]; // TODO: types | |||
} | |||
const ChartPie: FC<Props> = ({ data }) => ( | |||
<ResponsiveContainer | |||
width="100%" | |||
height={window.innerWidth < 468 ? 240 : 320} | |||
> | |||
<PieChart | |||
margin={{ | |||
top: window.innerWidth < 468 ? 56 : 0, | |||
right: window.innerWidth < 468 ? 56 : 0, | |||
bottom: window.innerWidth < 468 ? 56 : 0, | |||
left: window.innerWidth < 468 ? 56 : 0 | |||
}} | |||
> | |||
<Pie | |||
data={data} | |||
dataKey="value" | |||
innerRadius={window.innerWidth < 468 ? 20 : 80} | |||
fill="#B39DDB" | |||
label={({ name }) => name} | |||
/> | |||
<Tooltip /> | |||
</PieChart> | |||
</ResponsiveContainer> | |||
); | |||
export default ChartPie; |
@ -0,0 +1,3 @@ | |||
export { default as Area } from "./Area"; | |||
export { default as Bar } from "./Bar"; | |||
export { default as Pie } from "./Pie"; |
@ -0,0 +1,97 @@ | |||
import React, { FC } from "react"; | |||
import styled, { css } from "styled-components"; | |||
import { ifProp } from "styled-tools"; | |||
import { Flex, BoxProps } from "reflexbox/styled-components"; | |||
import Text, { Span } from "./Text"; | |||
interface InputProps { | |||
checked: boolean; | |||
id?: string; | |||
name: string; | |||
} | |||