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:
parent
98fb043e15
commit
cf136132e3
13 changed files with 269 additions and 77 deletions
21
src/App.vue
21
src/App.vue
|
@ -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, {})
|
||||
}
|
||||
|
|
|
@ -139,7 +139,7 @@
|
|||
import ColorPicker from '../global/colorPicker'
|
||||
|
||||
export default {
|
||||
name: "EditList",
|
||||
name: 'EditList',
|
||||
data() {
|
||||
return {
|
||||
list: ListModel,
|
||||
|
|
|
@ -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 => {
|
||||
|
|
95
src/components/namespaces/ListNamespaces.vue
Normal file
95
src/components/namespaces/ListNamespaces.vue
Normal 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>
|
|
@ -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"
|
||||
|
|
15
src/helpers/colorIsDark.js
Normal file
15
src/helpers/colorIsDark.js
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
@import 'modal';
|
||||
@import 'list-backgrounds';
|
||||
@import 'color-picker';
|
||||
@import 'namespaces';
|
||||
|
|
131
src/styles/components/namespaces.scss
Normal file
131
src/styles/components/namespaces.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue