diff --git a/package.json b/package.json index fbed6eb6..3d822e28 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "jest": { "testPathIgnorePatterns": [ "cypress" - ] + ], + "testEnvironment": "jsdom" } } diff --git a/src/components/list/partials/list-card.vue b/src/components/list/partials/list-card.vue new file mode 100644 index 00000000..053e6528 --- /dev/null +++ b/src/components/list/partials/list-card.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/helpers/color/colorIsDark.js b/src/helpers/color/colorIsDark.js index 01cf9e80..d548fdcd 100644 --- a/src/helpers/color/colorIsDark.js +++ b/src/helpers/color/colorIsDark.js @@ -1,4 +1,8 @@ export const colorIsDark = color => { + if (typeof color === 'undefined') { + return true // Defaults to dark + } + if (color === '#' || color === '') { return true // Defaults to dark } diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index e183ce40..ec7cdf76 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -4,6 +4,7 @@ "welcomeMorning": "Good Morning {username}", "welcomeDay": "Hi {username}", "welcomeEvening": "Good Evening {username}", + "lastViewed": "Last viewed", "list": { "newText": "You can create a new list for your new tasks:", "new": "Create a new list", diff --git a/src/modules/listHistory.js b/src/modules/listHistory.js new file mode 100644 index 00000000..96acbe33 --- /dev/null +++ b/src/modules/listHistory.js @@ -0,0 +1,26 @@ +export const getHistory = () => { + const savedHistory = localStorage.getItem('listHistory') + if (savedHistory === null) { + return [] + } + + return JSON.parse(savedHistory) +} + +export function saveListToHistory(list) { + const history = getHistory() + + // Remove the element if it already exists in history, preventing duplicates and essentially moving it to the beginning + for (const i in history) { + if (history[i].id === list.id) { + history.splice(i, 1) + } + } + + history.unshift(list) + + if (history.length > 5) { + history.pop() + } + localStorage.setItem('listHistory', JSON.stringify(history)) +} diff --git a/src/modules/listHistory.test.js b/src/modules/listHistory.test.js new file mode 100644 index 00000000..d3d8b842 --- /dev/null +++ b/src/modules/listHistory.test.js @@ -0,0 +1,67 @@ +import {getHistory, saveListToHistory} from './listHistory' + +test('return an empty history when none was saved', () => { + Storage.prototype.getItem = jest.fn(() => null) + const h = getHistory() + expect(h).toStrictEqual([]) +}) + +test('return a saved history', () => { + const saved = [{id: 1}, {id: 2}] + Storage.prototype.getItem = jest.fn(() => JSON.stringify(saved)) + + const h = getHistory() + expect(h).toStrictEqual(saved) +}) + +test('store list in history', () => { + let saved = {} + Storage.prototype.getItem = jest.fn(() => null) + Storage.prototype.setItem = jest.fn((key, lists) => { + saved = lists + }) + + saveListToHistory({id: 1}) + expect(saved).toBe('[{"id":1}]') +}) + +test('store only the last 5 lists in history', () => { + let saved = null + Storage.prototype.getItem = jest.fn(() => saved) + Storage.prototype.setItem = jest.fn((key, lists) => { + saved = lists + }) + + saveListToHistory({id: 1}) + saveListToHistory({id: 2}) + saveListToHistory({id: 3}) + saveListToHistory({id: 4}) + saveListToHistory({id: 5}) + saveListToHistory({id: 6}) + expect(saved).toBe('[{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]') +}) + +test('don\'t store the same list twice', () => { + let saved = null + Storage.prototype.getItem = jest.fn(() => saved) + Storage.prototype.setItem = jest.fn((key, lists) => { + saved = lists + }) + + saveListToHistory({id: 1}) + saveListToHistory({id: 1}) + expect(saved).toBe('[{"id":1}]') +}) + +test('move a list to the beginning when storing it multiple times', () => { + let saved = null + Storage.prototype.getItem = jest.fn(() => saved) + Storage.prototype.setItem = jest.fn((key, lists) => { + saved = lists + }) + + saveListToHistory({id: 1}) + saveListToHistory({id: 2}) + saveListToHistory({id: 1}) + expect(saved).toBe('[{"id":1},{"id":2}]') +}) diff --git a/src/store/modules/lists.js b/src/store/modules/lists.js index cd088b86..5f5b57db 100644 --- a/src/store/modules/lists.js +++ b/src/store/modules/lists.js @@ -78,6 +78,6 @@ export default { return Promise.reject(e) }) .finally(() => cancel()) - } + }, }, } \ No newline at end of file diff --git a/src/styles/components/list.scss b/src/styles/components/list.scss index d20d2e13..cfd512c5 100644 --- a/src/styles/components/list.scss +++ b/src/styles/components/list.scss @@ -163,3 +163,140 @@ $filter-container-top-link-share-list: -47px; .is-archived .notification.is-warning { margin-bottom: 1rem; } + +$lists-per-row: 5; +$list-height: 150px; +$list-spacing: 1rem; + +.list-card { + cursor: pointer; + width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row}); + height: $list-height; + background: $white; + margin: 0 $list-spacing $list-spacing 0; + padding: 1rem; + border-radius: $radius; + box-shadow: $shadow-sm; + transition: box-shadow $transition; + + display: flex; + justify-content: space-between; + flex-wrap: wrap; + + &:hover { + box-shadow: $shadow-md; + } + + &:active, + &:focus, + &:focus:not(:active) { + box-shadow: $shadow-xs !important; + } + + @media screen and (min-width: $widescreen) { + &:nth-child(#{$lists-per-row}n) { + margin-right: 0; + } + } + + @media screen and (max-width: $widescreen) and (min-width: $tablet) { + $lists-per-row: 3; + & { + width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row}); + } + + &:nth-child(#{$lists-per-row}n) { + margin-right: 0; + } + } + + @media screen and (max-width: $tablet) { + $lists-per-row: 2; + & { + width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row}); + } + + &:nth-child(#{$lists-per-row}n) { + margin-right: 0; + } + } + + @media screen and (max-width: $mobile) { + $lists-per-row: 1; + & { + width: 100%; + margin-right: 0; + } + } + + .is-archived-container { + width: 100%; + text-align: right; + + .is-archived { + font-size: .75rem; + float: left; + } + } + + .title { + align-self: flex-end; + font-family: $vikunja-font; + font-weight: 400; + font-size: 1.5rem; + color: $text; + width: 100%; + margin-bottom: 0; + max-height: calc(100% - 2rem); // 1rem padding, 1rem height of the "is archived" badge + overflow: hidden; + text-overflow: ellipsis; + + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } + + &.has-light-text .title { + color: $light; + } + + &.has-background { + background-size: cover; + background-repeat: no-repeat; + background-position: center; + + .title { + text-shadow: 0 0 10px $black, 1px 1px 5px $grey-700, -1px -1px 5px $grey-700; + color: $white; + } + } + + .favorite { + transition: opacity $transition, color $transition; + opacity: 0; + + &:hover { + color: $orange; + } + + &.is-archived { + display: none; + } + + &.is-favorite { + display: inline-block; + opacity: 1; + color: $orange; + } + } + + &:hover .favorite { + opacity: 1; + } +} + +.list-cards-wrapper-2-rows { + flex-wrap: wrap; + max-height: calc(#{$list-height * 2} + #{$list-spacing * 2}); + overflow: hidden; +} diff --git a/src/styles/components/namespaces.scss b/src/styles/components/namespaces.scss index e7b2e2cf..2e63d051 100644 --- a/src/styles/components/namespaces.scss +++ b/src/styles/components/namespaces.scss @@ -1,5 +1,3 @@ -$lists-per-row: 5; - .namespaces-list { .button.new-namespace { float: right; @@ -44,133 +42,6 @@ $lists-per-row: 5; .lists { display: flex; flex-flow: row wrap; - - .list { - cursor: pointer; - width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row}); - height: 150px; - background: $white; - margin: 0 1rem 1rem 0; - padding: 1rem; - border-radius: $radius; - box-shadow: $shadow-sm; - transition: box-shadow $transition; - - display: flex; - justify-content: space-between; - flex-wrap: wrap; - - &:hover { - box-shadow: $shadow-md; - } - - &:active, - &:focus, - &:focus:not(:active) { - box-shadow: $shadow-xs !important; - } - - @media screen and (min-width: $widescreen) { - &:nth-child(#{$lists-per-row}n) { - margin-right: 0; - } - } - - @media screen and (max-width: $widescreen) and (min-width: $tablet) { - $lists-per-row: 3; - & { - width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row}); - } - - &:nth-child(#{$lists-per-row}n) { - margin-right: 0; - } - } - - @media screen and (max-width: $tablet) { - $lists-per-row: 2; - & { - width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row}); - } - - &:nth-child(#{$lists-per-row}n) { - margin-right: 0; - } - } - - @media screen and (max-width: $mobile) { - $lists-per-row: 1; - & { - width: 100%; - margin-right: 0; - } - } - - .is-archived-container { - width: 100%; - text-align: right; - - .is-archived { - font-size: .75rem; - float: left; - } - } - - .title { - align-self: flex-end; - font-family: $vikunja-font; - font-weight: 400; - font-size: 1.5rem; - color: $text; - width: 100%; - margin-bottom: 0; - max-height: calc(100% - 2rem); // 1rem padding, 1rem height of the "is archived" badge - overflow: hidden; - text-overflow: ellipsis; - - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - } - - &.has-light-text .title { - color: $light; - } - - &.has-background { - background-size: cover; - background-repeat: no-repeat; - background-position: center; - - .title { - text-shadow: 0 0 10px $black, 1px 1px 5px $grey-700, -1px -1px 5px $grey-700; - color: $white; - } - } - - .favorite { - transition: opacity $transition, color $transition; - opacity: 0; - - &:hover { - color: $orange; - } - - &.is-archived { - display: none; - } - - &.is-favorite { - display: inline-block; - opacity: 1; - color: $orange; - } - } - - &:hover .favorite { - opacity: 1; - } - } } } } \ No newline at end of file diff --git a/src/views/Home.vue b/src/views/Home.vue index 325570b9..d2cbb266 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -23,6 +23,17 @@ {{ $t('home.list.import') }} +
+

{{ $t('home.lastViewed') }}

+
+ +
+
@@ -30,10 +41,13 @@