소스 검색

add new node components

axolotle 3 년 전
부모
커밋
659a16c29c

+ 162 - 0
src/components/nodes/NodeView.vue

@@ -0,0 +1,162 @@
+<template>
+  <article
+    class="node-view"
+    :class="['node-view-' + mode, 'node-view-' + type, 'node-view-' + nodeVariant]"
+  >
+    <div v-if="!loading" class="node-view-wrapper">
+      <component
+        :is="'node-view-header-' + type"
+        v-bind="{ node, mode }"
+        class="node-view-header"
+      />
+
+      <node-view-body v-bind="{ content: node.content, type: nodeType, mode }" />
+
+      <node-view-footer
+        v-bind="{ node, mode, type: nodeType }"
+        class="node-view-footer"
+      />
+    </div>
+
+    <b-overlay
+      :show="loading"
+      :spinner-variant="nodeVariant === 'depart' ? 'dark' : 'light'"
+      no-wrap
+    />
+
+    <slot name="bottom" />
+  </article>
+</template>
+
+<script>
+import {
+  NodeViewHeaderRef,
+  NodeViewHeaderProd,
+  NodeViewBody,
+  NodeViewFooter
+} from '@/components/nodes'
+
+
+export default {
+  name: 'NodeView',
+
+  components: {
+    NodeViewHeaderRef,
+    NodeViewHeaderProd,
+    NodeViewFooter,
+    NodeViewBody
+  },
+
+  props: {
+    node: { type: Object, default: undefined },
+    variant: { type: String, default: 'dark' },
+    type: { type: String, default: 'ref' },
+    mode: { type: String, default: 'view' }
+  },
+
+  computed: {
+    loading () {
+      const dataLevel = this.mode === 'view' ? 2 : 1
+      return this.node === undefined || this.node.dataLevel < dataLevel
+    },
+
+    nodeVariant () {
+      return this.node !== undefined ? this.node.variant : this.variant
+    },
+
+    nodeType () {
+      return this.node !== undefined ? this.node.type : this.type
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.node-view {
+  position: relative;
+
+  &-wrapper {
+    display: flex;
+    flex-direction: column;
+  }
+
+
+  // ╭─╴╭─┐┌─╮┌─╮
+  // │  ├─┤├┬╯│ │
+  // ╰─╴╵ ╵╵ ╰└─╯
+
+  &-card {
+    width: 560px;
+    height: 330px;
+    max-width: 560px;
+    max-height: 330px;
+    box-shadow: .5rem .5rem 1rem rgba($black, .25);
+  }
+
+  &-card &-header {
+    padding: $node-card-spacer-sm-y $node-card-spacer-sm-x 0;
+
+    @include media-breakpoint-up(md) {
+      padding: $node-card-spacer-y $node-card-spacer-x 0;
+    }
+  }
+
+  &-card &-footer {
+    padding: $node-card-spacer-y $node-card-spacer-x $node-card-spacer-y * 2;
+
+    @include media-breakpoint-up(md) {
+      padding: $node-card-spacer-sm-y $node-card-spacer-sm-x $node-card-spacer-sm-y * 2;
+    }
+  }
+
+
+  // ╷ ╷╶┬╴┌─╴╷╷╷
+  // │╭╯ │ ├─╴│││
+  // ╰╯ ╶┴╴╰─╴╰╯╯
+
+  &-view {
+    width: 100%;
+
+    @include media-breakpoint-up(md) {
+      height: 100%;
+      overflow-y: auto;
+
+      .node-view-wrapper {
+        min-height: 100%;
+      }
+    }
+  }
+
+  &-view &-header {
+    padding: $node-view-spacer-sm-y $node-view-spacer-sm-x 0;
+
+    @include media-breakpoint-up(md) {
+      padding: $node-view-spacer-y $node-view-spacer-x 0;
+      position: sticky;
+      z-index: 1;
+      top: 0;
+    }
+  }
+
+  &-view &-footer {
+    padding: $node-view-spacer-sm-y $node-view-spacer-sm-x;
+
+    @include media-breakpoint-up(md) {
+      padding: $node-view-spacer-y $node-view-spacer-x;
+      position: sticky;
+      bottom: 0;
+    }
+  }
+
+  @each $color, $value in $theme-colors {
+    &-#{$color} {
+      background-color: lighten($value, 3.25%);
+
+      .node-view-header,
+      .node-view-footer {
+        background-color: lighten($value, 3.25%);
+      }
+    }
+  }
+}
+</style>

+ 74 - 0
src/components/nodes/NodeViewBody.vue

@@ -0,0 +1,74 @@
+<template>
+  <div
+    class="node-view-body"
+    :class="['node-view-body-' + mode, 'node-view-body-' + type]"
+  >
+    <slot name="default">
+      <div class="node-view-body-wrapper" v-html="content" />
+    </slot>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'NodeViewBody',
+
+  props: {
+    content: { type: String, default: null },
+    type: { type: String, required: true },
+    mode: { type: String, required: true }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.node-view-body {
+  width: 100%;
+
+  font: {
+    family: $font-family-text;
+    line-height: inherit;
+
+  }
+
+  @include media-breakpoint-up(sm) {
+    font-size: 2rem;
+  }
+
+  .node-view-view & {
+    padding: 0 $node-view-spacer-sm-x;
+    font-size: 1.25rem;
+
+    @include media-breakpoint-up(sm) {
+      &-ref {
+        font-size: 2.6rem;
+      }
+
+      &-prod {
+        font-size: 1.5rem !important;
+      }
+    }
+
+    @include media-breakpoint-up(md) {
+      padding: 0 $node-view-spacer-x;
+    }
+  }
+
+  .node-view-card & {
+    padding: 0 $node-card-spacer-sm-x;
+    font-size: 1.1rem;
+
+    @include media-breakpoint-up(sm) {
+      font-size: 2rem;
+    }
+
+    @include media-breakpoint-up(md) {
+      padding: 0 $node-card-spacer-x;
+    }
+  }
+
+  &-card &-wrapper {
+    @include line-clamp(3, 1.5em);
+  }
+}
+</style>

+ 75 - 0
src/components/nodes/NodeViewChildList.vue

@@ -0,0 +1,75 @@
+<template>
+  <nav class="node-view-child-list nav-list" :class="{ 'smartphone': smartphone }">
+    <ul>
+      <li
+        v-for="child in children" :key="child.id"
+        class="node-view-child-list-item"
+      >
+        <dot-button
+          @click="onOpen(child.id)"
+          :variant="child.variant"
+          :active="activeNodes.includes(child.id)"
+        >
+          {{ $t('variants.' + child.variant) }}
+        </dot-button>
+      </li>
+    </ul>
+  </nav>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+
+export default {
+  name: 'NodeViewChildList',
+
+  props: {
+    children: { type: Array, required: true },
+    parentId: { type: Number, required: true },
+    smartphone: { type: Boolean, default: false }
+  },
+
+  data () {
+    return {
+    }
+  },
+
+  computed: {
+    ...mapGetters(['activeNodes']),
+  },
+
+  methods: {
+    onOpen (childId) {
+      this.$store.dispatch('OPEN_NODE', [this.parentId, childId])
+    }
+  },
+
+  mounted () {
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.node-view-child-list {
+  &-item {
+    margin-right: 20px;
+  }
+
+  &.smartphone {
+    padding: $node-view-spacer-sm-y $node-view-spacer-sm-x;
+    background-color: $white;
+    position: sticky;
+    top: 0;
+    z-index: 1;
+
+    ul {
+      justify-content: space-between;
+    }
+
+    @include media-breakpoint-up(md) {
+      display: none;
+    }
+  }
+}
+</style>

+ 86 - 0
src/components/nodes/NodeViewFooter.vue

@@ -0,0 +1,86 @@
+<template>
+  <div
+    class="node-view-footer" :class="'node-view-footer-' + mode"
+  >
+    <div class="d-flex w-100">
+      <div class="tags">
+        <b-badge
+          v-for="tag in node.tags" :key="tag.id"
+          variant="dark" pill
+        >
+          {{ tag.name }}
+        </b-badge>
+      </div>
+
+      <div v-if="mode === 'view' && node.siblings">
+        <b-button :id="'siblings-' + node.id">
+          {{ $t('siblings') }}
+        </b-button>
+
+        <b-popover
+          :target="'siblings-' + node.id" triggers="hover" placement="top"
+        >
+          <div v-for="sibling in node.siblings" :key="sibling.id">
+            <h6>
+              <a @click.prevent="onOpen(sibling.id)" href="#">
+                <div class="">
+                  {{ toCommaList(sibling.authors) }},
+                </div>
+                <div class="">
+                  {{ sibling.title }},
+                </div>
+                <div class="">
+                  {{ sibling.edition ? sibling.edition.name : sibling.edition }}
+                </div>
+              </a>
+            </h6>
+          </div>
+        </b-popover>
+      </div>
+
+      <div v-if="mode === 'card'">
+        <b-button @click="onOpen(node.id)" variant="outline-dark">
+          {{ $t('text.read') }}
+        </b-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { toCommaList } from '@/helpers/common'
+
+
+export default {
+  name: 'NodeViewFooter',
+
+  props: {
+    node: { type: Object, required: true },
+    type: { type: String, required: true },
+    mode: { type: String, required: true }
+  },
+
+  computed: {
+    authors () {
+      const authors = this.node.authors
+      if (!authors) return 'Pas d\'auteur⋅rices'
+      return authors.map(({ name }) => name).join(', ')
+    }
+  },
+
+  methods: {
+    toCommaList,
+
+    onOpen (nodeId) {
+      this.$store.dispatch('OPEN_NODE', [nodeId])
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.node-view-footer {
+  margin-top: auto;
+  font-family: $font-family-base;
+}
+</style>

+ 92 - 0
src/components/nodes/NodeViewHeaderProd.vue

@@ -0,0 +1,92 @@
+<template>
+  <div
+    class="node-view-header-prod" :class="'node-view-header-' + mode"
+  >
+    <div class="d-flex w-100">
+      <h4 class="mr-auto">
+        <div class="node-view-header-type">
+          {{ $t('variants.' + node.variant) }} {{ mode === 'card' ? $t('from') : '' }}
+        </div>
+
+        <div v-if="mode === 'card'" class="node-view-header-parent">
+          <!-- FIXME display parent title and authors -->
+          {{ parent }}
+        </div>
+      </h4>
+
+      <button-close v-if="mode === 'view'" @click="onClose()" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { trim, toCommaList } from '@/helpers/common'
+
+export default {
+  name: 'NodeViewHeaderProd',
+
+  props: {
+    node: { type: Object, required: true },
+    mode: { type: String, required: true }
+  },
+
+  computed: {
+    parent () {
+      if (!this.node || !this.node.parents) return
+      return this.node.parents.find(parent => parent.variant === 'depart')
+    }
+  },
+
+  methods: {
+    trim,
+    toCommaList,
+
+    onClose () {
+      this.$store.dispatch('CLOSE_NODE', this.node.id)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.node-view-header {
+  &-prod {
+    font: {
+      family: $font-family-base;
+      font-weight: $font-weight-bold;
+      size: inherit;
+      line-height: inherit;
+    }
+
+    @each $color, $value in $theme-colors {
+      .node-view-#{$color} & {
+        color: darken($value, 32%);
+      }
+    }
+  }
+
+  &-parent {
+    font-family: $font-family-text;
+  }
+
+  &-card {
+    h4 {
+      font-size: 0.8rem;
+
+      @include media-breakpoint-up(sm) {
+        font-size: 1.25rem;
+      }
+    }
+  }
+
+  &-view {
+    h4 {
+      font-size: 1rem;
+
+      @include media-breakpoint-up(sm) {
+        font-size: 1.4525rem;
+      }
+    }
+  }
+}
+</style>

+ 124 - 0
src/components/nodes/NodeViewHeaderRef.vue

@@ -0,0 +1,124 @@
+<template>
+  <div
+    class="node-view-header-ref" :class="'node-view-header-' + mode"
+  >
+    <div class="d-flex w-100">
+      <h4 class="mr-auto">
+        <strong class="d-block">
+          {{ toCommaList(node.authors) }},
+        </strong>
+        <span v-if="node.preTitle" v-html="node.preTitle + ','" />
+        <em v-html="(trim(node.title) || 'pas de titre ital') + ','" />
+        <div class="edition">
+          {{ node.edition.map(ed => ed.name).join(' ') }}
+        </div>
+      </h4>
+
+      <node-view-child-list v-if="mode === 'view'" :children="node.children" :parent-id="node.id" />
+      <button-close v-if="mode === 'view'" @click="onClose()" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { trim, toCommaList } from '@/helpers/common'
+import { NodeViewChildList } from '@/components/nodes'
+
+
+export default {
+  name: 'NodeViewHeaderRef',
+
+  components: {
+    NodeViewChildList
+  },
+
+  props: {
+    node: { type: Object, required: true },
+    mode: { type: String, required: true }
+  },
+
+  methods: {
+    trim,
+    toCommaList,
+
+    onClose () {
+      this.$store.dispatch('CLOSE_NODE', this.node.id)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.node-view-header {
+  &-ref {
+    font: {
+      family: $font-family-text;
+      size: inherit;
+      line-height: inherit;
+    }
+
+    h4 {
+      font-weight: $font-weight-base;
+      font-size: 32px;
+
+      .authors {
+        font-weight: $font-weight-bold;
+      }
+    }
+  }
+
+
+  // ╭─╴╭─┐┌─╮┌─╮
+  // │  ├─┤├┬╯│ │
+  // ╰─╴╵ ╵╵ ╰└─╯
+
+  &-card {
+    h4 {
+      font-size: 1.1rem;
+
+      .edition {
+        font-size: 0.65rem;
+      }
+
+      @include media-breakpoint-up(sm) {
+        font-size: 2rem;
+
+        .edition {
+          font-size: 1.15rem;
+        }
+      }
+    }
+  }
+
+
+  // ╷ ╷╶┬╴┌─╴╷╷╷
+  // │╭╯ │ ├─╴│││
+  // ╰╯ ╶┴╴╰─╴╰╯╯
+
+  &-view {
+    h4 {
+      font-size: 1.25rem;
+
+      .edition {
+        font-size: 0.75rem;
+      }
+
+      @include media-breakpoint-up(sm) {
+        font-size: 2.625rem;
+
+        .edition {
+          font-size: 1.5rem;
+        }
+      }
+    }
+
+    .node-view-child-list {
+      display: none;
+
+      @include media-breakpoint-up(md) {
+        display: block;
+      }
+    }
+  }
+}
+</style>

+ 6 - 0
src/components/nodes/index.js

@@ -0,0 +1,6 @@
+export { default as NodeViewChildList } from './NodeViewChildList'
+export { default as NodeViewHeaderRef } from './NodeViewHeaderRef'
+export { default as NodeViewHeaderProd } from './NodeViewHeaderProd'
+export { default as NodeViewBody } from './NodeViewBody'
+export { default as NodeViewFooter } from './NodeViewFooter'
+export { default as NodeView } from './NodeView'