Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion cms-i18n/nuxt/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
useHead({
meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }],
link: [{ rel: 'icon', href: '/favicon.ico' }],
htmlAttrs: { lang: 'en' },
});
</script>

Expand Down
14 changes: 12 additions & 2 deletions cms-i18n/nuxt/app/components/Footer.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<script setup lang="ts">
import { DEFAULT_LOCALE, type Locale } from '~/lib/i18n/config';
import { localizeLink } from '~/lib/i18n/utils';

export interface SocialLink {
service: string;
url: string;
Expand All @@ -23,6 +26,7 @@ export interface FooterProps {
description?: string | null;
social_links?: SocialLink[];
};
locale?: Locale;
}

const props = defineProps<FooterProps>();
Expand All @@ -39,14 +43,20 @@ const lightLogoUrl = computed(() =>
const darkLogoUrl = computed(() =>
props.globals.logo_dark_mode ? `${runtimeConfig.public.directusUrl}/assets/${props.globals.logo_dark_mode}` : '',
);

// Get the current locale from props or default
const currentLocale = computed<Locale>(() => props.locale || DEFAULT_LOCALE);

// Helper to localize internal paths
const localize = (path: string | null | undefined) => localizeLink(path, currentLocale.value);
</script>

<template>
<footer v-if="globals" ref="footerRef" class="bg-gray dark:bg-[var(--background-variant-color)] py-16">
<Container class="text-foreground dark:text-white">
<div class="flex flex-col md:flex-row justify-between items-start gap-8 pt-8">
<div class="flex-1">
<NuxtLink to="/" class="inline-block transition-opacity hover:opacity-70">
<NuxtLink :to="localize('/')" class="inline-block transition-opacity hover:opacity-70">
<img
v-if="lightLogoUrl"
:src="lightLogoUrl"
Expand Down Expand Up @@ -90,7 +100,7 @@ const darkLogoUrl = computed(() =>
<li v-for="item in props.navigation.items" :key="item.id">
<NuxtLink
v-if="item.page?.permalink"
:to="item.page.permalink"
:to="localize(item.page.permalink)"
class="text-nav font-medium hover:underline"
>
{{ item.title }}
Expand Down
112 changes: 87 additions & 25 deletions cms-i18n/nuxt/app/components/NavigationBar.vue
Original file line number Diff line number Diff line change
@@ -1,54 +1,104 @@
<script setup lang="ts">
import { Menu, ChevronDown } from 'lucide-vue-next';
import SearchModal from '~/components/base/SearchModel.vue';
import LanguageSwitcher from '~/components/shared/LanguageSwitcher.vue';
import { DEFAULT_LOCALE, type Locale } from '~/lib/i18n/config';
import { localizeLink } from '~/lib/i18n/utils';

interface NavigationItem {
id: string;
title: string;
url?: string;
page?: { permalink: string };
children?: NavigationItem[];
title?: string | null;
url?: string | null;
page?: {
permalink?: string | null;
} | null;
children?: NavigationItem[] | string[] | null;
}

// Using template ref to expose the navigation bar to the layout for visual editing
const navigationRef = useTemplateRef('navigationRef');
defineExpose({ navigationRef });

interface Navigation {
items: NavigationItem[];
id?: string | null;
items?: NavigationItem[] | string[] | null;
}

interface Globals {
logo?: string;
logo_dark_mode?: string;
logo?: string | { id: string } | null;
logo_dark_mode?: string | { id: string } | null;
}

const props = defineProps<{
navigation: Navigation;
globals: Globals;
locale?: Locale;
supportedLocales?: Locale[];
localeNames?: Record<Locale, string>;
}>();

const menuOpen = ref(false);
const runtimeConfig = useRuntimeConfig();
const { setAttr } = useVisualEditing();

const lightLogoUrl = computed(() =>
props.globals?.logo ? `${runtimeConfig.public.directusUrl}/assets/${props.globals.logo}` : '/images/logo.svg',
);
// Helper to get logo ID from string or object
const getLogoId = (logo: string | { id: string } | null | undefined): string | null => {
if (!logo) return null;
if (typeof logo === 'string') return logo;
return logo.id;
};

const lightLogoUrl = computed(() => {
const logoId = getLogoId(props.globals?.logo);
return logoId ? `${runtimeConfig.public.directusUrl}/assets/${logoId}` : '/images/logo.svg';
});

const darkLogoUrl = computed(() =>
props.globals?.logo_dark_mode ? `${runtimeConfig.public.directusUrl}/assets/${props.globals.logo_dark_mode}` : '',
);
const darkLogoUrl = computed(() => {
const logoId = getLogoId(props.globals?.logo_dark_mode);
return logoId ? `${runtimeConfig.public.directusUrl}/assets/${logoId}` : '';
});

const handleLinkClick = () => {
menuOpen.value = false;
};

// Current locale for link localization
const currentLocale = computed(() => props.locale || DEFAULT_LOCALE);

// Helper to localize internal paths using shared utility
const localize = (path: string | null | undefined) => localizeLink(path, currentLocale.value);

// Localized home path
const homeLink = computed(() => localize('/'));

// Helper to check if an item is a full NavigationItem (not just a string ID)
const isNavigationItem = (item: NavigationItem | string): item is NavigationItem => {
return typeof item !== 'string';
};

// Get navigation items as full objects (filter out string IDs)
const navigationItems = computed(() => {
const items = props.navigation?.items;
if (!items) return [];
return items.filter(isNavigationItem);
});

// Helper to get children as full NavigationItems
const getChildren = (item: NavigationItem): NavigationItem[] => {
if (!item.children) return [];
return item.children.filter(isNavigationItem);
};

// Helper to get page permalink from page object
const getPagePermalink = (page: { permalink?: string | null } | null | undefined): string | null => {
return page?.permalink || null;
};
</script>

<template>
<header ref="navigationRef" class="sticky top-0 z-50 w-full bg-background text-foreground">
<Container class="flex items-center justify-between p-4">
<NuxtLink to="/" class="flex-shrink-0">
<NuxtLink :to="homeLink" class="flex-shrink-0">
<img :src="lightLogoUrl" alt="Logo" class="w-[120px] h-auto dark:hidden" width="150" height="100" />
<img
v-if="darkLogoUrl"
Expand All @@ -65,22 +115,25 @@ const handleLinkClick = () => {
<NavigationMenu
class="hidden md:flex"
:data-directus="
setAttr({ collection: 'navigation', item: props.navigation.id, fields: ['items'], mode: 'modal' })
setAttr({ collection: 'navigation', item: props.navigation.id || null, fields: ['items'], mode: 'modal' })
"
>
<NavigationMenuList class="flex gap-6">
<NavigationMenuItem v-for="section in props.navigation.items" :key="section.id">
<template v-if="section.children?.length">
<NavigationMenuItem v-for="section in navigationItems" :key="section.id">
<template v-if="getChildren(section).length">
<NavigationMenuTrigger
class="focus:outline-none font-heading !text-nav hover:bg-background hover:text-accent"
>
{{ section.title }}
</NavigationMenuTrigger>
<NavigationMenuContent class="min-w-[200px] rounded-md bg-background p-4 shadow-md">
<ul class="min-h-[100px] flex flex-col gap-2">
<li v-for="child in section.children" :key="child.id">
<li v-for="child in getChildren(section)" :key="child.id">
<NavigationMenuLink as-child>
<NuxtLink :to="child.page?.permalink || child.url || '#'" class="font-heading text-nav">
<NuxtLink
:to="localize(getPagePermalink(child.page) || child.url)"
class="font-heading text-nav"
>
{{ child.title }}
</NuxtLink>
</NavigationMenuLink>
Expand All @@ -90,7 +143,10 @@ const handleLinkClick = () => {
</template>

<NavigationMenuLink v-else as-child>
<NuxtLink :to="section.page?.permalink || section.url || '#'" class="font-heading text-nav p-2">
<NuxtLink
:to="localize(getPagePermalink(section.page) || section.url)"
class="font-heading text-nav p-2"
>
{{ section.title }}
</NuxtLink>
</NavigationMenuLink>
Expand All @@ -116,8 +172,8 @@ const handleLinkClick = () => {
class="top-full w-screen p-6 shadow-md max-w-full overflow-hidden bg-background"
>
<div class="flex flex-col gap-4">
<div v-for="section in props.navigation.items" :key="section.id">
<Collapsible v-if="section.children?.length">
<div v-for="section in navigationItems" :key="section.id">
<Collapsible v-if="getChildren(section).length">
<CollapsibleTrigger
class="font-heading text-nav hover:text-accent w-full text-left flex items-center focus:outline-none"
>
Expand All @@ -126,9 +182,9 @@ const handleLinkClick = () => {
</CollapsibleTrigger>
<CollapsibleContent class="ml-4 mt-2 flex flex-col gap-2">
<NuxtLink
v-for="child in section.children"
v-for="child in getChildren(section)"
:key="child.id"
:to="child.page?.permalink || child.url || '#'"
:to="localize(getPagePermalink(child.page) || child.url)"
class="font-heading text-nav"
@click="handleLinkClick"
>
Expand All @@ -139,7 +195,7 @@ const handleLinkClick = () => {

<NuxtLink
v-else
:to="section.page?.permalink || section.url || '#'"
:to="localize(getPagePermalink(section.page) || section.url)"
class="font-heading text-nav"
@click="handleLinkClick"
>
Expand All @@ -151,6 +207,12 @@ const handleLinkClick = () => {
</DropdownMenu>
</div>

<LanguageSwitcher
v-if="locale && supportedLocales && localeNames"
:current-locale="locale"
:supported-locales="supportedLocales"
:locale-names="localeNames"
/>
<ThemeToggle />
</nav>
</Container>
Expand Down
37 changes: 28 additions & 9 deletions cms-i18n/nuxt/app/components/base/BaseButton.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
<script setup lang="ts">
import { computed } from 'vue';
import { NuxtLink } from '#components';
import { buttonVariants } from '~/components/ui/button';
import { buttonVariants, type ButtonVariants } from '~/components/ui/button';
import { ArrowRight, Plus } from 'lucide-vue-next';
import { cn } from '@@/shared/utils';
import Button from '../ui/button/Button.vue';
import { localizeLink } from '~/lib/i18n/utils';

// Button variant type from the UI component
type ButtonVariant = NonNullable<ButtonVariants['variant']>;
type ButtonSize = NonNullable<ButtonVariants['size']>;

export interface ButtonProps {
id: string;
label?: string | null;
variant?: string | null;
variant?: ButtonVariant | string | null;
url?: string | null;
type?: 'page' | 'post' | 'url' | 'submit' | null;
page?: { permalink: string | null };
post?: { slug: string | null };
size?: 'default' | 'sm' | 'lg' | 'icon';
size?: ButtonSize;
icon?: 'arrow' | 'plus';
customIcon?: any;
customIcon?: unknown;
iconPosition?: 'left' | 'right';
className?: string;
disabled?: boolean;
Expand All @@ -31,6 +36,13 @@ const props = withDefaults(defineProps<ButtonProps>(), {
block: false,
});

// Get locale from composable (handles SSR URL rewrite correctly)
const { currentLocale } = useLocale();
const locale = currentLocale.value;

// Helper to localize internal paths using shared utility
const localize = (path: string | null | undefined) => localizeLink(path, locale) || undefined;

const icons: Record<string, any> = {
arrow: ArrowRight,
plus: Plus,
Expand All @@ -39,14 +51,21 @@ const icons: Record<string, any> = {
const Icon = computed(() => props.customIcon || (props.icon ? icons[props.icon] : null));

const href = computed(() => {
if (props.type === 'page' && props.page?.permalink) return props.page.permalink;
if (props.type === 'post' && props.post?.slug) return `/blog/${props.post.slug}`;
return props.url || undefined;
if (props.type === 'page' && props.page?.permalink) return localize(props.page.permalink);
if (props.type === 'post' && props.post?.slug) return localize(`/blog/${props.post.slug}`);
if (props.url) return localize(props.url);
return undefined;
});

// Safe variant that falls back to 'default' if invalid
const safeVariant = computed<ButtonVariant>(() => {
const validVariants: ButtonVariant[] = ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'];
return validVariants.includes(props.variant as ButtonVariant) ? (props.variant as ButtonVariant) : 'default';
});

const buttonClasses = computed(() =>
cn(
buttonVariants({ variant: props.variant as any, size: props.size }),
buttonVariants({ variant: safeVariant.value, size: props.size }),
props.className,
props.disabled && 'opacity-50 cursor-not-allowed',
props.block && 'w-full',
Expand All @@ -59,7 +78,7 @@ const linkComponent = computed(() => {
</script>
<template>
<Button
:variant="variant as any"
:variant="safeVariant"
:size="size"
:class="buttonClasses"
:disabled="disabled"
Expand Down
15 changes: 12 additions & 3 deletions cms-i18n/nuxt/app/components/base/SearchModel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ref, onMounted, watch, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { useDebounceFn } from '@vueuse/core';
import { Search } from 'lucide-vue-next';
import { localizeLink } from '~/lib/i18n/utils';

type SearchResult = {
id: string;
Expand All @@ -19,6 +20,13 @@ const loading = ref(false);
const searched = ref(false);
const router = useRouter();

// Get locale from composable (handles SSR URL rewrite correctly)
const { currentLocale } = useLocale();
const locale = currentLocale.value;

// Helper to localize internal paths using shared utility
const localize = (path: string) => localizeLink(path, locale);

const fetchResults = async (search: string) => {
if (search.length < 3) {
results.value = [];
Expand All @@ -31,7 +39,8 @@ const fetchResults = async (search: string) => {

try {
const data = await $fetch<SearchResult[]>('/api/search', {
params: { search },
params: { search, locale },
headers: { 'x-locale': locale },
});

results.value = [...data];
Expand Down Expand Up @@ -80,7 +89,7 @@ watch(open, (isOpen) => {
<CommandInput
placeholder="Search for pages or posts"
class="m-2 p-4 focus:outline-none text-base leading-normal"
@input="(e) => debouncedFetchResults(e.target.value)"
@input="(e: Event) => debouncedFetchResults((e.target as HTMLInputElement).value)"
/>

<CommandList class="p-2 text-foreground max-h-[500px] overflow-auto">
Expand All @@ -98,7 +107,7 @@ watch(open, (isOpen) => {
class="flex items-start gap-4 px-2 py-3"
:value="`${result.title} ${result.description} ${result.type} ${result.link} ${result.content}`"
@select="
router.push(result.link);
router.push(localize(result.link));
open = false;
"
>
Expand Down
Loading
Loading