diff --git a/frontend/src/components/Dashboard/Post.vue b/frontend/src/components/Dashboard/Post.vue index 852719b..5ad6a52 100644 --- a/frontend/src/components/Dashboard/Post.vue +++ b/frontend/src/components/Dashboard/Post.vue @@ -9,14 +9,7 @@ > -
-
-
-
-
-
-
-
+
{{ message }} @@ -47,26 +40,20 @@ : 'fill-gray-300 dark:fill-gray-400 w-5 h-5' " /> -
-
-
-
-
-
+
diff --git a/frontend/src/components/NavBar/ThemeToggle.vue b/frontend/src/components/NavBar/ThemeToggle.vue index 5574f9e..e45da25 100644 --- a/frontend/src/components/NavBar/ThemeToggle.vue +++ b/frontend/src/components/NavBar/ThemeToggle.vue @@ -50,6 +50,7 @@ export default { } else { themeToggleDarkIcon && themeToggleDarkIcon.classList.remove('hidden'); } + this.$store.dispatch('setTheme', localStorage.getItem('color-theme')); }, methods: { toggleTheme() { @@ -82,6 +83,7 @@ export default { localStorage.setItem('color-theme', 'dark'); } } + this.$store.dispatch('setTheme', localStorage.getItem('color-theme')); }, }, }; diff --git a/frontend/src/components/NavBar/UserMenu.vue b/frontend/src/components/NavBar/UserMenu.vue index 086dbc7..2fbd89d 100644 --- a/frontend/src/components/NavBar/UserMenu.vue +++ b/frontend/src/components/NavBar/UserMenu.vue @@ -42,7 +42,7 @@ > Sign out diff --git a/frontend/src/components/Register.vue b/frontend/src/components/Register.vue index 93da6bf..d1d8b68 100644 --- a/frontend/src/components/Register.vue +++ b/frontend/src/components/Register.vue @@ -43,17 +43,11 @@ >This username already exists
-
-
-
-
-
-
-
-
+ small + class-names="xs:w-90 w-100" + />
@@ -87,17 +81,11 @@ >This email already exists
-
-
-
-
-
-
-
-
+ small + class-names="xs:w-90 w-100" + />
@@ -198,12 +186,14 @@ import { } from 'firebase/auth'; import Email from './misc/icons/Email.vue'; import Password from './misc/icons/Password.vue'; +import Loading from './misc/Loading.vue'; import { mapGetters } from 'vuex'; export default { components: { Email, Password, + Loading, }, data() { return { diff --git a/frontend/src/components/ResetPassword.spec.js b/frontend/src/components/ResetPassword.spec.js new file mode 100644 index 0000000..e273594 --- /dev/null +++ b/frontend/src/components/ResetPassword.spec.js @@ -0,0 +1,46 @@ +import { mount } from '@vue/test-utils'; +import ResetPassword from './ResetPassword.vue'; + +const mountComponent = () => + mount(ResetPassword, { + global: { + stubs: ['router-link'], + }, + }); + +describe('ResetPassword', () => { + describe('Email field', () => { + describe('when entering "test@test.com"', () => { + it('sets the input value to "test@test.com', async () => { + const wrapper = mountComponent(); + + const input = wrapper.find('input[type="email"]'); + + await input.setValue('test@test.com'); + + expect(input.element.value).toBe('test@test.com'); + }); + }); + }); + describe('Submit button', () => { + it('is disabled by default', () => { + const wrapper = mountComponent(); + + const button = wrapper.find('button[type="submit"]'); + + expect(button.element.disabled).toBe(true); + }); + describe('when entering a valid email and a password', () => { + it('is enabled', async () => { + const wrapper = mountComponent(); + + const input = wrapper.find('input[type="email"]'); + + await input.setValue('test@test.com'); + + const button = wrapper.find('button'); + expect(button.element.disabled).toBe(false); + }); + }); + }); +}); diff --git a/frontend/src/components/ResetPassword.vue b/frontend/src/components/ResetPassword.vue new file mode 100644 index 0000000..73470cf --- /dev/null +++ b/frontend/src/components/ResetPassword.vue @@ -0,0 +1,90 @@ + + + diff --git a/frontend/src/components/misc/Loading.vue b/frontend/src/components/misc/Loading.vue new file mode 100644 index 0000000..2f05d80 --- /dev/null +++ b/frontend/src/components/misc/Loading.vue @@ -0,0 +1,86 @@ + + + diff --git a/frontend/src/index.css b/frontend/src/index.css index 97ec612..bd6213e 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,75 +1,3 @@ @tailwind base; @tailwind components; -@tailwind utilities; - -.lds-ring { - display: inline-block; - position: relative; - width: 80px; - height: 80px; -} -.lds-ring div { - box-sizing: border-box; - display: block; - position: absolute; - width: 64px; - height: 64px; - margin: 8px; - border: 8px solid #fff; - border-radius: 50%; - animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; - border-color: #fff transparent transparent transparent; -} -.lds-ring div:nth-child(1) { - animation-delay: -0.45s; -} -.lds-ring div:nth-child(2) { - animation-delay: -0.3s; -} -.lds-ring div:nth-child(3) { - animation-delay: -0.15s; -} -@keyframes lds-ring { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -.lds-ring-small { - display: inline-block; - position: relative; - width: 10px; - height: 10px; -} -.lds-ring-small div { - box-sizing: border-box; - display: block; - position: absolute; - width: 8px; - height: 8px; - margin: 1px; - border: 1px solid #fff; - border-radius: 50%; - animation: lds-ring-small 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; - border-color: #fff transparent transparent transparent; -} -.lds-ring-small div:nth-child(1) { - animation-delay: -0.45s; -} -.lds-ring-small div:nth-child(2) { - animation-delay: -0.3s; -} -.lds-ring-small div:nth-child(3) { - animation-delay: -0.15s; -} -@keyframes lds-ring-small { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} \ No newline at end of file +@tailwind utilities; \ No newline at end of file diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js index 35b44fb..8952ce2 100644 --- a/frontend/src/routes/index.js +++ b/frontend/src/routes/index.js @@ -5,6 +5,7 @@ import Dashboard from '../components/Dashboard'; import Terms from '../components/misc/Terms'; import PrivacyPolicy from '../components/misc/PrivacyPolicy'; import About from '../components/misc/About'; +import ResetPassword from '../components/ResetPassword'; const router = createRouter({ mode: 'history', @@ -16,6 +17,11 @@ const router = createRouter({ name: 'Login', component: Login, }, + { + path: '/reset-password', + name: 'ResetPassword', + component: ResetPassword, + }, { path: '/register', name: 'Register', diff --git a/frontend/src/store/actions/index.js b/frontend/src/store/actions/index.js index 288dd66..1b256c3 100644 --- a/frontend/src/store/actions/index.js +++ b/frontend/src/store/actions/index.js @@ -89,4 +89,7 @@ export default { commit('SET_IS_CHECKING_NAME', false); } }, + setTheme({ commit }, theme) { + commit('SET_THEME', theme); + }, }; diff --git a/frontend/src/store/actions/index.spec.js b/frontend/src/store/actions/index.spec.js index 488d854..ce919fc 100644 --- a/frontend/src/store/actions/index.spec.js +++ b/frontend/src/store/actions/index.spec.js @@ -1,5 +1,5 @@ import actions from '.'; -import { apiRequest } from './api'; +import { apiRequest, unAuthApiRequest } from './api'; jest.mock('./api'); // helper for testing action with expected mutations @@ -122,8 +122,14 @@ describe('actions', () => { }); }); describe('postMessage', () => { + const date = new Date(2020, 3, 1); afterEach(() => { jest.resetAllMocks(); + jest.useRealTimers(); + }); + beforeEach(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(date); }); it('posts the message to the API', (done) => { apiRequest.mockResolvedValueOnce({ data: POSTS_RESPONSE_FIXTURE[0] }); @@ -157,5 +163,136 @@ describe('actions', () => { }, ); }); + it('can post a comment to a message', (done) => { + apiRequest.mockResolvedValueOnce({ + data: { ...POSTS_RESPONSE_FIXTURE[0], parentId: '1' }, + }); + + testAction( + actions.postMessage, + 'Hello World 2', + { + posts: [{ id: '1', message: 'hello world' }], + creatingPost: false, + parentId: '1', + }, + [ + { + type: 'IS_CREATING_POST', + payload: true, + }, + { + type: 'PUSH_COMMENT', + payload: { + message: 'Hello World 2', + date: date.getTime(), + parentId: '1', + }, + }, + { type: 'POP_COMMENT' }, + { + type: 'PUSH_COMMENT', + payload: { + message: 'Hello World 2', + date: 1555555555555, + parentId: '1', + }, + }, + { type: 'INCREMENT_COMMENTS_COUNT' }, + { + type: 'IS_CREATING_POST', + payload: false, + }, + ], + () => { + expect(apiRequest).toHaveBeenCalledWith('POST', '/posts', null, { + message: 'Hello World 2', + parentId: '1', + }); + done(); + }, + ); + }); + }); + describe('toggleLike', () => { + beforeEach(() => { + apiRequest.mockResolvedValueOnce({ + data: { ...POSTS_RESPONSE_FIXTURE[0] }, + }); + }); + it('toggles the like on the message', (done) => { + testAction( + actions.toggleLike, + { + post: { id: '1', likes: 0, message: 'Hello World' }, + like: true, + }, + { posts: [{ id: '1', message: 'Hello World', likes: 0 }] }, + [ + { + type: 'SET_LIKING_POST', + payload: '1', + }, + { + type: 'SET_POST_BY_ID', + payload: { + id: '1', + message: 'Hello World', + likes: 1, + likedByMe: true, + }, + }, + { + type: 'SET_POST_BY_ID', + payload: POSTS_RESPONSE_FIXTURE[0], + }, + { + type: 'SET_LIKING_POST', + payload: null, + }, + ], + () => { + expect(apiRequest).toHaveBeenCalledWith('PUT', '/posts/1', null, { + like: true, + }); + done(); + }, + ); + }); + }); + describe('checkExists', () => { + beforeEach(() => { + unAuthApiRequest.mockResolvedValueOnce({ data: { exists: true } }); + }); + it('checks if the username exists', (done) => { + const payload = { name: 'test' }; + testAction( + actions.checkExists, + payload, + {}, + [ + { + type: 'SET_IS_CHECKING_NAME', + payload: true, + }, + { + type: 'SET_USER_NAME_EXISTS', + payload: true, + }, + { + type: 'SET_IS_CHECKING_NAME', + payload: false, + }, + ], + () => { + expect(unAuthApiRequest).toHaveBeenCalledWith( + 'POST', + '/users/exists', + payload, + ); + done(); + }, + ); + }); }); }); diff --git a/frontend/src/store/getters.spec.js b/frontend/src/store/getters.spec.js index d31ff0f..9879fbd 100644 --- a/frontend/src/store/getters.spec.js +++ b/frontend/src/store/getters.spec.js @@ -52,4 +52,23 @@ describe('getters', () => { expect(result).toEqual(state.posts); }); }); + describe('getPostById', () => { + it('should return the post by id', () => { + const state = { + posts: [ + { + id: '1', + message: 'Hello World', + }, + { + id: '2', + message: 'Hello World 2', + }, + ], + }; + const result = getters.getPostById(state, '2'); + + expect(result).toEqual(state.posts[1]); + }); + }); }); diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 0cc3679..602e9ad 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -21,6 +21,7 @@ const store = createStore({ isCheckingEmail: false, isCheckingName: false, parentId: null, + theme: null, }, getters, mutations, diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.js index aa8e713..46be6a5 100644 --- a/frontend/src/store/mutations.js +++ b/frontend/src/store/mutations.js @@ -75,4 +75,7 @@ export default { SET_IS_CHECKING_NAME(state, data) { state.isCheckingName = data; }, + SET_THEME(state, data) { + state.theme = data; + }, }; diff --git a/frontend/src/store/mutations.spec.js b/frontend/src/store/mutations.spec.js index 7f7e8f6..87bcb19 100644 --- a/frontend/src/store/mutations.spec.js +++ b/frontend/src/store/mutations.spec.js @@ -1,8 +1,17 @@ import mutations from './mutations'; // destructure assign `mutations` -const { SET_USER, SET_POSTS, PUSH_MESSAGE, PUSH_COMMENT, SET_MESSAGE } = - mutations; +const { + INCREMENT_COMMENTS_COUNT, + SET_USER, + SET_POST_BY_ID, + SET_POSTS, + POP_COMMENT, + POP_MESSAGE, + PUSH_MESSAGE, + PUSH_COMMENT, + SET_MESSAGE, +} = mutations; describe('mutations', () => { describe('SET_USER', () => { @@ -16,6 +25,17 @@ describe('mutations', () => { expect(state.user).toEqual(userFixture); }); }); + describe('SET_POST_BY_ID', () => { + it('can set the post by id', () => { + const postFixture = { id: '123', message: 'hello2' }; + // mock state + const state = { posts: [{ id: '123', message: 'hello1' }] }; + // apply mutation + SET_POST_BY_ID(state, postFixture); + // assert result + expect(state.posts).toEqual([postFixture]); + }); + }); describe('SET_POSTS', () => { it('can set the posts', () => { const postsFixture = [ @@ -34,16 +54,25 @@ describe('mutations', () => { }); describe('PUSH_MESSAGE', () => { it('can push a message to the posts', () => { + const posts = [ + { + id: '2', + message: 'Hello World', + }, + ]; const messageFixture = { id: '1', - message: 'Hello World', + message: 'Hello World 2', }; // mock state - const state = { posts: [], user: { displayName: 'Test' } }; + const state = { posts, user: { displayName: 'Test' } }; // apply mutation PUSH_MESSAGE(state, messageFixture); // assert result - expect(state.posts).toEqual([{ ...messageFixture, userName: 'Test' }]); + expect(state.posts).toEqual([ + { ...messageFixture, userName: 'Test' }, + ...posts, + ]); }); }); describe('PUSH_COMMENT', () => { @@ -67,6 +96,70 @@ describe('mutations', () => { ]); }); }); + describe('POP_MESSAGE', () => { + it('can pop a message from the posts', () => { + const posts = [ + { + id: '2', + message: 'Hello World', + }, + { + id: '1', + message: 'Hello World 2', + }, + ]; + // mock state + const state = { posts }; + // apply mutation + POP_MESSAGE(state); + // assert result + expect(state.posts).toEqual([posts[1]]); + }); + }); + describe('POP_COMMENT', () => { + it('can pop a comment from the posts', () => { + const posts = [ + { + id: '2', + message: 'Hello World', + }, + { + id: '1', + message: 'Hello World 2', + parentId: '2', + }, + ]; + // mock state + const state = { posts }; + // apply mutation + POP_COMMENT(state); + // assert result + expect(state.posts).toEqual([posts[0]]); + }); + }); + describe('INCREMENT_COMMENTS_COUNT', () => { + it('can increment the comments count', () => { + const posts = [ + { + id: '2', + message: 'Hello World', + comments: 0, + }, + { + id: '1', + message: 'Hello World 2', + parentId: '2', + }, + ]; + // mock state + const state = { posts, parentId: '2' }; + // apply mutation + INCREMENT_COMMENTS_COUNT(state); + // assert result + expect(state.posts).toEqual(posts); + expect(state.posts[0].comments).toEqual(1); + }); + }); describe('SET_MESSAGE', () => { it('can set the message', () => { const messageFixture = {