(notes) add routing between notes

This commit is contained in:
2021-03-13 22:11:58 +01:00
parent 2bb43ac961
commit 8fad931dfd
12 changed files with 343 additions and 65 deletions

View File

@@ -1,9 +1,9 @@
<template> <template>
<div id="app"> <div id="app">
<nav> <!-- <nav>
<router-link to="/">Home</router-link> | <router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> <router-link to="/about">About</router-link>
</nav> </nav> -->
<router-view /> <router-view />
</div> </div>
</template> </template>

8
src/bus/noteBusEvent.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createEventBus } from 'retrobus'
interface EventBusParams {
path: string
currentNoteSHA?: string
}
export const noteEventBus = createEventBus<EventBusParams>('note')

View File

@@ -0,0 +1,40 @@
<template>
<div class="stacked-note" v-html="content"></div>
</template>
<script lang="ts">
import { defineComponent, nextTick, watch } from 'vue'
import { useFile } from '@/hooks/useFile.hook'
import { useLinks } from '@/hooks/useLinks.hook'
export default defineComponent({
name: 'StackedNote',
props: {
user: { type: String, required: true },
repo: { type: String, required: true },
sha: { type: String, required: true }
},
setup(props) {
const { content } = useFile(props.user, props.repo, props.sha)
const { listenToClick } = useLinks('stacked-note', props.sha)
watch(content, () => {
if (content.value) {
nextTick(() => {
listenToClick()
})
}
})
return { content }
}
})
</script>
<style lang="scss" scoped>
.stacked-note {
text-align: left;
border-left: 1px solid rgba(18, 19, 58, 0.2);
padding-left: 1rem;
}
</style>

30
src/hooks/useFile.hook.ts Normal file
View File

@@ -0,0 +1,30 @@
import { ref } from 'vue'
import { request } from '@octokit/request'
import { useMarkdown } from '@/hooks/useMarkdown.hook'
export const useFile = (owner: string, repo: string, sha: string) => {
const content = ref('')
const getContent = async () => {
const { render } = useMarkdown()
const file = await request(
'GET /repos/{owner}/{repo}/git/blobs/{file_sha}',
{
owner,
repo,
file_sha: sha
}
)
if (!file) {
return
}
content.value = render(file.data.content)
}
getContent()
return {
content
}
}

View File

@@ -1,7 +1,27 @@
export const useLinks = (className: string) => { import { noteEventBus } from '@/bus/noteBusEvent'
const linkNote: EventListenerOrEventListenerObject = (e) => { import { onUnmounted } from '@vue/runtime-core'
e.preventDefault()
console.log('use links') const LINKS = ['http://', 'https://']
export const useLinks = (className: string, sha?: string) => {
const linkNote: EventListener = (event) => {
event.preventDefault()
const target = event.target as HTMLElement
const href = target.getAttribute('href')
if (!href) {
return
}
if (LINKS.some((link) => href.startsWith(link))) {
window.open(href, '_blank')
return
}
noteEventBus.emit({
path: href,
currentNoteSHA: sha
})
} }
const selector = `.${className} a` const selector = `.${className} a`
@@ -21,8 +41,11 @@ export const useLinks = (className: string) => {
}) })
} }
onUnmounted(() => {
removeListeners()
})
return { return {
listenToClick, listenToClick
removeListeners
} }
} }

View File

@@ -0,0 +1,20 @@
import MarkdownIt from 'markdown-it'
import markdownItClass from '@toycode/markdown-it-class'
// import sanitizeHtml from 'sanitize-html'
const md = new MarkdownIt().use(markdownItClass, {
h1: ['title', 'is-1'],
h2: ['subtitle', 'is-2'],
h3: ['subtitle', 'is-3'],
h4: ['subtitle', 'is-4'],
h5: ['subtitle', 'is-5'],
h6: ['subtitle', 'is-6'],
table: ['table', 'is-striped', 'is-hoverable', 'is-fullwidth']
})
export const useMarkdown = () => {
return {
render: (content: string) =>
md.render(decodeURIComponent(escape(atob(content))))
}
}

