Skip to content

Instantly share code, notes, and snippets.

@ever-dev
Last active October 8, 2020 18:07
Show Gist options
  • Save ever-dev/6b123eaf9b800ddc41dae800d353b17f to your computer and use it in GitHub Desktop.
Save ever-dev/6b123eaf9b800ddc41dae800d353b17f to your computer and use it in GitHub Desktop.
Dynamic Page Builder for documentation.
<template>
<section :class="content.type" fluid class="content-container">
<!-- Content Header -->
<v-row>
<v-col v-if="content.showTitle" cols="12">
<p class="heading text-left text-xs-center d-flex align-center mb-5">
<v-icon
v-if="content.type === 'changelog'"
:class="{ open: collapse }"
@click="toggleCollapse()"
class="mr-3"
>
mdi-chevron-down
</v-icon>
<markdown-view :content="content.title"></markdown-view>
<a
v-if="
content.type === 'titleWithAction' &&
content.target.startsWith('http')
"
:href="content.target"
>
<Button width="130" rounded class="ml-3" track-id="Title Button">
{{ content.content }}
</Button>
</a>
<nuxt-link
v-if="
content.type === 'titleWithAction' &&
!content.target.startsWith('http')
"
:to="content.target"
>
<Button width="130" rounded class="ml-3" track-id="Title Button">
{{ content.content }}
</Button>
</nuxt-link>
</p>
</v-col>
<!-- !Content Header -->
<v-col v-if="content.description" cols="12" class="description mb-5">
<markdown-view :content="content.description"></markdown-view>
</v-col>
<!-- Content Contents -->
<!-- link blocks -->
<template v-if="content.type === 'link block'">
<v-col
v-for="(item, idx) in content.content"
:key="idx"
cols="12"
md="4"
class="content-block"
>
<link-block :content="item" />
</v-col>
</template>
<!-- markdown text -->
<template
v-else-if="
content.type === 'markdown' ||
content.type === 'sub-content' ||
content.type === 'sub-content-header'
"
>
<v-col
:class="{
'sub-content': content.type === 'sub-content',
'sub-content-header': content.type === 'sub-content-header'
}"
cols="12"
class="content-block"
>
<markdown-view :content="content.content"></markdown-view>
</v-col>
</template>
<!-- change log -->
<template v-else-if="content.type === 'changelog'">
<v-col v-if="!collapse" cols="12" class="content-block">
<markdown-view :content="content.content"></markdown-view>
</v-col>
</template>
<!-- tutorial -->
<template v-else-if="content.type === 'tutorial'">
<v-col
v-for="(item, idx) in content.content"
:key="idx"
cols="12"
class="content-block"
>
<v-row>
<v-col cols="12"> <v-img :src="item.src"></v-img></v-col>
<v-col cols="12">
<markdown-view :content="item.description"></markdown-view>
</v-col>
</v-row>
</v-col>
</template>
<!-- video tutorial -->
<template v-else-if="content.type === 'video tutorial'">
<v-col
v-for="(item, idx) in content.content"
:key="idx"
cols="12"
class="content-block"
>
<v-row>
<v-col :md="item.description ? 8 : 12" cols="12">
<div class="video">
<client-only>
<youtube :video-id="item.src" :fitParent="true"></youtube>
</client-only>
</div>
</v-col>
<v-col v-if="item.description" md="4" cols="12">
<markdown-view :content="item.description"></markdown-view>
</v-col>
</v-row>
</v-col>
</template>
<!-- diagram explanation -->
<template v-else-if="content.type === 'diagram explanation'">
<v-col
v-for="(item, idx) in content.content"
:key="idx"
cols="12"
class="content-block"
>
<diagram-explanation :content="item"></diagram-explanation>
</v-col>
</template>
</v-row>
</section>
<!-- !Content Contents -->
</template>
<style lang="scss" scoped>
.content-container {
padding: 0;
width: 100%;
max-width: 700px;
&.changelog {
.heading {
font-weight: normal;
font-size: 18px;
line-height: 25px;
}
.content-block {
background-color: var(--theme-quote-bg);
padding: 25px 30px;
margin-left: 12px;
}
}
}
.content-block {
margin-bottom: 50px;
&.sub-content-header,
&.sub-content {
margin-bottom: 30px;
}
}
.heading {
margin-bottom: 8px;
line-height: 50px;
font-weight: normal;
@media (max-width: 960px) {
font-size: 26px;
line-height: 40px;
}
.markdown-body > ::v-deep p {
font-size: 20px;
font-weight: bold;
em {
font-size: 18px;
font-style: normal;
font-weight: normal;
color: var(--theme-text--active);
}
}
}
.video {
> * {
width: 100%;
}
}
.description {
font-size: 16px;
line-height: 14px;
font-weight: 300;
padding: 0 12px;
}
.v-icon {
outline: none;
color: var(--theme-text);
}
.v-icon.open {
transform: rotate(-90deg);
}
.col {
padding-top: 0;
padding-bottom: 0;
}
@media (max-width: 960px) {
.row {
margin-top: -6px;
margin-bottom: -6px;
}
.col-12 {
padding-top: 6px;
padding-bottom: 6px;
}
}
</style>
<script>
import MarkdownView from '../MarkdownView'
import Button from '../Button'
import LinkBlock from './LinkBlock'
import DiagramExplanation from './DiagramExplanation'
export default {
components: {
LinkBlock,
MarkdownView,
DiagramExplanation,
Button
},
props: {
content: {
type: Object,
required: true
},
order: {
type: Number,
default: 0
}
},
data() {
return {
collapse: true // collapsable when it's changelog
}
},
mounted() {
this.collapse = this.order !== 1
},
methods: {
toggleCollapse() {
this.collapse = !this.collapse
}
}
}
</script>
<template>
<div class="docs-content d-flex">
<v-container class="page-container">
<div class="content-wrapper">
<!-- Next Page -->
<!-- <nuxt-link
v-if="data.nextPage"
:to="getPageURL(data.nextPage)"
class="next-page__link"
>
{{ data.nextPage }}
<v-icon>
mdi-chevron-right
</v-icon>
</nuxt-link> -->
<!-- !Next Page -->
<!-- Page Title -->
<v-row>
<v-col cols="12" class="d-flex align-center justify-space-between">
<h1>{{ data.pageTitle }}</h1>
</v-col>
</v-row>
<!-- !Page Title -->
<!-- Breadcurmbs -->
<!-- <v-row v-if="data.breadCrumb && data.breadCrumb.length">
<v-col cols="12" class="d-flex align-center breadcrumb">
<template v-for="(item, index) in data.breadCrumb">
<div :key="index" class="d-flex align-center">
<nuxt-link
:to="item.to.startsWith('/') ? item.to : getPageURL(item.to)"
class="breadcrumb-link"
>
{{ item.text }}
</nuxt-link>
<v-icon v-if="index < data.breadCrumb.length - 1">
mdi-chevron-right
</v-icon>
</div>
</template>
</v-col>
</v-row> -->
<!-- !Breadcurmbs -->
<v-row v-if="data.description" class="mt-5">
<v-col cols="12">
<markdown-view :content="data.description"></markdown-view>
</v-col>
</v-row>
<v-row>
<v-col cols="12" class="space"></v-col>
</v-row>
<template v-for="(item, idx) in data.content">
<content-builder
:key="idx"
:order="idx"
:class="{ 'hidden-sm-and-down': !item.showMobile }"
:content="item"
:id="item.contentLink"
v-intersect="
item.contentLink && {
handler: onIntersect,
options: {
threshold: [0, 0.5, 1.0]
}
}
"
></content-builder>
</template>
</div>
</v-container>
<!--- Begin: Documentation Contents Bar --->
<v-navigation-drawer right class="docs-contents hidden-sm-and-down">
<template v-if="data.contentLinks && data.contentLinks.length">
<no-ssr>
<VuePerfectScrollbar class="h-100">
<ul class="documentation-toc">
<li class="contents-heading">Contents</li>
<li
v-for="(link, index) in data.contentLinks"
:key="index"
class="documentation-toc__link"
>
<a
v-ripple
@click="$vuetify.goTo(link.to)"
:title="link.text"
:class="{ active: link.to === `#${currentSection}` }"
class="contents-link"
>
{{ link.text }}
</a>
<ul v-if="link.children" class="documentation-toc">
<li
v-for="(subLink, subIndex) in link.children"
:key="subIndex"
class="documntation-toc__link"
>
<a
v-ripple
@click="$vuetify.goTo(subLink.to)"
:title="subLink.text"
:class="{ active: subLink.to == `#${currentSection}` }"
class="contents-link"
>
{{ subLink.text }}
</a>
</li>
</ul>
</li>
</ul>
</VuePerfectScrollbar>
</no-ssr>
</template>
</v-navigation-drawer>
<!--- End: Documentation Contents Bar --->
</div>
</template>
<style lang="scss" scoped>
.page-container {
padding: 100px 48px 30vh 64px;
position: relative;
background-color: var(--theme-block-bg1);
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
z-index: 1;
max-width: calc(100% - 260px);
margin: 0;
@media (max-width: 960px) {
padding: 50px 60px 0 50px;
max-width: 100%;
}
@media (max-width: 600px) {
padding: 50px 30px 0 30px;
}
.content-wrapper {
max-width: min(700px, 100%);
margin: 0 auto;
}
}
.space {
padding-top: 0;
padding-bottom: 0;
margin-bottom: 20px;
}
.breadcrumb {
padding-top: 0;
padding-bottom: 0;
}
.breadcrumb-link {
font-size: 16px;
line-height: 22px;
color: var(--theme-text);
&.nuxt-link-exact-active {
color: var(--theme-text--active);
}
}
.docs-content {
mix-blend-mode: normal;
width: 100%;
}
.docs-contents {
// min-width: 256px;
width: 260px !important;
padding: 80px 20px;
background-color: var(--theme-block-bg1);
/* sticky navigation */
height: calc(100vh - 58px) !important;
top: 58px !important;
position: sticky;
.documentation-toc {
.documentation-toc__link {
.contents-link.active {
color: var(--theme-text--active);
font-weight: bold;
}
}
.contents-heading {
font-size: 24px;
line-height: 32px;
margin-bottom: 10px;
}
.contents-link {
font-size: 16px;
line-height: 22px;
color: var(--theme-text);
margin-bottom: 5px;
display: block;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
.next-page__link {
position: absolute;
top: 120px;
right: 48px;
color: var(--theme-page-link);
.v-icon {
color: var(--theme-page-link);
}
@media (max-width: 960px) {
top: 40px;
right: 28px;
}
}
.documentation-toc {
list-style: none;
padding: 0 1em;
}
</style>
<script>
import MarkdownView from '../MarkdownView'
import ContentBuilder from './ContentBuilder'
export default {
components: {
ContentBuilder,
MarkdownView
},
props: {
data: {
type: Object,
required: true
}
},
data() {
return {
sections: {},
currentSection: ''
}
},
watch: {
sections(value) {
let maxKey = 0
Object.keys(value).forEach((item) => {
maxKey = value[item] > (value[maxKey] || 0) ? item : maxKey
})
this.currentSection = maxKey
}
},
mounted() {
this.data.contentLinks.forEach((element) => {
this.sections[element.to.substr(1)] = 0
if (element.children) {
element.children.forEach(
(child) => (this.sections[child.to.substr(1)] = 0)
)
}
})
},
methods: {
onIntersect(entries) {
this.sections = {
...this.sections,
[entries[0].target.id]: entries[0].intersectionRatio
}
},
getPageURL(pageName) {
return (
this.$store.state.pages.pages[pageName] &&
this.$store.state.pages.pages[pageName].url
)
}
}
}
</script>
<template>
<client-only>
<div v-html="html" class="markdown-body"></div>
</client-only>
</template>
<style lang="scss">
.markdown-body {
color: var(--theme-text);
// font-family: 'Europa Regular';
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
& > :first-child {
margin-top: 0 !important;
}
& > :last-child {
margin-bottom: 0 !important;
}
& > * {
margin-bottom: 16px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 16px;
margin-top: 24px;
border: none;
font-family: 'Nunito Sans';
font-weight: 600;
line-height: 1.25;
color: var(--theme-text);
}
h3 {
font-size: 1.25em;
}
code {
margin: 0;
padding: 0.2em 0.4em;
font-size: 14px;
letter-spacing: 0;
color: var(--theme-text-code);
box-shadow: none;
background-color: white;
border: 1px solid #e5e5e5;
font-family: 'Courier New';
font-weight: normal;
}
p > code[block] {
max-width: 545px;
width: 100%;
padding: 1.5em;
color: var(--theme-text);
}
a {
// color: var(--theme-text);
// font-family: 'Europa Regular';
color: var(--theme-text--active);
font-weight: bold;
// text-decoration: underline;
}
strong {
font-weight: 600;
}
table {
width: 100%;
display: block;
overflow: auto;
border-collapse: collapse;
border-spacing: 0;
tr {
background-color: transparent;
}
td,
th {
border: 1px solid #aeaeae;
padding: 6px 13px;
}
th {
font-weight: bold;
}
}
blockquote {
padding: 10px 20px;
color: var(--theme-text);
background-color: var(--theme-quote-bg);
border-left: 0.25em solid #dfe2e5;
max-width: 545px;
& > :first-child {
margin-top: 10px !important;
}
& > :last-child {
margin-bottom: 10px !important;
}
code {
color: var(--theme-text);
}
}
p {
// font-family: 'Europa Regular';
font-size: 16px;
line-height: 20px;
font-weight: 400;
margin-bottom: 10px;
}
ul {
padding-left: 1em;
li {
// font-family: 'Europa Regular';
font-weight: 300;
font-size: 16px;
line-height: 24px;
}
}
li {
// font-family: 'Europa Regular';
font-size: 16px;
line-height: 22px;
font-weight: 300;
& + li {
margin-top: 0.25em;
}
}
svg {
width: 100%;
}
img {
background-color: transparent;
display: block;
margin: 30px 0 10px;
max-width: 100%;
& + em {
display: block;
font-style: italic;
color: var(--theme-text--em);
}
}
}
</style>
<script>
import marked from 'marked'
export default {
props: {
content: {
type: String,
required: true
}
},
data() {
return {
html: marked(this.content)
}
}
}
</script>
{
"pageName": "PerceptiLabs Code of Conduct",
"pageTitle": "PerceptiLabs Code of Conduct",
"description": `Welcome to the PerceptiLabs community! We're glad you've joined us.`,
"content": [
{
"title": "",
"showMobile": true,
"contentLink": "",
"subTitle": "",
"description": "",
"showTitle": true,
"type": "markdown",
"content": `
Together we can build an amazing community that’s helpful and friendly.
The following participation guidelines help to ensure that the intent and purpose of the community is met to the fullest possible extent.
> ### **Note:**
> This scope of this policy also applies outside the "context" of the community where a person is representing PerceptiLabs as a whole including,
but not limited to: emails, in-person events, social media, message chats, and commit messages (e.g., on GitHub).
`
},
{
"title": "Expected Behavior",
"showMobile": true,
"contentLink": "expected-behavior",
"subTitle": "",
"description": "",
"showTitle": true,
"type": "markdown",
"content": `
- Participate in an authentic, active, and collaborative way.
- Be kind, welcoming, and respectful of all community members regardless of their background including, but not limited to their: race, gender, orientation, and religious beliefs.
- Work towards and focus on collaboration instead of conflict.
- Guide conversations toward issue resolution.
`
},
{
"title": "Friendly and Safe Participation",
"showMobile": true,
"contentLink": "participation",
"subTitle": "",
"description": "",
"showTitle": true,
"type": "markdown",
"content": `
- Maintain a friendly and professional tone in all communications.
- Exercise consideration and respect in your speech and actions.
- Critique ideas, not the people behind them.
- Review all content before posting/replying to ensure it meets the requirements outlined in this code of conduct.
`
},
{
"title": "Good Etiquette/Encouraged Behaviours",
"showMobile": true,
"contentLink": "e-behaviors",
"subTitle": "",
"description": "",
"showTitle": true,
"type": "markdown",
"content": `
- Try to ensure conversations/threads come to a close (e.g., don't leave questions unanswered).
- Post questions/start conversations in the appropriate channels best related to the topic of interest (e.g., appropriate forum threads, correct Slack channels, etc.).
- Try to understand different perspectives.
- Remember that other members may have different levels of knowledge, so be sure to provide context and background information when possible.
`
},
{
"title": "Unacceptable Behavior",
"showMobile": true,
"contentLink": "unacceptable-behaviors",
"subTitle": "",
"description": "",
"showTitle": true,
"type": "markdown",
"content": `
- Name-calling, personal attacks of any sort, and bullying.
- Demeaning, discriminatory, or harassing behavior and speech. This includes, but isn’t limited to: intimidation, vulgar/derogatory language, direct or indirect threats, sexually suggestive remarks, patterns of inappropriate social contact, trolling, and sustained disruptions of discussion.
- Retaliation against other community members. Instead, work to resolve the issue.
- Posting "spam" including but not limited to: marketing, promotional content, and advertising of any kind.
- Using any form of device or computer program that enables automated submission of postings without consent of PerceptiLabs.
- Posting content or links to content that is illegal or violates any applicable local or national laws.
- Posting or spreading viruses, worms, malware, or other software intended to harm another user's computer.
- Influencing, encouraging, or leading any of the unacceptable behaviors listed above.
`
},
{
"title": "Privacy",
"showMobile": true,
"contentLink": "privacy",
"subTitle": "",
"description": "",
"showTitle": true,
"type": "markdown",
"content": `
- Do not post any confidential information related to a company or an individual including but not limited to: credentials, API keys, and other sensitive information.
- Verify that your content does not violate copyright or intellectual property entitlements.
`
},
{
"title": "Reporting Guidelines",
"showMobile": true,
"contentLink": "reporting-guidelines",
"subTitle": "",
"description": "",
"showTitle": true,
"type": "markdown",
"content": `
If you are subject to or witness unacceptable behavior, or have any other concerns, please notify us as soon as possible by emailing <todo>.
`
},
{
"title": "Enforcement",
"showMobile": true,
"contentLink": "enforcement",
"subTitle": "",
"description": "",
"showTitle": true,
"type": "markdown",
"content": `
Our community is a moderated space and all members are required to comply with the aforementioned code of conduct. Failure to do so may result in the restriction of further participation.
`
}
],
"contentLinks": [
{ "text": "Expected Behavior", "to": "#expected-behavior" },
{ "text": "Friendly and Safe Participation", "to": "#participation" },
{ "text": "Good Etiquette/Encouraged Behaviours", "to": "#e-behaviors" },
{ "text": "Unacceptable Behavior", "to": "#unacceptable-behavior" },
{ "text": "Privacy", "to": "#privacy" },
{ "text": "Reporting Guidelines", "to": "#reporting-guidelines" },
{ "text": "Enforcement", "to": "#enforcement" }
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment