Skip to content

Commit

Permalink
Merge pull request #3 from murchinroom/feat-content
Browse files Browse the repository at this point in the history
Add /blogs: Nuxt Content
  • Loading branch information
cdfmlr authored Dec 20, 2024
2 parents f774529 + e678ec3 commit f9a1109
Show file tree
Hide file tree
Showing 18 changed files with 5,251 additions and 152 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
public/blogs.test

# Nuxt dev/build outputs
.output
.data
Expand Down
67 changes: 10 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,83 +11,36 @@ Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introdu
Make sure to install the dependencies:

```bash
# npm
npm install

# pnpm
pnpm install

# yarn
yarn install

# bun
bun install
```

## Development Server

Start the development server on `http://localhost:3000`:

```bash
# npm
npm run dev

# pnpm
pnpm run dev

# yarn
yarn dev

# bun
bun run dev
```

## Production

Build the application for production:

```bash
# npm
npm run build

# pnpm
pnpm run build

# yarn
yarn build

# bun
bun run build
```

Locally preview production build:

```bash
# npm
npm run preview

# pnpm
pnpm run preview

# yarn
yarn preview

# bun
bun run preview
```

Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

## Static Site Generation

Docs: [Static Hosting](https://nuxt.com/docs/getting-started/deployment#static-hosting)
Build for production with static site generation:

```bash
pnpm nuxi generate
# or
pnpm run generate-static
```

Docs: [Static Hosting](https://nuxt.com/docs/getting-started/deployment#static-hosting)

## Template

This site is built with [Gr33nW33n/nuxtship-template](https://github.com/Gr33nW33n/nuxtship-template)

## Blogs

[Nuxt Content](https://content.nuxt.com) is set up to read
Markdown articles from [`public/blogs`](public/blogs).

See also: [public/blogs/.README.md](public/blogs/.README.md)
113 changes: 113 additions & 0 deletions components/blogs/Breadcrumb.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<template>
<div ref="breadcrumbContainer" class="relative">
<nav class="flex items-center space-x-2 max-w-screen-xl text-ellipsis overflow-scroll">
<template v-for="(item, index) in breadcrumbs" :key="index">
<div class="flex items-center">

<!-- for larger screens: show the full path -->
<span class="max-md:hidden flex items-center">
<span v-if="index > 0" class="ml-0.5 mr-2">></span>
<span @click="toggleTreeView(item._path)" :class="{'truncate max-w-xs mx-1 font-bold text-black cursor-default': item._path === $route.path, 'cursor-pointer text-gray-600 hover:text-gray-900 hover:underline': item._path !== $route.path}">
<Icon :name="item.children ? 'bx:bxs-folder-open' : 'bx:bxs-file'" class="mx-0.5 mb-1 inline-block align-middle"/>
{{ item.title }}
</span>
</span>

<!-- for small screens: fold the mid dirs -->
<span class="md:hidden flex items-center" v-if="index === 0 || index >= breadcrumbs.length-2">
<span v-if="index > 0" class="ml-0.5 mr-2">></span>
<span @click="toggleTreeView(item._path)" :class="{'truncate max-w-xs mx-1 font-bold text-black cursor-default': item._path === $route.path, 'cursor-pointer text-gray-600 hover:text-gray-900 hover:underline': item._path !== $route.path}">
<Icon :name="item.children ? 'bx:bxs-folder-open' : 'bx:bxs-file'" class="mr-0.5 mb-1 inline-block align-middle"/>
<span v-if="index === 0 || index === breadcrumbs.length-1">{{ item.title }}</span>
<span v-else>..</span>
</span>
</span>

</div>
</template>
</nav>
<div v-if="showTreeView" class="tree-panel absolute left-0 top-full mt-2 h-screen w-full bg-slate-50 shadow-md z-10 overflow-y-auto" @click.stop>
<BlogsTreeView :items="navigation" :expandPath="expandPath" />
<button @click="showTreeView = false" class="absolute top-2 right-2 text-black bg-gray-200 rounded-full p-1">✖️</button>
</div>
</div>
</template>

<script>
import {defineComponent, ref, onMounted, onBeforeUnmount, watch} from 'vue';
import {useRoute} from 'vue-router';
export default defineComponent({
props: {navigation: {type: Array, required: true}},
setup(props) {
const route = useRoute();
const breadcrumbs = ref([]);
const showTreeView = ref(false);
const expandPath = ref('');
const breadcrumbContainer = ref(null);
const buildBreadcrumbs = (navigation) => {
const pathParts = route.path.split('/').filter(Boolean);
let currentLevel = navigation;
breadcrumbs.value = pathParts.map(part => {
const item = currentLevel.find(navItem => navItem._path.split('/').pop() === part);
if (!item) return {title: part, _path: `/${part}`, children: []};
currentLevel = item.children || [];
return item;
});
};
const toggleTreeView = (path) => {
expandPath.value = path;
showTreeView.value = !showTreeView.value;
};
const handleClickOutside = (event) => {
if (breadcrumbContainer.value && !breadcrumbContainer.value.contains(event.target)) {
showTreeView.value = false;
}
};
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside);
});
watch(() => props.navigation, (newNavigation) => {
buildBreadcrumbs(newNavigation);
}, {immediate: true});
return {breadcrumbs, showTreeView, expandPath, toggleTreeView, breadcrumbContainer};
}
});
</script>

<style scoped>
nav {
@apply flex items-center space-x-2;
}
.tree-panel {
min-width: 80vw;
max-width: 80vw;
}
span.truncate {
/* min-width: 4rem; */
max-width: 36vw; /* Adjust the max-width as needed */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
button {
@apply text-black bg-gray-200 rounded-full p-1;
}
.absolute {
width: auto; /* Remove fixed width */
}
</style>
22 changes: 22 additions & 0 deletions components/blogs/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<div class="min-w-full p-6 bg-white border border-gray-200 rounded-lg shadow hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 hover:-translate-y-1 duration-300 ">
<h5 class="min-h-8 min-w-16 mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">{{ article?.title }}</h5>
<p class="min-h-8 min-w-16 font-normal text-gray-700 dark:text-gray-400">{{ article?.description }}</p>
<span class="min-h-8 min-w-16 font-mono">{{ article?.publishedAt }}</span>
</div>
</template>

<script setup lang="ts">
defineProps<{
article?: {
title?: string,
description?: string,
publishedAt?: string,
}
}>()
</script>

<style scoped>
</style>
18 changes: 18 additions & 0 deletions components/blogs/Navbar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<div>
<ContentNavigation v-slot="{ navigation }">
<div class="breadcrumb">
<BlogsBreadcrumb :navigation="navigation" />
</div>
</ContentNavigation>
</div>
</template>

<script>
</script>

<style scoped>
.breadcrumb {
@apply flex items-center space-x-2;
}
</style>
54 changes: 54 additions & 0 deletions components/blogs/Surround.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>
<div class="bg-black px-10 py-16 max-sm:py-10 mt-20 mx-auto max-w-5xl rounded-lg flex flex-col items-center text-center">
<div v-if="prevArticle || nextArticle">
<h2 class="text-white text-2xl md:text-4xl">
Continue reading...
</h2>

<p v-if="nextArticle" class="text-slate-500 mt-8 text-lg md:text-xl inline-flex items-center line-clamp-1">
<!-- <Icon class="h-8 m-2 text-slate-500" name="bx:bxs-chevron-left" size="24"/>-->
<span class="mr-1">Next<span class="max-sm:hidden"> article</span>: </span>
<a :href="nextArticle?._path" class="text-slate-100 max-w-xs line-clamp-1 hover:underline">{{
nextArticle.title
}}</a>
</p>

<p v-if="prevArticle" class="text-slate-500 mt-4 text-lg md:text-xl inline-flex items-center line-clamp-1">
<!-- <Icon class="h-8 m-2 text-slate-500" name="bx:bxs-chevron-right" size="24"/>-->
<span class="mr-1">Previous<span class="max-sm:hidden"> article</span>: </span>
<a :href="prevArticle?._path" class="text-slate-100 max-w-xs line-clamp-1 hover:underline">{{
prevArticle.title
}}</a>
</p>
</div>
<div v-else>
<h2 class="text-white text-2xl md:text-4xl">
Continue reading...
</h2>

<p class="text-slate-500 mt-8 text-lg md:text-xl inline-flex items-center line-clamp-1">
<!-- <Icon class="h-8 m-2 text-slate-500" name="bx:bxs-chevron-left" size="24"/>-->
<span class="mr-1.5">Back<span class="max-sm:hidden"> to</span>: </span>
<a href="/blogs" class="text-slate-100 max-w-xs line-clamp-1 hover:underline"> /blogs </a>
</p>

</div>
</div>
</template>

<script setup lang="ts">
import {type ParsedContent} from "@nuxt/content";
interface ArticleMetadata {
title?: string,
_path?: string,
// description?: string,
// publishedAt?: string,
}
defineProps<{
prevArticle?: ArticleMetadata
nextArticle?: ArticleMetadata
}>()
</script>
87 changes: 87 additions & 0 deletions components/blogs/Toc.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<template>
<div class="toc">
<h2>{{ article?.title }}</h2>
<p v-if="article?.description" class="description">{{ article.description }}</p>
<p v-if="article?.publishedAt" class="published-at">{{ new Date(article.publishedAt).toLocaleDateString() }}</p>
<ul>
<li v-for="link in article?.body?.toc?.links" :key="link.id">
<a :href="'#' + link.id" @click.prevent="scrollToHeader(link.id)" :class="{'ml-4': link.depth === 3}">
{{ link.text }}
</a>
<ul v-if="link.children">
<li v-for="child in link.children" :key="child.id">
<a :href="'#' + child.id" @click.prevent="scrollToHeader(child.id)" class="ml-4">
{{ child.text }}
</a>
</li>
</ul>
</li>
</ul>
</div>
</template>

<script lang="ts" setup>
import {type ParsedContent} from "@nuxt/content";
interface ArticleMetadata {
title?: string;
description?: string;
publishedAt?: string;
}
defineProps<{
article?: ArticleMetadata & ParsedContent;
}>();
const scrollToHeader = (id: string) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({behavior: 'smooth'});
}
};
</script>

<style scoped>
.toc {
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: #f9f9f9;
}
h2 {
font-size: 1.25rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.description {
font-size: 1rem;
color: #666;
margin-bottom: 0.5rem;
}
.published-at {
font-size: 0.875rem;
color: #999;
margin-bottom: 1rem;
}
ul {
list-style-type: none;
padding-left: 0;
}
a {
text-decoration: none;
color: #007bff;
}
a:hover {
text-decoration: underline;
}
.ml-4 {
margin-left: 1rem;
}
</style>
Loading

0 comments on commit f9a1109

Please sign in to comment.