feat: apply figma responsive home design
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 49s

This commit is contained in:
TerryM
2026-05-17 19:38:43 +08:00
parent 5b67279734
commit 2c76039c44
13 changed files with 69 additions and 47 deletions

View File

@@ -7,6 +7,6 @@ Source: Figma file `uHDZkVHjAp7BXDKQKB0PM4`, responsive reference node `3761:109
- `banner-576.png` — mobile/tablet banner crop from node `3726:13099`. - `banner-576.png` — mobile/tablet banner crop from node `3726:13099`.
- `banner-440.png` — mobile banner crop from node `3726:14199`. - `banner-440.png` — mobile banner crop from node `3726:14199`.
- `banner-375.png` — mobile banner crop from node `3726:14238`. - `banner-375.png` — mobile banner crop from node `3726:14238`.
- `recommendation-1.png` ... `recommendation-5.png` — official recommendation cover exports from the 1920px frame card image nodes. - `official-recommendation-1.png` ... `official-recommendation-5.png` — official recommendation cover exports from the 1920px frame card image nodes; used only as fallback/placeholder covers so real resource cards keep accurate API-provided imagery.
These files are visual UI assets only. They do not change backend data or API contracts. These files are visual UI assets only. They do not change backend data or API contracts.

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

View File

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 153 KiB

View File

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

View File

@@ -1,3 +1,4 @@
import { lazy, Suspense } from "react";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import { I18nProvider } from "./i18n"; import { I18nProvider } from "./i18n";
import { PublicLayout } from "./layouts/PublicLayout"; import { PublicLayout } from "./layouts/PublicLayout";
@@ -7,12 +8,17 @@ import { CategoryPage } from "./pages/CategoryPage";
import { SearchPage } from "./pages/SearchPage"; import { SearchPage } from "./pages/SearchPage";
import { FavoritesPage } from "./pages/FavoritesPage"; import { FavoritesPage } from "./pages/FavoritesPage";
import { ResourceDetail } from "./pages/ResourceDetail"; import { ResourceDetail } from "./pages/ResourceDetail";
import { WalletPage } from "./pages/WalletPage";
import { AboutPage } from "./pages/AboutPage"; import { AboutPage } from "./pages/AboutPage";
import { adminUiPrefix } from "./adminPaths"; import { adminUiPrefix } from "./adminPaths";
import { AdminRouteTree } from "./adminRouteTree"; import { AdminRouteTree } from "./adminRouteTree";
import { AdminRouterModeProvider } from "./adminRouterMode"; import { AdminRouterModeProvider } from "./adminRouterMode";
const WalletPage = lazy(() =>
import("./pages/WalletPage").then((module) => ({
default: module.WalletPage,
})),
);
const adminEnabled = import.meta.env.VITE_DISABLE_ADMIN !== "true"; const adminEnabled = import.meta.env.VITE_DISABLE_ADMIN !== "true";
export default function App() { export default function App() {
@@ -28,7 +34,14 @@ export default function App() {
<Route path="/search" element={<SearchPage />} /> <Route path="/search" element={<SearchPage />} />
<Route path="/favorites" element={<FavoritesPage />} /> <Route path="/favorites" element={<FavoritesPage />} />
<Route path="/resource/:id" element={<ResourceDetail />} /> <Route path="/resource/:id" element={<ResourceDetail />} />
<Route path="/wallet" element={<WalletPage />} /> <Route
path="/wallet"
element={
<Suspense fallback={null}>
<WalletPage />
</Suspense>
}
/>
<Route path="/about" element={<AboutPage />} /> <Route path="/about" element={<AboutPage />} />
</Route> </Route>

View File

@@ -1,16 +1,16 @@
const FIGMA_ASSET_BASE = "/assets/ark-library/figma"; const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
export const recommendationCoverFallbacks = [ export const officialRecommendationCoverFallbacks = [
`${FIGMA_ASSET_BASE}/recommendation-1.png`, `${FIGMA_ASSET_BASE}/official-recommendation-1.png`,
`${FIGMA_ASSET_BASE}/recommendation-2.png`, `${FIGMA_ASSET_BASE}/official-recommendation-2.png`,
`${FIGMA_ASSET_BASE}/recommendation-3.png`, `${FIGMA_ASSET_BASE}/official-recommendation-3.png`,
`${FIGMA_ASSET_BASE}/recommendation-4.png`, `${FIGMA_ASSET_BASE}/official-recommendation-4.png`,
`${FIGMA_ASSET_BASE}/recommendation-5.png`, `${FIGMA_ASSET_BASE}/official-recommendation-5.png`,
] as const; ] as const;
export function FigmaBanner() { export function FigmaBanner() {
return ( return (
<picture className="block overflow-hidden border border-[#2a2a32] bg-black shadow-[0_24px_70px_rgba(0,0,0,0.18)] max-md:-mx-4 max-md:rounded-none max-md:border-x-0 md:rounded-xl"> <picture className="-mx-4 block overflow-hidden border border-[#2a2a32] bg-black shadow-[0_24px_70px_rgba(0,0,0,0.18)] min-[440px]:-mx-5 sm:-mx-6 md:mx-0 md:rounded-xl">
<source <source
media="(max-width: 439px)" media="(max-width: 439px)"
srcSet={`${FIGMA_ASSET_BASE}/banner-375.png`} srcSet={`${FIGMA_ASSET_BASE}/banner-375.png`}

View File

@@ -5,14 +5,14 @@ import { assetUrl, postJSON } from "../api";
import { useI18n } from "../i18n"; import { useI18n } from "../i18n";
import { useMemo } from "react"; import { useMemo } from "react";
import { formatDateYmd } from "../utils/format"; import { formatDateYmd } from "../utils/format";
import { recommendationCoverFallbacks } from "./FigmaBanner"; import { officialRecommendationCoverFallbacks } from "./FigmaBanner";
function isPlaceholderAsset(path: string | undefined | null) { function isPlaceholderAsset(path: string | undefined | null) {
return !path || path.includes("placeholder-cover"); return !path || path.includes("placeholder-cover");
} }
const CARD_CLASS = const CARD_CLASS =
"group flex w-[232px] shrink-0 flex-col overflow-hidden rounded-xl border border-ark-line bg-ark-panel transition hover:border-ark-gold/55 max-[439px]:w-[232px] min-[440px]:w-[230px] sm:w-[240px] lg:w-[246.4px]"; "group flex w-[232px] shrink-0 flex-col overflow-hidden rounded-xl border border-ark-line bg-ark-panel transition hover:border-ark-gold/55 max-[439px]:w-[232px] min-[440px]:w-[230px] sm:w-[240px] lg:w-[246.4px] min-[1100px]:max-xl:w-[273px] xl:w-[246.4px]";
export function RecommendedCard({ export function RecommendedCard({
r, r,
@@ -25,8 +25,8 @@ export function RecommendedCard({
const cover = useMemo(() => { const cover = useMemo(() => {
const original = r.coverImage || r.previewUrl; const original = r.coverImage || r.previewUrl;
if (isPlaceholderAsset(original)) { if (isPlaceholderAsset(original)) {
return recommendationCoverFallbacks[ return officialRecommendationCoverFallbacks[
visualIndex % recommendationCoverFallbacks.length visualIndex % officialRecommendationCoverFallbacks.length
]; ];
} }
return assetUrl(original); return assetUrl(original);
@@ -105,8 +105,8 @@ export function ComingSoonRecommendedCard({
visualIndex?: number; visualIndex?: number;
}) { }) {
const cover = const cover =
recommendationCoverFallbacks[ officialRecommendationCoverFallbacks[
visualIndex % recommendationCoverFallbacks.length visualIndex % officialRecommendationCoverFallbacks.length
]; ];
return ( return (

View File

@@ -77,7 +77,7 @@ export function PublicLayout() {
return ( return (
<div className="min-h-full flex flex-col pb-20 md:pb-0"> <div className="min-h-full flex flex-col pb-20 md:pb-0">
<header className="sticky top-0 z-40 border-b border-ark-line bg-ark-nav/98 backdrop-blur-md"> <header className="sticky top-0 z-40 border-b border-ark-line bg-ark-nav/98 backdrop-blur-md">
<div className="mx-auto max-w-[1280px] px-4 py-[15px] md:px-8 xl:px-0"> <div className="mx-auto max-w-[1280px] px-4 py-[15px] min-[440px]:px-5 sm:px-6 md:px-9 xl:px-0">
{/* Single row (md+): logo | scrollable nav (左對齊,可橫向滑動) | 搜尋 + 語言 */} {/* Single row (md+): logo | scrollable nav (左對齊,可橫向滑動) | 搜尋 + 語言 */}
<div className="flex h-10 items-center gap-2 lg:gap-4"> <div className="flex h-10 items-center gap-2 lg:gap-4">
<Link <Link
@@ -85,13 +85,13 @@ export function PublicLayout() {
className="flex min-w-0 shrink-0 items-center gap-2.5 rounded-sm text-xl font-bold tracking-wide text-ark-gold outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg" className="flex min-w-0 shrink-0 items-center gap-2.5 rounded-sm text-xl font-bold tracking-wide text-ark-gold outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg"
> >
<ArkLogoMark className="h-10 w-10 shrink-0" /> <ArkLogoMark className="h-10 w-10 shrink-0" />
<span className="max-w-[9rem] truncate text-ark-gold sm:inline md:max-w-[10rem] lg:max-w-none"> <span className="max-w-[7.125rem] truncate text-ark-gold sm:inline">
{t("brand")} {t("brand")}
</span> </span>
</Link> </Link>
<nav <nav
className="header-nav-scroll hidden min-w-0 flex-1 items-center justify-center gap-4 overflow-x-auto overflow-y-hidden py-1 md:flex lg:gap-5" className="header-nav-scroll hidden min-w-0 flex-1 items-center justify-center gap-4 overflow-x-auto overflow-y-hidden py-1 min-[1200px]:flex lg:gap-5"
aria-label={t("mainNav")} aria-label={t("mainNav")}
> >
<Link <Link
@@ -152,15 +152,15 @@ export function PublicLayout() {
</Link> </Link>
</nav> </nav>
<div className="flex shrink-0 items-center justify-end gap-2"> <div className="flex min-w-0 flex-1 items-center justify-end gap-2 min-[1200px]:flex-none">
<div className="hidden h-10 items-center gap-2 rounded-full border border-ark-line bg-[#1a1b20] py-2 pl-3 pr-3 shadow-inner md:flex lg:pr-4"> <div className="hidden h-10 min-w-0 flex-1 items-center gap-2 rounded-full border border-ark-line bg-[#1a1b20] py-2 pl-3 pr-3 shadow-inner md:flex min-[1200px]:w-44 min-[1200px]:flex-none lg:pr-4 xl:w-52">
<SearchIcon size={16} className="shrink-0 text-[#c6c7cf]" /> <SearchIcon size={16} className="shrink-0 text-[#c6c7cf]" />
<input <input
value={q} value={q}
onChange={(e) => setQ(e.target.value)} onChange={(e) => setQ(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && goSearch()} onKeyDown={(e) => e.key === "Enter" && goSearch()}
placeholder={t("searchPlaceholder")} placeholder={t("searchPlaceholder")}
className="w-24 rounded-md bg-transparent text-sm text-neutral-200 outline-none placeholder:text-[#777985] focus-visible:ring-2 focus-visible:ring-ark-gold/60 focus-visible:ring-offset-2 focus-visible:ring-offset-[#1a1b20] md:w-28 lg:w-44 xl:w-52" className="min-w-0 flex-1 rounded-md bg-transparent text-sm text-neutral-200 outline-none placeholder:text-[#777985] focus-visible:ring-2 focus-visible:ring-ark-gold/60 focus-visible:ring-offset-2 focus-visible:ring-offset-[#1a1b20]"
/> />
</div> </div>
<div className="hidden h-10 items-center gap-2 rounded-full border border-ark-line bg-[#1a1b20] px-2 py-2 md:flex lg:px-3"> <div className="hidden h-10 items-center gap-2 rounded-full border border-ark-line bg-[#1a1b20] px-2 py-2 md:flex lg:px-3">
@@ -182,7 +182,7 @@ export function PublicLayout() {
</div> </div>
<button <button
type="button" type="button"
className="md:hidden inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-ark-line bg-[#1a1b20] text-neutral-200 outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg" className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-ark-line bg-[#1a1b20] text-neutral-200 outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg min-[1200px]:hidden"
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
aria-label="menu" aria-label="menu"
> >
@@ -193,7 +193,7 @@ export function PublicLayout() {
</div> </div>
{open ? ( {open ? (
<div className="md:hidden border-t border-ark-line bg-ark-nav px-4 py-3 grid gap-2"> <div className="grid gap-2 border-t border-ark-line bg-ark-nav px-4 py-3 min-[440px]:px-5 sm:px-6 md:px-9 min-[1200px]:hidden">
<div className="mb-1 flex items-center gap-2 rounded-full border border-ark-line bg-[#1a1b20] px-3 py-2"> <div className="mb-1 flex items-center gap-2 rounded-full border border-ark-line bg-[#1a1b20] px-3 py-2">
<SearchIcon size={16} className="shrink-0 text-[#c6c7cf]" /> <SearchIcon size={16} className="shrink-0 text-[#c6c7cf]" />
<input <input
@@ -280,12 +280,12 @@ export function PublicLayout() {
) : null} ) : null}
</header> </header>
<main className="mx-auto w-full max-w-[1280px] flex-1 px-4 py-6 md:px-8 md:py-10 xl:px-0"> <main className="mx-auto w-full max-w-[1280px] flex-1 px-4 py-6 min-[440px]:px-5 sm:px-6 md:px-9 md:py-10 xl:px-0">
<Outlet /> <Outlet />
</main> </main>
<footer className="mt-auto border-t border-ark-line bg-ark-nav/90 mb-20 md:mb-0"> <footer className="mt-auto border-t border-ark-line bg-ark-nav/90 mb-20 md:mb-0">
<div className="mx-auto flex max-w-[1280px] flex-wrap gap-x-6 gap-y-2 px-4 py-6 text-sm text-neutral-400 md:px-8 xl:px-0"> <div className="mx-auto flex max-w-[1280px] flex-wrap gap-x-6 gap-y-2 px-4 py-6 text-sm text-neutral-400 min-[440px]:px-5 sm:px-6 md:px-9 xl:px-0">
<Link <Link
to="/about" to="/about"
className="rounded-sm outline-none hover:text-ark-gold2 focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg" className="rounded-sm outline-none hover:text-ark-gold2 focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg"

View File

@@ -1,11 +1,7 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";
import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit";
import "./index.css"; import "./index.css";
import "@rainbow-me/rainbowkit/styles.css";
import { wagmiConfig } from "./wagmiConfig";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -27,20 +23,9 @@ void (async () => {
const { default: App } = await import("./App"); const { default: App } = await import("./App");
ReactDOM.createRoot(root).render( ReactDOM.createRoot(root).render(
<React.StrictMode> <React.StrictMode>
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<RainbowKitProvider
theme={darkTheme({
accentColor: "#d4af37",
accentColorForeground: "#0a0a0a",
borderRadius: "medium",
})}
modalSize="wide"
>
<App /> <App />
</RainbowKitProvider>
</QueryClientProvider> </QueryClientProvider>
</WagmiProvider>
</React.StrictMode>, </React.StrictMode>,
); );
})(); })();

View File

@@ -58,7 +58,7 @@ export function Home() {
} }
return ( return (
<div className="space-y-12 pb-10 md:space-y-14 md:pb-16"> <div className="space-y-[30px] pb-10 md:space-y-10 md:pb-16 xl:space-y-[34px]">
<section className="-mt-6 md:mt-0"> <section className="-mt-6 md:mt-0">
<FigmaBanner /> <FigmaBanner />
</section> </section>
@@ -69,7 +69,7 @@ export function Home() {
viewAllTo="/browse" viewAllTo="/browse"
viewAllLabel={t("viewAll")} viewAllLabel={t("viewAll")}
/> />
<div className="mt-7 grid grid-cols-3 gap-3 min-[440px]:gap-3.5 md:grid-cols-5 md:gap-3 xl:grid-cols-7 xl:gap-4"> <div className="mt-7 grid grid-cols-3 gap-3 min-[440px]:gap-3.5 md:grid-cols-5 md:gap-3 lg:grid-cols-6 xl:grid-cols-7 xl:gap-4">
{cats.map((c) => { {cats.map((c) => {
const { line1, line2 } = categoryCardLines(c.name); const { line1, line2 } = categoryCardLines(c.name);
return ( return (

View File

@@ -1,5 +1,9 @@
import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit";
import { WagmiProvider } from "wagmi";
import "@rainbow-me/rainbowkit/styles.css";
import { WalletLoginControls } from "../components/WalletLoginControls"; import { WalletLoginControls } from "../components/WalletLoginControls";
import { useI18n } from "../i18n"; import { useI18n } from "../i18n";
import { wagmiConfig } from "../wagmiConfig";
export function WalletPage() { export function WalletPage() {
const { t } = useI18n(); const { t } = useI18n();
@@ -16,7 +20,27 @@ export function WalletPage() {
<li>{t("walletStepSign")}</li> <li>{t("walletStepSign")}</li>
</ul> </ul>
<div className="rounded-2xl border border-ark-line bg-ark-panel p-6 space-y-4"> <div className="rounded-2xl border border-ark-line bg-ark-panel p-6 space-y-4">
{import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ? (
<WagmiProvider config={wagmiConfig}>
<RainbowKitProvider
theme={darkTheme({
accentColor: "#d4af37",
accentColorForeground: "#0a0a0a",
borderRadius: "medium",
})}
modalSize="wide"
>
<WalletLoginControls /> <WalletLoginControls />
</RainbowKitProvider>
</WagmiProvider>
) : (
<p
className="text-sm text-amber-500/90 leading-relaxed"
title={t("walletMissingProjectId")}
>
{t("walletSetupNeeded")}
</p>
)}
</div> </div>
</div> </div>
); );