feat(ui): implement dynamic dropdown navigation and refactor project cards
Replaced the static navigation with a dynamic, internationalized dropdown menu powered by a new `useNavLinks` composable. The navigation items are now sourced from i18n files. The featured project cards on the homepage have been refactored to use the `<UPageCard>` component, and the content schema is updated with `spotlight` and `highlight` options for enhanced display.
This commit is contained in:
85
app/composables/NavLinks.ts
Normal file
85
app/composables/NavLinks.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// composables/useNavLinks.ts
|
||||||
|
import type { NavigationMenuItem } from "@nuxt/ui";
|
||||||
|
|
||||||
|
export const useNavLinks = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const navLinks = computed<NavigationMenuItem[]>(() => [
|
||||||
|
{
|
||||||
|
label: t("common.header.services.label"),
|
||||||
|
icon: "mdi:briefcase-outline",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: t("common.header.services.children.webDev.label"),
|
||||||
|
description: t("common.header.services.children.webDev.description"),
|
||||||
|
icon: "mdi:web",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.services.children.softwareDev.label"),
|
||||||
|
description: t(
|
||||||
|
"common.header.services.children.softwareDev.description"
|
||||||
|
),
|
||||||
|
icon: "mdi:tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.services.children.eventVisual.label"),
|
||||||
|
description: t(
|
||||||
|
"common.header.services.children.eventVisual.description"
|
||||||
|
),
|
||||||
|
icon: "mdi:monitor-dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.services.children.lab.label"),
|
||||||
|
description: t("common.header.services.children.lab.description"),
|
||||||
|
icon: "mdi:test-tube-off",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.projects.label"),
|
||||||
|
icon: "mdi:lightbulb-group-outline",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: t("common.header.projects.children.commercialWebsite.label"),
|
||||||
|
description: t(
|
||||||
|
"common.header.projects.children.commercialWebsite.description"
|
||||||
|
),
|
||||||
|
icon: "mdi:web",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.projects.children.gameDev.label"),
|
||||||
|
description: t("common.header.projects.children.gameDev.description"),
|
||||||
|
icon: "mdi:gamepad-variant-outline",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.projects.children.tools.label"),
|
||||||
|
description: t("common.header.projects.children.tools.description"),
|
||||||
|
icon: "mdi:tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.projects.children.special.label"),
|
||||||
|
description: t("common.header.projects.children.special.description"),
|
||||||
|
icon: "mdi:star",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.header.insights.label"),
|
||||||
|
icon: "mdi:brain",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: t("common.header.insights.children.xiaomaiBlog.label"),
|
||||||
|
description: t(
|
||||||
|
"common.header.insights.children.xiaomaiBlog.description"
|
||||||
|
),
|
||||||
|
icon: "mdi:pencil-outline",
|
||||||
|
to: "https://xiaomai.tootaio.com/",
|
||||||
|
type: "link",
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return navLinks;
|
||||||
|
};
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<UPage>
|
<UPage>
|
||||||
<UHeader>
|
<UHeader
|
||||||
|
:ui="{
|
||||||
|
left: 'flex items-center gap-1.5',
|
||||||
|
center: 'hidden lg:flex lg:flex-16',
|
||||||
|
right: 'flex items-center justify-end gap-1.5',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<template #title> Tootaio Studio </template>
|
<template #title> Tootaio Studio </template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<UNavigationMenu :items="navLinks" variant="link" />
|
<UNavigationMenu
|
||||||
|
:items="navLinks"
|
||||||
|
variant="link"
|
||||||
|
class="w-full justify-center"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #body>
|
<template #body>
|
||||||
<UNavigationMenu :items="navLinks" orientation="vertical" />
|
<UNavigationMenu :items="navLinks" orientation="vertical" />
|
||||||
@@ -42,6 +52,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const { setLocale } = useI18n();
|
const { setLocale } = useI18n();
|
||||||
|
const navLinks = useNavLinks();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
@@ -23,26 +23,24 @@
|
|||||||
:items="page?.featuredProjects.projects"
|
:items="page?.featuredProjects.projects"
|
||||||
:ui="{ item: 'basis-full sm:basis-1/2 lg:basis-1/3' }"
|
:ui="{ item: 'basis-full sm:basis-1/2 lg:basis-1/3' }"
|
||||||
>
|
>
|
||||||
<UCard class="my-2">
|
<UPageCard
|
||||||
<template #header>
|
class="my-2"
|
||||||
<h3 class="text-2xl font-bold">{{ item.title }}</h3>
|
:title="item.title"
|
||||||
</template>
|
:description="item.description"
|
||||||
<template #default>
|
:highlight="item.highlight"
|
||||||
<img :src="item.image" :alt="item.title" />
|
:spotlight="item.spotlight"
|
||||||
<p class="mt-2 line-clamp-3">{{ item.description }}</p>
|
>
|
||||||
</template>
|
<img :src="item.image" :alt="item.title" />
|
||||||
<template #footer>
|
<UButton
|
||||||
<UButton
|
v-if="item.demoLink"
|
||||||
v-if="item.demoLink"
|
:href="item.demoLink"
|
||||||
:href="item.demoLink"
|
target="_blank"
|
||||||
target="_blank"
|
rel="noopener"
|
||||||
rel="noopener"
|
size="sm"
|
||||||
size="sm"
|
>
|
||||||
>
|
{{ $t("index.featuredProjects.viewDemo") }}
|
||||||
{{ $t("index.featuredProjects.viewDemo") }}
|
</UButton>
|
||||||
</UButton>
|
</UPageCard>
|
||||||
</template>
|
|
||||||
</UCard>
|
|
||||||
</UCarousel>
|
</UCarousel>
|
||||||
</UPageSection>
|
</UPageSection>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import type {NavigationMenuItem} from "@nuxt/ui";
|
|
||||||
|
|
||||||
export const navLinks: NavigationMenuItem[] = [{
|
|
||||||
label: "Services",
|
|
||||||
icon: "mdi:briefcase-outline"
|
|
||||||
}, {
|
|
||||||
label: "Projects",
|
|
||||||
icon: "mdi:lightbulb-group-outline"
|
|
||||||
}, {
|
|
||||||
label: "Insights",
|
|
||||||
icon: "mdi:brain"
|
|
||||||
}]
|
|
||||||
@@ -1,86 +1,56 @@
|
|||||||
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
|
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
|
||||||
|
|
||||||
|
const defineIndexSchema = () =>
|
||||||
|
z.object({
|
||||||
|
capabilities: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
features: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
icon: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
featuredProjects: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
projects: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
image: z.string(),
|
||||||
|
demoLink: z.string(),
|
||||||
|
highlight: z.boolean(),
|
||||||
|
spotlight: z.boolean(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
techStack: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
}),
|
||||||
|
whyChooseUs: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
features: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
icon: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export default defineContentConfig({
|
export default defineContentConfig({
|
||||||
collections: {
|
collections: {
|
||||||
index_en: defineCollection({
|
index_en: defineCollection({
|
||||||
type: "page",
|
type: "page",
|
||||||
source: "en-US/index.yml",
|
source: "en-US/index.yml",
|
||||||
schema: z.object({
|
schema: defineIndexSchema(),
|
||||||
capabilities: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
features: z.array(
|
|
||||||
z.object({
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
icon: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
featuredProjects: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
projects: z.array(
|
|
||||||
z.object({
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
image: z.string(),
|
|
||||||
demoLink: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
techStack: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
}),
|
|
||||||
whyChooseUs: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
features: z.array(
|
|
||||||
z.object({
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
icon: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
index_zh: defineCollection({
|
index_zh: defineCollection({
|
||||||
type: "page",
|
type: "page",
|
||||||
source: "zh-CN/index.yml",
|
source: "zh-CN/index.yml",
|
||||||
schema: z.object({
|
schema: defineIndexSchema(),
|
||||||
capabilities: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
features: z.array(
|
|
||||||
z.object({
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
icon: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
featuredProjects: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
projects: z.array(
|
|
||||||
z.object({
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
image: z.string(),
|
|
||||||
demoLink: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
techStack: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
}),
|
|
||||||
whyChooseUs: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
features: z.array(
|
|
||||||
z.object({
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
icon: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ featuredProjects:
|
|||||||
description: "An interactive guide which help more than 5000 students back to China Mainland to continue their study during pandemic COVID-19 in year 2022. This project is supported by The Ministry of Foreign Affairs (MFA) in Malaysia"
|
description: "An interactive guide which help more than 5000 students back to China Mainland to continue their study during pandemic COVID-19 in year 2022. This project is supported by The Ministry of Foreign Affairs (MFA) in Malaysia"
|
||||||
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png"
|
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png"
|
||||||
demoLink: "https://tootaio.github.io"
|
demoLink: "https://tootaio.github.io"
|
||||||
|
spotlight: true
|
||||||
- title: "Light Chasing"
|
- title: "Light Chasing"
|
||||||
description: "This is an entry from the 2023 G-bits University GameJam Challenge, based on the Godot engine."
|
description: "This is an entry from the 2023 G-bits University GameJam Challenge, based on the Godot engine."
|
||||||
image: "https://img.tootaio.com/i/2025/09/26/j2swgq.png"
|
image: "https://img.tootaio.com/i/2025/09/26/j2swgq.png"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ featuredProjects:
|
|||||||
description: "2022 年疫情期间,为马来西亚留学生开发的返校攻略网站。帮助 5000+ 名留学生顺利返校。并获得马来西亚外交部推荐。"
|
description: "2022 年疫情期间,为马来西亚留学生开发的返校攻略网站。帮助 5000+ 名留学生顺利返校。并获得马来西亚外交部推荐。"
|
||||||
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png"
|
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png"
|
||||||
demoLink: "https://tootaio.github.io"
|
demoLink: "https://tootaio.github.io"
|
||||||
|
spotlight: true
|
||||||
- title: "光追"
|
- title: "光追"
|
||||||
description: "基于 Godot 引擎的 2023 年吉比特高校挑战赛参赛作品。"
|
description: "基于 Godot 引擎的 2023 年吉比特高校挑战赛参赛作品。"
|
||||||
image: "https://img.tootaio.com/i/2025/09/26/j2swgq.png"
|
image: "https://img.tootaio.com/i/2025/09/26/j2swgq.png"
|
||||||
|
|||||||
57
i18n/locales/en-US/common.json
Normal file
57
i18n/locales/en-US/common.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"header": {
|
||||||
|
"insights": {
|
||||||
|
"label": "Insights",
|
||||||
|
"children": {
|
||||||
|
"xiaomaiBlog": {
|
||||||
|
"label": "Founder's Blog",
|
||||||
|
"description": "The blog sites of our studio founder - Xiaomai."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"label": "Services",
|
||||||
|
"children": {
|
||||||
|
"softwareDev": {
|
||||||
|
"label": "Software Development",
|
||||||
|
"description": "Software and Tools / Automation Development"
|
||||||
|
},
|
||||||
|
"eventVisual": {
|
||||||
|
"label": "Event Big Screen Visualization",
|
||||||
|
"description": "Display the content whichever you want on the big screen of your events!"
|
||||||
|
},
|
||||||
|
"lab": {
|
||||||
|
"label": "Labs and Tests",
|
||||||
|
"description": "Unavailable yet..."
|
||||||
|
},
|
||||||
|
"webDev": {
|
||||||
|
"label": "Web Development",
|
||||||
|
"description": "Customise Web design + CMS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"label": "Projects",
|
||||||
|
"children": {
|
||||||
|
"commercialWebsite": {
|
||||||
|
"label": "Commercial Website",
|
||||||
|
"description": "Fully customize website showcases."
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"label": "Tools and Softwares",
|
||||||
|
"description": "Some QoL tools we developed"
|
||||||
|
},
|
||||||
|
"special": {
|
||||||
|
"label": "Special Projects",
|
||||||
|
"description": "Special projects to meet customer needs"
|
||||||
|
},
|
||||||
|
"gameDev": {
|
||||||
|
"label": "Indie Games",
|
||||||
|
"description": "Have a look at our indie games!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
i18n/locales/zh-CN/common.json
Normal file
57
i18n/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"header": {
|
||||||
|
"services": {
|
||||||
|
"label": "服务",
|
||||||
|
"children": {
|
||||||
|
"softwareDev": {
|
||||||
|
"label": "软件开发",
|
||||||
|
"description": "软件和工具/自动化开发"
|
||||||
|
},
|
||||||
|
"eventVisual": {
|
||||||
|
"label": "活动大屏可视化",
|
||||||
|
"description": "在您的活动大屏幕上显示您想要的内容!"
|
||||||
|
},
|
||||||
|
"lab": {
|
||||||
|
"label": "实验室和测试",
|
||||||
|
"description": "还无法使用..."
|
||||||
|
},
|
||||||
|
"webDev": {
|
||||||
|
"label": "网站开发",
|
||||||
|
"description": "定制网页设计 + CMS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"label": "案例",
|
||||||
|
"children": {
|
||||||
|
"commercialWebsite": {
|
||||||
|
"label": "商业网站",
|
||||||
|
"description": "完全定制网站展示。"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"label": "工具和软件",
|
||||||
|
"description": "我们开发的一些生活质量工具"
|
||||||
|
},
|
||||||
|
"special": {
|
||||||
|
"label": "特别项目",
|
||||||
|
"description": "特殊项目满足客户需求"
|
||||||
|
},
|
||||||
|
"gameDev": {
|
||||||
|
"label": "独立游戏",
|
||||||
|
"description": "看看我们的独立游戏!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"insights": {
|
||||||
|
"label": "博客",
|
||||||
|
"children": {
|
||||||
|
"xiaomaiBlog": {
|
||||||
|
"label": "创始人博客",
|
||||||
|
"description": "我们工作室创始人小麦的博客网站。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,13 +36,13 @@ export default defineNuxtConfig({
|
|||||||
code: "en",
|
code: "en",
|
||||||
iso: "en-US",
|
iso: "en-US",
|
||||||
name: "English",
|
name: "English",
|
||||||
files: ["en-US/index.json"],
|
files: ["en-US/common.json", "zh-CN/index.json"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: "zh-CN",
|
code: "zh-CN",
|
||||||
iso: "zh-CN",
|
iso: "zh-CN",
|
||||||
name: "简体中文",
|
name: "简体中文",
|
||||||
files: ["zh-CN/index.json"],
|
files: ["zh-CN/common.json", "zh-CN/index.json"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
strategy: "no_prefix"
|
strategy: "no_prefix"
|
||||||
|
|||||||
Reference in New Issue
Block a user