Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions apps/frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ Browser API requests use the same-origin `/api` path. During local development,
Nuxt proxies that path to `NUXT_API_INTERNAL_BASE`; the Docker Compose setup
configures this automatically.

The application supports English at `/` and Simplified Chinese at `/zh`.
Translations live in `i18n/locales`; keep both locale files structurally in
sync when adding interface copy.

## Production

Build the application for production:
Expand Down
13 changes: 9 additions & 4 deletions apps/frontend/app/app.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<script setup lang="ts">
useSiteHead();
</script>

<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
<NuxtRouteAnnouncer />
<NuxtLoadingIndicator color="#4b3832" />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
46 changes: 46 additions & 0 deletions apps/frontend/app/assets/css/main.css
Original file line number Diff line number Diff line change
@@ -1 +1,47 @@
@import "tailwindcss";

@theme {
--color-brand-50: #faf7f5;
--color-brand-100: #f2eae5;
--color-brand-200: #e5d4ca;
--color-brand-500: #86665b;
--color-brand-700: #664c43;
--color-brand-900: #4b3832;
--color-accent-400: #93b77a;
--color-accent-600: #648b4f;
--font-sans:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
}

@layer base {
html {
background: var(--color-brand-50);
color: var(--color-brand-900);
scroll-behavior: smooth;
}

body {
min-width: 20rem;
min-height: 100dvh;
margin: 0;
background:
radial-gradient(
circle at top left,
rgb(147 183 122 / 16%),
transparent 34rem
),
var(--color-brand-50);
font-family: var(--font-sans);
text-rendering: optimizeLegibility;
}

::selection {
background: var(--color-brand-200);
}

:focus-visible {
outline: 3px solid var(--color-accent-600);
outline-offset: 3px;
}
}
10 changes: 10 additions & 0 deletions apps/frontend/app/components/AppFooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<template>
<footer class="border-brand-200/80 border-t bg-white/55">
<div
class="text-brand-700 mx-auto flex w-full max-w-6xl flex-col gap-2 px-5 py-8 text-sm sm:flex-row sm:items-center sm:justify-between sm:px-8"
>
<p>{{ $t("footer.tagline") }}</p>
<p>{{ $t("footer.copyright") }}</p>
</div>
</footer>
</template>
38 changes: 38 additions & 0 deletions apps/frontend/app/components/AppHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
const localePath = useLocalePath();
</script>

<template>
<header class="border-brand-200/80 border-b bg-white/75 backdrop-blur">
<div
class="mx-auto flex min-h-16 w-full max-w-6xl items-center justify-between gap-6 px-5 py-3 sm:px-8"
>
<NuxtLink
:to="localePath('index')"
class="flex items-center gap-3 rounded-sm font-semibold tracking-tight"
>
<img
alt=""
class="size-9 [image-rendering:pixelated]"
height="36"
src="/logo-mark.svg"
width="36"
/>
<span>AlienCommons</span>
</NuxtLink>

<nav
:aria-label="$t('navigation.primary')"
class="flex items-center gap-3"
>
<NuxtLink
:to="localePath('index')"
class="text-brand-700 hover:text-brand-900 hidden rounded-sm px-2 py-1 text-sm font-medium sm:inline-block"
>
{{ $t("navigation.home") }}
</NuxtLink>
<LocaleSwitcher />
</nav>
</div>
</header>
</template>
32 changes: 32 additions & 0 deletions apps/frontend/app/components/LocaleSwitcher.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
const { locale } = useI18n();

const localeOptions = [
{ code: "en", label: "EN", nameKey: "locale.english" },
{ code: "zh", label: "中文", nameKey: "locale.chinese" },
] as const;
</script>

<template>
<div
:aria-label="$t('locale.label')"
class="border-brand-200 bg-brand-50 flex rounded-lg border p-1"
role="group"
>
<SwitchLocalePathLink
v-for="option in localeOptions"
:key="option.code"
:aria-current="locale === option.code ? 'true' : undefined"
:aria-label="$t(option.nameKey)"
:class="[
'rounded-md px-2.5 py-1 text-xs font-semibold transition-colors',
locale === option.code
? 'bg-brand-900 text-white shadow-sm'
: 'text-brand-700 hover:bg-brand-100 hover:text-brand-900',
]"
:locale="option.code"
>
{{ option.label }}
</SwitchLocalePathLink>
</div>
</template>
16 changes: 16 additions & 0 deletions apps/frontend/app/composables/useSiteHead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function useSiteHead() {
const localeHead = useLocaleHead({ seo: true });

useHead(() => ({
htmlAttrs: localeHead.value.htmlAttrs,
link: [
...(localeHead.value.link ?? []),
{ href: "/favicon.svg", rel: "icon", type: "image/svg+xml" },
],
meta: localeHead.value.meta,
titleTemplate: (title) =>
title && title !== "AlienCommons"
? `${title} · AlienCommons`
: "AlienCommons",
}));
}
54 changes: 54 additions & 0 deletions apps/frontend/app/error.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script setup lang="ts">
import type { NuxtError } from "#app";

const props = defineProps<{
error: NuxtError;
}>();

const { t } = useI18n();
const localePath = useLocalePath();
const isNotFound = computed(() => props.error.statusCode === 404);
const title = computed(() =>
t(isNotFound.value ? "error.notFoundTitle" : "error.genericTitle")
);
const description = computed(() =>
t(isNotFound.value ? "error.notFoundDescription" : "error.genericDescription")
);

useSiteHead();
useSeoMeta({
robots: "noindex, nofollow",
title,
});

function returnHome() {
clearError({ redirect: localePath("index") });
}
</script>

<template>
<main
class="mx-auto grid min-h-dvh w-full max-w-3xl place-items-center px-5 py-16 text-center sm:px-8"
>
<div>
<p class="text-accent-600 text-sm font-bold tracking-widest uppercase">
{{ error.statusCode || 500 }}
</p>
<h1
class="text-brand-900 mt-4 text-4xl font-semibold tracking-tight sm:text-5xl"
>
{{ title }}
</h1>
<p class="text-brand-700 mx-auto mt-5 max-w-xl text-lg leading-8">
{{ description }}
</p>
<button
class="bg-brand-900 hover:bg-brand-700 mt-8 rounded-lg px-5 py-3 text-sm font-semibold text-white transition-colors"
type="button"
@click="returnHome"
>
{{ $t("error.returnHome") }}
</button>
</div>
</main>
</template>
16 changes: 16 additions & 0 deletions apps/frontend/app/layouts/default.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<template>
<div class="flex min-h-dvh flex-col">
<a
class="bg-brand-900 fixed top-3 left-3 z-50 -translate-y-24 rounded-md px-4 py-2 text-sm font-semibold text-white transition-transform focus:translate-y-0"
href="#main-content"
>
{{ $t("accessibility.skipToContent") }}
</a>

<AppHeader />
<main id="main-content" class="flex flex-1 flex-col" tabindex="-1">
<slot />
</main>
<AppFooter />
</div>
</template>
42 changes: 42 additions & 0 deletions apps/frontend/app/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script setup lang="ts">
const { t } = useI18n();

useSeoMeta({
description: () => t("home.description"),
ogDescription: () => t("home.description"),
ogTitle: () => t("home.metaTitle"),
title: () => t("home.metaTitle"),
});
</script>

<template>
<section
class="mx-auto flex w-full max-w-6xl flex-1 items-center px-5 py-20 sm:px-8 sm:py-28"
>
<div class="max-w-3xl">
<p
class="text-accent-600 mb-5 text-sm font-bold tracking-widest uppercase"
>
{{ $t("home.eyebrow") }}
</p>
<h1
class="text-brand-900 text-4xl leading-tight font-semibold tracking-tight text-balance sm:text-6xl"
>
{{ $t("home.title") }}
</h1>
<p
class="text-brand-700 mt-6 max-w-2xl text-lg leading-8 text-pretty sm:text-xl"
>
{{ $t("home.description") }}
</p>
<div
class="border-brand-200 bg-brand-100/65 mt-10 rounded-2xl border p-5 sm:p-6"
>
<p class="text-brand-900 font-semibold">{{ $t("home.statusTitle") }}</p>
<p class="text-brand-700 mt-1 leading-7">
{{ $t("home.statusDescription") }}
</p>
</div>
</div>
</section>
</template>
4 changes: 3 additions & 1 deletion apps/frontend/app/plugins/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export default defineNuxtPlugin(() => {
readonly: true,
});

const apiInternalBase = config.apiInternalBase.trim();
const apiInternalBase = import.meta.server
? config.apiInternalBase.trim()
: "";
const missingInternalBase = import.meta.server && !apiInternalBase;
const baseUrl = import.meta.server
? apiInternalBase || MISSING_INTERNAL_API_URL
Expand Down
33 changes: 33 additions & 0 deletions apps/frontend/i18n/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"accessibility": {
"skipToContent": "Skip to main content"
},
"error": {
"genericDescription": "Something prevented this page from loading. You can return home and try again.",
"genericTitle": "Something went wrong",
"notFoundDescription": "The page you were looking for does not exist or may have moved.",
"notFoundTitle": "Page not found",
"returnHome": "Return home"
},
"footer": {
"copyright": "© AlienCommons",
"tagline": "Built for the Technical Minecraft community."
},
"home": {
"description": "A shared place to publish knowledge, exchange ideas, and build lasting resources for the Technical Minecraft community.",
"eyebrow": "Technical Minecraft, together",
"metaTitle": "Technical Minecraft community",
"statusDescription": "The Nuxt application shell, localized routing, API foundation, and server-side rendering are ready for the first product features.",
"statusTitle": "The foundation is ready",
"title": "A home for knowledge built block by block."
},
"locale": {
"chinese": "Switch to Simplified Chinese",
"english": "Switch to English",
"label": "Language"
},
"navigation": {
"home": "Home",
"primary": "Primary navigation"
}
}
33 changes: 33 additions & 0 deletions apps/frontend/i18n/locales/zh.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"accessibility": {
"skipToContent": "跳到主要内容"
},
"error": {
"genericDescription": "页面加载时遇到了问题。你可以返回首页后重试。",
"genericTitle": "出现了一些问题",
"notFoundDescription": "你访问的页面不存在,或者已经移动。",
"notFoundTitle": "找不到页面",
"returnHome": "返回首页"
},
"footer": {
"copyright": "© AlienCommons",
"tagline": "为技术型 Minecraft 社区而建。"
},
"home": {
"description": "一个为技术型 Minecraft 社区发布知识、交流想法并共同沉淀长期资源的共享空间。",
"eyebrow": "一起探索技术型 Minecraft",
"metaTitle": "技术型 Minecraft 社区",
"statusDescription": "Nuxt 应用外壳、本地化路由、API 地基和服务端渲染已经就绪,可以开始构建第一个产品功能。",
"statusTitle": "项目地基已经就绪",
"title": "一砖一瓦,共建知识家园。"
},
"locale": {
"chinese": "切换到简体中文",
"english": "切换到英文",
"label": "语言"
},
"navigation": {
"home": "首页",
"primary": "主导航"
}
}
Loading