✨ (notes) add routing between notes
This commit is contained in:
@@ -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
8
src/bus/noteBusEvent.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createEventBus } from 'retrobus'
|
||||||
|
|
||||||
|
interface EventBusParams {
|
||||||
|
path: string
|
||||||
|
currentNoteSHA?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const noteEventBus = createEventBus<EventBusParams>('note')
|
||||||
40
src/components/StackedNote.vue
Normal file
40
src/components/StackedNote.vue
Normal 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
30
src/hooks/useFile.hook.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/hooks/useMarkdown.hook.ts
Normal file
20
src/hooks/useMarkdown.hook.ts
Normal 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
98
src/hooks/useNote.hook.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,32 @@
|
|||||||
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 () => {
|
||||||
|
try {
|
||||||
const README = await request('GET /repos/{owner}/{repo}/readme', {
|
const README = await request('GET /repos/{owner}/{repo}/readme', {
|
||||||
repo,
|
repo,
|
||||||
owner
|
owner
|
||||||
})
|
})
|
||||||
|
|
||||||
if (README) {
|
if (README) {
|
||||||
readme.value = md.render(
|
readme.value = render(README.data.content)
|
||||||
decodeURIComponent(escape(atob(README.data.content)))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const commits = await request('GET /repos/{owner}/{repo}/commits', {
|
const commits = await request('GET /repos/{owner}/{repo}/commits', {
|
||||||
@@ -31,7 +40,7 @@ export const useRepo = (owner: string, repo: string) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const tree = await request(
|
const treeResponse = await request(
|
||||||
'GET /repos/{owner}/{repo}/git/trees/{tree_sha}',
|
'GET /repos/{owner}/{repo}/git/trees/{tree_sha}',
|
||||||
{
|
{
|
||||||
owner,
|
owner,
|
||||||
@@ -41,10 +50,18 @@ export const useRepo = (owner: string, repo: string) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log(tree.data.tree.filter((t) => t.type === 'blob'))
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
51
src/views/Note.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user