Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(admin): Monaco, Resize panels, improved explorer #509

Merged
merged 13 commits into from
Jun 24, 2021
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dev": "ADMIN_DEV=true nuxt dev docs",
"dev:nuxtjs": "nuxt dev nuxtjs.org",
"dev:admin": "vite --config src/admin/app/vite.config.ts",
"build:admin": "vite build --config src/admin/app/vite.config.ts",
"generate": "nuxt generate --force-build docs",
"generate:nuxtjs": "nuxt generate --force-build nuxtjs.org",
"start": "nuxt start docs",
Expand Down Expand Up @@ -131,9 +132,12 @@
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-nuxt": "^2.0.0",
"eslint-plugin-prettier": "^3.4.0",
"monaco-editor": "^0.25.2",
"prettier": "^2.3.1",
"siroc": "^0.9.3",
"splitpanes": "^3.0.4",
"standard-version": "^9.3.0",
"vite": "^2.3.8",
"vite-plugin-components": "^0.11.2",
"vite-plugin-icons": "^0.6.3"
},
Expand Down
File renamed without changes.
27 changes: 8 additions & 19 deletions src/admin/api/functions/content.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { promises as fs } from 'fs'
import { join, extname } from 'path'
import matter from 'gray-matter'
import { createError, Middleware, useBody } from 'h3'
import dirTree from 'directory-tree'
import { FileData, File } from '../../type'
import { normalizeFiles, r } from '../utils'

interface Body {
data: any
content: string
}