98
src/hooks/useNote.hook.ts Normal file
View File

@@ -0,0 +1,98 @@
import { nextTick, onUnmounted, watch } from '@vue/runtime-core'
import { useRoute, useRouter } from 'vue-router'
import { noteEventBus } from '@/bus/noteBusEvent'
import { ref } from '@vue/reactivity'
import { useLinks } from '@/hooks/useLinks.hook'
import { useRepo } from '@/hooks/useRepo.hook'
const sanitizePath = (path: string) => {
if (path.startsWith('./')) {
return decodeURIComponent(path.replace('./', ''))
}
return decodeURIComponent(path)
}
export const useNote = (user: string, repo: string) => {
const { push } = useRouter()
const { readme, notFound, tree } = useRepo(user, repo)
const { listenToClick } = useLinks('note-display')
const { query } = useRoute()
const stackedNotes = ref(
query.stackedNotes
? Array.isArray(query.stackedNotes)
? query.stackedNotes
: [query.stackedNotes]
: []
)
const unsubscribe = noteEventBus.addEventBusListener(
({ path, currentNoteSHA }) => {
const currentFile = tree.value.find((file) => file.sha === currentNoteSHA)
const absolutePathArray = currentFile?.path?.split('/') ?? []
absolutePathArray?.pop()
const absolutePath = absolutePathArray.join('/')
const finalPath = absolutePath
? `${sanitizePath(absolutePath)}/${sanitizePath(path)}`
: sanitizePath(path)
const file = tree.value.find((file) => file.path === finalPath)
if (!file?.sha || stackedNotes.value.includes(file.sha)) {
return
}
const getStackedNotes = () => {
if (!file?.sha) {
return []
}
if (!currentNoteSHA) {
return [file.sha]
}
const [splittedStackedNotes] = stackedNotes.value
.join(';')
.split(currentNoteSHA)
return [
...splittedStackedNotes.replaceAll(';;', ';').split(';'),
currentNoteSHA,
file.sha
].filter((sha) => !!sha)
}
const newStackedNotes = getStackedNotes()
push({
name: 'Note',
query: {
stackedNotes: newStackedNotes
}
})
stackedNotes.value = newStackedNotes
}
)
watch(readme, () => {
if (readme.value) {
nextTick(() => {
listenToClick()
})
}
})
onUnmounted(() => {
unsubscribe()
})
return {
readme,
notFound,
stackedNotes
}
}

View File

@@ -1,50 +1,67 @@
import { onMounted, ref } from '@vue/runtime-core' import { onMounted, ref } from '@vue/runtime-core'
import MarkdownIt from 'markdown-it'
import { request } from '@octokit/request' import { request } from '@octokit/request'
import { useMarkdown } from '@/hooks/useMarkdown.hook'
const md = new MarkdownIt() interface Tree {
path?: string
mode?: string
type?: string
sha?: string
size?: number
url?: string
}
export const useRepo = (owner: string, repo: string) => { export const useRepo = (owner: string, repo: string) => {
const { render } = useMarkdown()
const readme = ref<string | null>(null) const readme = ref<string | null>(null)
const notFound = ref(false)
const tree = ref<Tree[]>([])
onMounted(async () => { onMounted(async () => {
const README = await request('GET /repos/{owner}/{repo}/readme', { try {
repo, const README = await request('GET /repos/{owner}/{repo}/readme', {
owner
})
if (README) {
readme.value = md.render(
decodeURIComponent(escape(atob(README.data.content)))
)
}
const commits = await request('GET /repos/{owner}/{repo}/commits', {
owner,
repo
})
const lastCommit = commits.data.shift()
if (!lastCommit) {
return
}
const tree = await request(
'GET /repos/{owner}/{repo}/git/trees/{tree_sha}',
{
owner,
repo, repo,
tree_sha: lastCommit.commit.tree.sha, owner
recursive: 'true' })
}
)
console.log(tree.data.tree.filter((t) => t.type === 'blob')) if (README) {
readme.value = render(README.data.content)
}
const commits = await request('GET /repos/{owner}/{repo}/commits', {
owner,
repo
})
const lastCommit = commits.data.shift()
if (!lastCommit) {
return
}
const treeResponse = await request(
'GET /repos/{owner}/{repo}/git/trees/{tree_sha}',
{
owner,
repo,
tree_sha: lastCommit.commit.tree.sha,
recursive: 'true'
}
)
if (treeResponse) {
tree.value = treeResponse.data.tree.filter((t) => t.type === 'blob')
console.log(tree.value)
}
} catch (error) {
notFound.value = true
}
}) })
return { return {
readme readme,
notFound,
tree
} }
} }

