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>
|
||||
<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 #default>
|
||||
<UNavigationMenu :items="navLinks" variant="link" />
|
||||
<UNavigationMenu
|
||||
:items="navLinks"
|
||||
variant="link"
|
||||
class="w-full justify-center"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<UNavigationMenu :items="navLinks" orientation="vertical" />
|
||||
@@ -42,6 +52,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { setLocale } = useI18n();
|
||||
const navLinks = useNavLinks();
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
@@ -23,15 +23,14 @@
|
||||
:items="page?.featuredProjects.projects"
|
||||
:ui="{ item: 'basis-full sm:basis-1/2 lg:basis-1/3' }"
|
||||
>
|
||||
<UCard class="my-2">
|
||||
<template #header>
|
||||
<h3 class="text-2xl font-bold">{{ item.title }}</h3>
|
||||
</template>
|
||||
<template #default>
|
||||
<UPageCard
|
||||
class="my-2"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:highlight="item.highlight"
|
||||
:spotlight="item.spotlight"
|
||||
>
|
||||
<img :src="item.image" :alt="item.title" />
|
||||
<p class="mt-2 line-clamp-3">{{ item.description }}</p>
|
||||
</template>
|
||||
<template #footer>
|
||||
<UButton
|
||||
v-if="item.demoLink"
|
||||
:href="item.demoLink"
|
||||
@@ -41,8 +40,7 @@
|
||||
>
|
||||
{{ $t("index.featuredProjects.viewDemo") }}
|
||||
</UButton>
|
||||
</template>
|
||||
</UCard>
|
||||
</UPageCard>
|
||||
</UCarousel>
|
||||
</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";
|
||||
|
||||
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({
|
||||
collections: {
|
||||
index_en: defineCollection({
|
||||
type: "page",
|
||||
source: "en-US/index.yml",
|
||||
schema: 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(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
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(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
schema: defineIndexSchema(),
|
||||
}),
|
||||
index_zh: defineCollection({
|
||||
type: "page",
|
||||
source: "zh-CN/index.yml",
|
||||
schema: 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(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
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(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
schema: defineIndexSchema(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png"
|
||||
demoLink: "https://tootaio.github.io"
|
||||
spotlight: true
|
||||
- title: "Light Chasing"
|
||||
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"
|
||||
|
||||
@@ -34,6 +34,7 @@ featuredProjects:
|
||||
description: "2022 年疫情期间,为马来西亚留学生开发的返校攻略网站。帮助 5000+ 名留学生顺利返校。并获得马来西亚外交部推荐。"
|
||||
image: "http://img.tootaio.com/i/2025/11/05/d9kcma.png"
|
||||
demoLink: "https://tootaio.github.io"
|
||||
spotlight: true
|
||||
- title: "光追"
|
||||
description: "基于 Godot 引擎的 2023 年吉比特高校挑战赛参赛作品。"
|
||||
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",
|
||||
iso: "en-US",
|
||||
name: "English",
|
||||
files: ["en-US/index.json"],
|
||||
files: ["en-US/common.json", "zh-CN/index.json"],
|
||||
},
|
||||
{
|
||||
code: "zh-CN",
|
||||
iso: "zh-CN",
|
||||
name: "简体中文",
|
||||
files: ["zh-CN/index.json"],
|
||||
files: ["zh-CN/common.json", "zh-CN/index.json"],
|
||||
},
|
||||
],
|
||||
strategy: "no_prefix"
|
||||
|
||||
Reference in New Issue
Block a user