export default <Middleware>async function contentHandler(req) {
const url = req.url

Expand All @@ -23,13 +18,11 @@ export default <Middleware>async function contentHandler(req) {
try {
const path = join(r('content'), url)
const file = await fs.readFile(path, 'utf-8')
const { content, data } = matter(file)

return {
return <File>{
path: path.replace(r('content'), ''),
extension: extname(path),
data,
content
raw: file
}
} catch (err) {
return createError({
Expand All @@ -41,24 +34,20 @@ export default <Middleware>async function contentHandler(req) {

// Update changes
if (req.method === 'PUT') {
const { data, content } = await useBody<Body>(req)

if (!data || !content) {
const { raw } = await useBody<FileData>(req)
if (raw == null) {
return createError({
statusCode: 400,
statusMessage: 'data and content keys are required'
statusMessage: '"raw" key is required'
})
}

const path = join(r('content'), url)

try {
// @ts-ignore
await fs.stat(path, 'utf-8')

const file = matter.stringify(content, data)

await fs.writeFile(path, file)
// await fs.stat(path, 'utf-8')
await fs.writeFile(path, raw)

return { ok: true }
} catch (err) {
Expand Down
13 changes: 1 addition & 12 deletions src/admin/app/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,9 @@
<Component :is="Component" />
</KeepAlive>
</RouterView>

<Preview />
</main>
</template>

<script lang="ts">
import { defineComponent } from 'vue3'
<script setup lang="ts">
import AppHeader from './components/AppHeader.vue'
import Preview from './components/Preview.vue'

export default defineComponent({
components: {
AppHeader,
Preview
}
})
</script>
87 changes: 25 additions & 62 deletions src/admin/app/components/Editor.vue
Original file line number Diff line number Diff line change
@@ -1,72 +1,35 @@
<template>
<textarea v-model="frontmatter" class="h-24 w-full font-mono px-4 py-2 d-border border-b outline-none text-sm" />
<textarea v-model="content" class="w-full h-full font-mono px-4 py-2 outline-none text-sm" />
<Monaco :value="raw" language="markdown" @change="update" />
</template>

<script lang="ts">
import YAML from 'js-yaml'
import { GrayMatterFile } from 'gray-matter'
import { defineComponent, computed, ref, watch, PropType } from 'vue3'
<script setup lang="ts">
import type { PropType } from 'vue3'
import { ref, watch, defineProps } from 'vue3'
import type { File } from '../../type'
import { useApi } from '../plugins/api'
import Monaco from './Monaco.vue'

type PermissiveGrayMatterFile = GrayMatterFile<any> & { file: any; path: any }

export default defineComponent({
props: {
file: {
type: Object as PropType<PermissiveGrayMatterFile>,
required: true
}
},
setup(props) {
const api = useApi()

// Sync local data when file changes
watch(
() => props.file,
newVal => {
data.value = newVal.data
content.value = newVal.content
}
)

// Local data
const data = ref(props.file.data)
const content = ref(props.file.content)

// Stringified reference for frontmatter text-area
const frontmatter = computed({
get() {
return YAML.dump(data.value, null, 2)
},
set(value: string) {
try {
data.value = YAML.parse(value)
} catch (e) {
// New value is not a valid YAML string.
// Do nothing and wait for the next valid YAML input.
}
}
})
const props = defineProps({
file: {
type: Object as PropType<File>,
required: true
}
})

// API update on change on data or content
watch([data, content], () => {
api.put(`/content${props.file.path}`, {
data: data.value,
content: content.value
})
})
const api = useApi()
const raw = ref(props.file.raw)

return {
frontmatter,
content
}
// Sync local data when file changes
watch(
() => props.file,
newVal => {
raw.value = newVal.raw
}
})
</script>
)

<style lang="postcss" scoped>
textarea {
@apply whitespace-nowrap overflow-scroll bg-transparent;
function update(content) {
api.put(`/content${props.file.path}`, {
raw: content
})
}
</style>
</script>
29 changes: 17 additions & 12 deletions src/admin/app/components/FilesTree.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<template>
<ul>
<li
v-for="file of files"
:key="file.path"
class="rounded"
:class="isCurrent(file) ? 'bg-gray-400 bg-opacity-20' : 'opacity-75'"
>
<li v-for="file of files" :key="file.path">
<div
v-if="!isHidden(file)"
class="
Expand All @@ -18,40 +13,50 @@
text-sm
leading-5
rounded
overflow-hidden
select-none
hover:bg-gray-400 hover:bg-opacity-15
"
:class="isCurrent(file) ? 'bg-gray-400 bg-opacity-20' : 'opacity-75'"
@click="open(file)"
>
<FilesTreeIcon :file="file" />
<span>{{ filename(file.name) }}</span>
<TreeToggler :file="file" />
<FilesTreeIcon class="w-8 min-w-8" :file="file" />
<div class="whitespace-nowrap overflow-ellipsis">{{ filename(file.name) }}</div>
</div>
<FilesTree
v-if="isDir(file) && file.isOpen"
:files="file.children"
:current-file="currentFile"
:is-root="false"
class="pl-2"
class="pl-3"
@open="open"
/>
</li>
</ul>
</template>

<script>
<script lang="ts">
import { defineComponent, provide } from 'vue3'
import type { PropType } from 'vue3'
import type { File } from '../../type'
import { isImage } from '../utils'
import FilesTreeIcon from './FilesTreeIcon.vue'
import TreeToggler from './TreeToggler.vue'

export default defineComponent({
name: 'FilesTree',
components: { FilesTreeIcon },
components: {
FilesTreeIcon,
TreeToggler
},
props: {
isRoot: {
type: Boolean,
default: true
},
files: {
type: Array,
type: Array as PropType<File[]>,
default: () => []
},
currentFile: {
Expand Down
104 changes: 18 additions & 86 deletions src/admin/app/components/FilesTreeIcon.vue
Original file line number Diff line number Diff line change
@@ -1,93 +1,25 @@
<template>
<svg
v-if="isDir(file) && file.isOpen"
class="mr-1 h-4 w-4 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>

<svg
v-else-if="isDir(file)"
class="mr-1 h-4 w-4 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>

<span v-else class="mr-1 inline-block h-4 w-4" />

<svg
v-if="isDir(file)"
class="mr-1 h-5 w-5 text-blue-400"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
</svg>

<svg
v-else-if="isImage(file)"
class="mr-1 h-5 w-5 text-gray-500"
:class="{ 'text-gray-600': isCurrent(file) }"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>

<svg
v-else
class="mr-1 h-5 w-5 text-gray-500"
:class="{ 'text-gray-600': isCurrent(file) }"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<vscode-icons:default-folder-opened v-if="isDir && file.isOpen" />
<vscode-icons:default-folder v-else-if="isDir" />
<mdi:language-markdown v-else-if="isMarkdown" />
<vscode-icons:file-type-vue v-else-if="isVue" />
<heroicons-outline:photograph v-else-if="isImage" />
<heroicons-outline:document v-else />
</template>

<script lang="ts">
import { defineComponent, inject } from 'vue3'

export default defineComponent({
props: {
file: {
type: Object,
required: true
}
},
setup() {
const { isDir, isCurrent, isImage, hasOneDir, open } = inject('tree-helpers')
<script setup lang="ts">
import { defineProps, computed } from 'vue3'
import { isImage as isFileImage } from '../utils'

return {
isDir,
isCurrent,
isImage,
hasOneDir,
open
}
const props = defineProps({
file: {
type: Object,
required: true
}
})

const isDir = computed(() => props.file.type === 'directory')
const isImage = computed(() => isFileImage(props.file))
const isMarkdown = computed(() => props.file.extension === '.md')
const isVue = computed(() => props.file.extension === '.vue')
</script>
Loading