View File

@@ -1,4 +1,5 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue' import Home from '@/views/Home.vue'
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
@@ -11,6 +12,12 @@ const routes: Array<RouteRecordRaw> = [
path: '/about', path: '/about',
name: 'About', name: 'About',
component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue') component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue')
},
{
path: '/note/:user/:repo',
name: 'Note',
props: true,
component: () => import(/* webpackChunkName: "note" */ '@/views/Note.vue')
} }
] ]

View File

@@ -1,24 +1,12 @@
@charset "utf-8"; @charset "utf-8";
@import '~bulma/bulma.sass'; @import '~bulma/bulma.sass';
html {
overflow-y: auto;
}
html, html,
body { body {
text-align: center; text-align: center;
min-height: 100vh; min-height: 100vh;
} }
h1 {
font-size: 3rem;
}
h2 {
font-size: 2.5rem;
}
h3 {
font-size: 2rem;
}
h4 {
font-size: 1.5rem;
}

View File

@@ -5,7 +5,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, watch, onUnmounted, nextTick } from 'vue' import { defineComponent, watch, nextTick } from 'vue'
import { useRepo } from '@/hooks/useRepo.hook' import { useRepo } from '@/hooks/useRepo.hook'
import { useLinks } from '@/hooks/useLinks.hook' import { useLinks } from '@/hooks/useLinks.hook'
@@ -13,7 +13,7 @@ export default defineComponent({
name: 'Home', name: 'Home',
setup() { setup() {
const { readme } = useRepo('jcalixte', 'notes') const { readme } = useRepo('jcalixte', 'notes')
const { listenToClick, removeListeners } = useLinks('note') const { listenToClick } = useLinks('note')
watch(readme, () => { watch(readme, () => {
if (readme.value) { if (readme.value) {
@@ -23,10 +23,6 @@ export default defineComponent({
} }
}) })
onUnmounted(() => {
removeListeners()
})
return { return {
readme readme
} }

51
src/views/Note.vue Normal file
View File

@@ -0,0 +1,51 @@
<template>
<div class="note content">
<hr v-if="notFound" />
<div v-if="notFound" class="columns is-centered">
<div class="column is-one-third notification is-warning" v-if="notFound">
Not found.
</div>
</div>
<div class="columns">
<div class="column">
<h1 class="title is-1">
<router-link :to="{ name: 'Note' }">
{{ repo }}
</router-link>
</h1>
<h2 class="subtitle is-2">{{ user }}</h2>
<p class="note-display" v-html="readme"></p>
</div>
<div
class="column"
v-for="stackedNote in stackedNotes"
:key="stackedNote"
>
<stacked-note :user="user" :repo="repo" :sha="stackedNote" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue'
import { useNote } from '@/hooks/useNote.hook'
const StackedNote = defineAsyncComponent(() =>
import('@/components/StackedNote.vue')
)
export default defineComponent({
name: 'Home',
components: {
StackedNote
},
props: {
user: { type: String, required: true },
repo: { type: String, required: true }
},
setup(props) {
return useNote(props.user, props.repo)
}
})
</script>