Skip to content

Commit f5ce6f9

Browse files
Add Banner to /docs (#373)
* add banner * add hover motion * fix return null
1 parent 1cf0a99 commit f5ce6f9

File tree

11 files changed

+224
-3
lines changed

11 files changed

+224
-3
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ NUXT_PUBLIC_PRODUCT_DIRECTUS_URL=your_url_here
88
NUXT_IMAGE_DOMAINS=your_url_here
99
ALGOLIA_API_KEY=
1010
ALGOLIA_APPLICATION_ID=
11+
DIRECTUS_URL=

app/app.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { spec } from '@directus/openapi';
33
import { useRoute } from 'vue-router';
44
import { nextTick, watch } from 'vue';
5+
56
const route = useRoute();
67
78
const { data: navigation } = useAsyncData('navigation', () => fetchContentNavigation());
@@ -19,11 +20,11 @@ const { data: files } = useLazyFetch<ParsedContent[]>('/api/search.json', { defa
1920
const updateLinks = () => {
2021
nextTick(() => {
2122
const links = document.querySelectorAll('a');
22-
links.forEach(link => {
23+
links.forEach((link) => {
2324
const href = link.getAttribute('href');
2425
if (
25-
href?.startsWith('http') &&
26-
link.hostname !== window.location.hostname
26+
href?.startsWith('http')
27+
&& link.hostname !== window.location.hostname
2728
) {
2829
link.setAttribute('target', '_blank');
2930
link.setAttribute('rel', 'noopener noreferrer');

app/components/DocsBanner.vue

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script setup lang="ts">
2+
const { data: banner } = useFetch('/api/banner');
3+
4+
const dismissedBanners = useCookie('directus-dismissed-banners', {
5+
default: () => [] as string[],
6+
});
7+
8+
const bannerVisible = computed(() => {
9+
if (!unref(banner)) return false;
10+
return unref(dismissedBanners).includes(unref(banner)!.id) === false;
11+
});
12+
13+
const dismiss = (id: string) => {
14+
dismissedBanners.value = [...unref(dismissedBanners), id];
15+
};
16+
17+
const iconName = computed(() => {
18+
if (!unref(banner)) return null;
19+
return getIconName(unref(banner)!.icon);
20+
});
21+
</script>
22+
23+
<template>
24+
<div
25+
v-if="banner && bannerVisible"
26+
class="bg-foreground cursor-pointer h-8"
27+
>
28+
<UContainer class="h-full flex items-center gap-x-4">
29+
<NuxtLink
30+
class="flex-grow h-full flex items-center text-background no-underline text-xs leading-xs font-semibold group"
31+
:href="banner.link ?? undefined"
32+
>
33+
<Icon
34+
v-if="iconName"
35+
class="mr-2 size-5"
36+
:name="iconName"
37+
/>
38+
<span
39+
class="whitespace-nowrap overflow-hidden text-ellipsis"
40+
v-html="banner.content"
41+
/>
42+
<Icon
43+
class="hidden md:block transform duration-150 ease-out ml-1 group-hover:translate-x-1 size-5"
44+
name="material-symbols:arrow-forward"
45+
/>
46+
</NuxtLink>
47+
48+
<button
49+
aria-label="Close"
50+
class="text-background"
51+
:padded="false"
52+
icon="material-symbols:close"
53+
@click="dismiss(banner.id)"
54+
>
55+
<Icon
56+
name="material-symbols:close"
57+
class="size-5"
58+
/>
59+
</button>
60+
</UContainer>
61+
</div>
62+
</template>

app/components/DocsHeader.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ const algoliaNavigator = {
115115
:links="links"
116116
:ui="route.path.startsWith('/api') ? { container: 'max-w-screen' } : {}"
117117
>
118+
<template #top>
119+
<DocsBanner />
120+
</template>
121+
118122
<template #logo>
119123
<LogoDocs class="w-auto h-8 shrink-0" />
120124
</template>

app/utils/icons.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const filledIcons = [
2+
'all_inclusive',
3+
'apartment',
4+
'api',
5+
'api',
6+
'arrow_back',
7+
'arrow_forward',
8+
'arrow_outward',
9+
'autopay',
10+
'autostop',
11+
'avg_pace',
12+
'cached',
13+
'check',
14+
'checklist',
15+
'checklist_rtl',
16+
'close',
17+
'cloudy',
18+
'code',
19+
'compare_arrows',
20+
'cruelty_free',
21+
'dynamic_feed',
22+
'electrical_services',
23+
'emoji_people',
24+
'expand_less',
25+
'expand_more',
26+
'full_stacked_bar_chart',
27+
'functions',
28+
'globe_uk',
29+
'home_app_logo',
30+
'horizontal_rule',
31+
'laps',
32+
'link',
33+
'image_search',
34+
'insights',
35+
'login',
36+
'menu_rounded',
37+
'money_off',
38+
'monitoring',
39+
'online_prediction',
40+
'open_in_new',
41+
'password',
42+
'partner_exchange',
43+
'post_add',
44+
'public',
45+
'published_with_changes',
46+
'query_stats',
47+
'repeat',
48+
'search',
49+
'security',
50+
'sort_by_alpha',
51+
'sports_martial_arts',
52+
'support',
53+
'sync_alt',
54+
'timeline',
55+
'translate',
56+
'trending_up',
57+
'update',
58+
'webhook',
59+
'work',
60+
'support_agent',
61+
'edit_sharp',
62+
];
63+
64+
export function getIconName(name: string): string | undefined {
65+
if (!name) return;
66+
// Convert the icon coming from the API to the name of the icon component
67+
// Directus uses Google Material Icons and the icon values are snake_case (e.g. "account_circle")
68+
const prefix = 'material-symbols:';
69+
// Change snake case to kebab case
70+
const kebabCase = name.replace(/_/g, '-');
71+
// If the icon is one of the filled icons, do not add the suffix '-outline'. Needed because of descrepancies between the Google Material Font we use in Directus icon interface and the Iconify library.
72+
const iconName = prefix + kebabCase + (filledIcons.includes(name) ? '' : '-outline');
73+
return iconName;
74+
}

nuxt.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export default defineNuxtConfig({
9797
},
9898
},
9999
},
100+
directusUrl: process.env.DIRECTUS_URL,
100101
},
101102

102103
build: {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
},
1212
"dependencies": {
1313
"@directus/openapi": "0.1.15",
14+
"@directus/sdk": "^19.1.0",
1415
"@docsearch/css": "3.8.3",
1516
"@docsearch/js": "3.8.3",
1617
"@iconify-json/heroicons-outline": "1.2.1",

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/api/banner.get.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export default defineCachedEventHandler(async () => {
2+
try {
3+
const [data] = await directusServer.request(readItems('site_banners', {
4+
fields: ['id', 'icon', 'content', 'link'],
5+
filter: {
6+
show_on: {
7+
// @ts-expect-error - _contains works for csv fields in Directus
8+
_contains: 'docs',
9+
},
10+
},
11+
sort: ['-date_created'],
12+
limit: 1,
13+
}));
14+
15+
if (!data) return {};
16+
17+
return data;
18+
}
19+
catch (error) {
20+
console.error(error);
21+
return {};
22+
}
23+
}, {
24+
maxAge: 60 * 5, // 5 minutes
25+
swr: true,
26+
});

server/utils/directus-server.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { $fetch } from 'ofetch';
2+
import {
3+
createDirectus,
4+
readItem,
5+
readItems,
6+
rest,
7+
type QueryFilter,
8+
} from '@directus/sdk';
9+
import type { Schema } from '#shared/types/schema';
10+
11+
const {
12+
directusUrl,
13+
} = useRuntimeConfig();
14+
15+
const directusServer = createDirectus<Schema>(directusUrl as string, {
16+
globals: {
17+
fetch: $fetch,
18+
},
19+
}).with(rest());
20+
21+
export {
22+
directusServer,
23+
readItem,
24+
readItems,
25+
};
26+
export type { QueryFilter };

0 commit comments

Comments
 (0)