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 @@
+
+
+
+
+ {{ $t('namespace.archived') }}
+
+
+
+
+
+
+ {{ list.title }}
+
+
+
+
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 @@