Namespaces & Lists Page (#160)

Center list backgrounds

Better alignment of new namespace and filter button

Make creating new namespace button clear

Hide archived lists unless the user wants it

Make all cards responsive

Cleanup

Show it if a namespace is archived

Show if a list is archived

Fix not updating the list in store after updating the background

Make task cards smaller

Display list backgrounds in cards and look good while doing it

lighter shadow

Change background to stripes

Set list backgrounds as card backgrounds

Add background color check to color appropriatly

Move color check to mixin

Use background color from tasks

Change list card color

Make create new namespace button stick to the right

Shadow all the things

Don't keep list backgrounds set when navigating back

Make links to list clickable

Add seperate page for namespaces

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/160
This commit is contained in:
konrad 2020-06-16 22:20:37 +00:00
parent 98fb043e15
commit cf136132e3
13 changed files with 269 additions and 77 deletions

View file

@ -108,11 +108,11 @@
</router-link>
</li>
<li>
<router-link :to="{ name: 'newNamespace'}">
<router-link :to="{ name: 'namespaces.index'}">
<span class="icon">
<icon icon="layer-group"/>
</span>
New Namespace
Namespaces & Lists
</router-link>
</li>
<li>
@ -126,9 +126,6 @@
</ul>
</div>
<aside class="menu namespaces-lists">
<fancycheckbox v-model="showArchived" class="show-archived-check">
Show Archived
</fancycheckbox>
<div class="spinner" :class="{ 'is-loading': namespaceService.loading}"></div>
<template v-for="n in namespaces">
<div :key="n.id">
@ -163,9 +160,6 @@
</span>
{{n.title}} ({{n.lists.length}})
</span>
<span class="is-archived" v-if="n.isArchived">
Archived
</span>
</label>
</div>
<input
@ -188,9 +182,6 @@
</span>
{{l.title}}
</span>
<span class="is-archived" v-if="l.isArchived">
Archived
</span>
</router-link>
</li>
</ul>
@ -272,13 +263,11 @@
import swEvents from './ServiceWorker/events'
import Notification from './components/global/notification'
import Fancycheckbox from './components/global/fancycheckbox'
import {CURRENT_LIST, IS_FULLPAGE, ONLINE} from './store/mutation-types'
export default {
name: 'app',
components: {
Fancycheckbox,
Notification,
},
data() {
@ -288,7 +277,6 @@
currentDate: new Date(),
userMenuActive: false,
authTypes: authTypes,
showArchived: false,
// Service Worker stuff
updateAvailable: false,
@ -388,7 +376,7 @@
online: ONLINE,
fullpage: IS_FULLPAGE,
namespaces(state) {
return state.namespaces.namespaces.filter(n => this.showArchived ? true : !n.isArchived)
return state.namespaces.namespaces.filter(n => !n.isArchived)
},
currentList: CURRENT_LIST,
background: 'background',
@ -425,7 +413,8 @@
this.$route.name === 'listLabels' ||
this.$route.name === 'migrateStart' ||
this.$route.name === 'migrate.wunderlist' ||
this.$route.name === 'userSettings'
this.$route.name === 'userSettings' ||
this.$route.name === 'namespaces.index'
) {
this.$store.commit(CURRENT_LIST, {})
}

View file

@ -139,7 +139,7 @@
import ColorPicker from '../global/colorPicker'
export default {
name: "EditList",
name: 'EditList',
data() {
return {
list: ListModel,

View file

@ -148,6 +148,7 @@
this.backgroundService.update({id: backgroundId, listId: this.listId})
.then(l => {
this.$store.commit(CURRENT_LIST, l)
this.$store.commit('namespaces/setListInNamespaceById', l)
this.success({message: 'The background has been set successfully!'}, this)
})
.catch(e => {
@ -162,6 +163,7 @@
this.backgroundUploadService.create(this.listId, this.$refs.backgroundUploadInput.files[0])
.then(l => {
this.$store.commit(CURRENT_LIST, l)
this.$store.commit('namespaces/setListInNamespaceById', l)
this.success({message: 'The background has been set successfully!'}, this)
})
.catch(e => {

View file

@ -0,0 +1,95 @@
<template>
<div class="content namespaces-list">
<router-link :to="{name: 'newNamespace'}" class="button is-success new-namespace">
<span class="icon is-small">
<icon icon="plus"/>
</span>
Create new namespace
</router-link>
<fancycheckbox v-model="showArchived" class="show-archived-check">
Show Archived
</fancycheckbox>
<div class="namespace" v-for="n in namespaces" :key="`n${n.id}`">
<h1>
<span>{{ n.title }}</span>
<span class="is-archived" v-if="n.isArchived">
Archived
</span>
</h1>
<div class="lists">
<template v-for="l in n.lists">
<router-link
:to="{ name: 'list.index', params: { listId: l.id} }"
class="list"
:key="`l${l.id}`"
v-if="showArchived ? true : !l.isArchived"
:style="{
'background-color': l.hexColor,
'background-image': typeof backgrounds[l.id] !== 'undefined' ? `url(${backgrounds[l.id]})` : false,
}"
:class="{
'has-light-text': !colorIsDark(l.hexColor),
'has-background': typeof backgrounds[l.id] !== 'undefined',
}"
>
<div class="is-archived-container">
<span class="is-archived" v-if="l.isArchived">
Archived
</span>
</div>
<div class="title">{{ l.title }}</div>
</router-link>
</template>
</div>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
import ListService from '../../services/list'
import Fancycheckbox from '../global/fancycheckbox'
export default {
name: 'ListNamespaces',
components: {
Fancycheckbox,
},
data() {
return {
showArchived: false,
// listId is the key, the object is the background blob
backgrounds: {},
}
},
computed: mapState({
namespaces(state) {
return state.namespaces.namespaces.filter(n => this.showArchived ? true : !n.isArchived)
},
}),
created() {
this.loadBackgroundsForLists()
},
methods: {
loadBackgroundsForLists() {
const listService = new ListService()
this.namespaces.forEach(n => {
n.lists.forEach(l => {
if (l.backgroundInformation) {
listService.background(l)
.then(b => {
this.$set(this.backgrounds, l.id, b)
})
.catch(e => {
this.error(e, this)
})
}
})
})
},
},
}
</script>

View file

@ -28,7 +28,12 @@
<div class="row" v-for="(t, k) in theTasks" :key="t.id" :style="{background: 'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' + (k % 2 === 0 ? '#fafafa 1px, #fafafa ' : '#fff 1px, #fff ') + dayWidth + 'px)'}">
<VueDragResize
class="task"
:class="{'done': t.done, 'is-current-edit': taskToEdit !== null && taskToEdit.id === t.id, 'has-light-text': !t.hasDarkColor(), 'has-dark-text': t.hasDarkColor()}"
:class="{
'done': t.done,
'is-current-edit': taskToEdit !== null && taskToEdit.id === t.id,
'has-light-text': !colorIsDark(t.hexColor),
'has-dark-text': colorIsDark(t.hexColor)
}"
:style="{'border-color': t.hexColor, 'background-color': t.hexColor}"
:isActive="true"
:x="t.offsetDays * dayWidth - 6"

View file

@ -0,0 +1,15 @@
export const colorIsDark = color => {
if (color === '#' || color === '') {
return true // Defaults to dark
}
let rgb = parseInt(color.substring(1, 7), 16); // convert rrggbb to decimal
let r = (rgb >> 16) & 0xff; // extract red
let g = (rgb >> 8) & 0xff; // extract green
let b = (rgb >> 0) & 0xff; // extract blue
// luma will be a value 0..255 where 0 indicates the darkest, and 255 the brightest
let luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
return luma > 128
}

View file

@ -140,6 +140,7 @@ Vue.directive('focus', {
// Mixins
import message from './message'
import {format, formatDistance} from 'date-fns'
import {colorIsDark} from './helpers/colorIsDark'
Vue.mixin({
methods: {
formatDateSince: date => {
@ -158,6 +159,7 @@ Vue.mixin({
formatDate: date => format(date, 'PPPPpppp'),
error: (e, context, actions = []) => message.error(e, context, actions),
success: (s, context, actions = []) => message.success(s, context, actions),
colorIsDark: colorIsDark
}
})

View file

@ -1,5 +1,6 @@
import AbstractModel from './abstractModel'
import UserModel from "./user";
import UserModel from './user'
import {colorIsDark} from '../helpers/colorIsDark'
export default class LabelModel extends AbstractModel {
constructor(data) {
@ -11,13 +12,13 @@ export default class LabelModel extends AbstractModel {
if (this.hexColor.substring(0, 1) !== '#') {
this.hexColor = '#' + this.hexColor
}
this.textColor = this.hasDarkColor() ? '#4a4a4a' : '#e5e5e5'
this.textColor = colorIsDark(this.hexColor) ? '#4a4a4a' : '#e5e5e5'
this.createdBy = new UserModel(this.createdBy)
this.created = new Date(this.created)
this.updated = new Date(this.updated)
}
defaults() {
return {
id: 0,
@ -27,24 +28,9 @@ export default class LabelModel extends AbstractModel {
createdBy: UserModel,
listId: 0,
textColor: '',
created: null,
updated: null,
}
}
hasDarkColor() {
if (this.hexColor === '#') {
return true // Defaults to dark
}
let rgb = parseInt(this.hexColor.substring(1, 7), 16); // convert rrggbb to decimal
let r = (rgb >> 16) & 0xff; // extract red
let g = (rgb >> 8) & 0xff; // extract green
let b = (rgb >> 0) & 0xff; // extract blue
// luma will be a value 0..255 where 0 indicates the darkest, and 255 the brightest
let luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
return luma > 128
}
}

View file

@ -135,25 +135,6 @@ export default class TaskModel extends AbstractModel {
}
}
/**
* Checks if the hexColor of a task is dark.
* @returns {boolean}
*/
hasDarkColor() {
if (this.hexColor === '#') {
return true // Defaults to dark
}
let rgb = parseInt(this.hexColor.substring(1, 7), 16); // convert rrggbb to decimal
let r = (rgb >> 16) & 0xff; // extract red
let g = (rgb >> 8) & 0xff; // extract green
let b = (rgb >> 0) & 0xff; // extract blue
// luma will be a value 0..255 where 0 indicates the darkest, and 255 the brightest
let luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
return luma > 128
}
async cancelScheduledNotifications() {
if (!('showTrigger' in Notification.prototype)) {
console.debug('This browser does not support triggered notifications')

View file

@ -19,6 +19,7 @@ import TaskDetailView from '../components/tasks/TaskDetailView'
// Namespace Handling
import NewNamespaceComponent from '@/components/namespaces/NewNamespace'
import EditNamespaceComponent from '@/components/namespaces/EditNamespace'
import ListNamespaces from '../components/namespaces/ListNamespaces'
// Team Handling
import ListTeamsComponent from '@/components/teams/ListTeams'
import EditTeamComponent from '@/components/teams/EditTeam'
@ -144,6 +145,11 @@ export default new Router({
},
]
},
{
path: '/namespaces',
name: 'namespaces.index',
component: ListNamespaces,
},
{
path: '/namespaces/:id/list',
name: 'newList',

View file

@ -18,3 +18,4 @@
@import 'modal';
@import 'list-backgrounds';
@import 'color-picker';
@import 'namespaces';

View file

@ -0,0 +1,131 @@
$lists-per-row: 5;
.namespaces-list {
.button.new-namespace {
float: right;
@media screen and (max-width: $tablet / 2) {
float: none;
width: 100%;
margin-bottom: 1rem;
}
}
.show-archived-check {
margin-bottom: 1rem;
}
.namespace {
margin-bottom: 1rem;
h1 {
display: flex;
align-items: center;
}
.is-archived {
font-size: 0.75rem;
border: 1px solid $grey;
color: $grey !important;
padding: 2px 4px;
margin-left: .5rem;
border-radius: 3px;
font-family: $vikunja-font;
background: rgba($white, 0.75);
}
.lists {
display: flex;
flex-flow: row wrap;
.list {
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: 0.3em 0.3em 1em lighten($dark, 75);
transition: box-shadow $transition;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
&:hover {
box-shadow: 0 0 1em lighten($dark, 65);
}
@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: $tablet / 2) {
$lists-per-row: 1;
& {
width: 100%;
margin-right: 0;
}
}
.is-archived-container {
width: 100%;
text-align: right;
.is-archived {
font-size: .75em;
}
}
.title {
align-self: flex-end;
font-family: $vikunja-font;
font-weight: 400;
font-size: 1.5rem;
color: $text;
width: 100%;
margin-bottom: 0;
}
&.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-darker, -1px -1px 5px $grey-darker;
color: $white;
}
}
}
}
}
}

View file

@ -125,15 +125,6 @@
}
}
.show-archived-check {
width: 100%;
text-align: right;
span {
vertical-align: super;
}
}
.menu {
.menu-label {
font-size: 1em;
@ -171,18 +162,6 @@
}
}
.is-archived {
font-size: 0.75em;
border: 1px solid $grey;
color: $grey !important;
padding: 2px 4px;
border-radius: 3px;
font-family: $vikunja-font;
min-width: 60px;
display: block;
margin-left: 3px;
text-align: center;
}
}
.menu-label {