feat: apply figma responsive home design
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 49s
@@ -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-440.png` — mobile banner crop from node `3726:14199`.
|
||||
- `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.
|
||||
|
||||
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
17
src/App.tsx
@@ -1,3 +1,4 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||
import { I18nProvider } from "./i18n";
|
||||
import { PublicLayout } from "./layouts/PublicLayout";
|
||||
@@ -7,12 +8,17 @@ import { CategoryPage } from "./pages/CategoryPage";
|
||||
import { SearchPage } from "./pages/SearchPage";
|
||||
import { FavoritesPage } from "./pages/FavoritesPage";
|
||||
import { ResourceDetail } from "./pages/ResourceDetail";
|
||||
import { WalletPage } from "./pages/WalletPage";
|
||||
import { AboutPage } from "./pages/AboutPage";
|
||||
import { adminUiPrefix } from "./adminPaths";
|
||||
import { AdminRouteTree } from "./adminRouteTree";
|
||||
import { AdminRouterModeProvider } from "./adminRouterMode";
|
||||
|
||||
const WalletPage = lazy(() =>
|
||||
import("./pages/WalletPage").then((module) => ({
|
||||
default: module.WalletPage,
|
||||
})),
|
||||
);
|
||||
|
||||
const adminEnabled = import.meta.env.VITE_DISABLE_ADMIN !== "true";
|
||||
|
||||
export default function App() {
|
||||
@@ -28,7 +34,14 @@ export default function App() {
|
||||
<Route path="/search" element={<SearchPage />} />
|
||||
<Route path="/favorites" element={<FavoritesPage />} />
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
|
||||
|
||||
export const recommendationCoverFallbacks = [
|
||||
`${FIGMA_ASSET_BASE}/recommendation-1.png`,
|
||||
`${FIGMA_ASSET_BASE}/recommendation-2.png`,
|
||||
`${FIGMA_ASSET_BASE}/recommendation-3.png`,
|
||||
`${FIGMA_ASSET_BASE}/recommendation-4.png`,
|
||||
`${FIGMA_ASSET_BASE}/recommendation-5.png`,
|
||||
export const officialRecommendationCoverFallbacks = [
|
||||
`${FIGMA_ASSET_BASE}/official-recommendation-1.png`,
|
||||
`${FIGMA_ASSET_BASE}/official-recommendation-2.png`,
|
||||
`${FIGMA_ASSET_BASE}/official-recommendation-3.png`,
|
||||
`${FIGMA_ASSET_BASE}/official-recommendation-4.png`,
|
||||
`${FIGMA_ASSET_BASE}/official-recommendation-5.png`,
|
||||
] as const;
|
||||
|
||||
export function FigmaBanner() {
|
||||
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
|
||||
media="(max-width: 439px)"
|
||||
srcSet={`${FIGMA_ASSET_BASE}/banner-375.png`}
|
||||
|
||||
@@ -5,14 +5,14 @@ import { assetUrl, postJSON } from "../api";
|
||||
import { useI18n } from "../i18n";
|
||||
import { useMemo } from "react";
|
||||
import { formatDateYmd } from "../utils/format";
|
||||
import { recommendationCoverFallbacks } from "./FigmaBanner";
|
||||
import { officialRecommendationCoverFallbacks } from "./FigmaBanner";
|
||||
|
||||
function isPlaceholderAsset(path: string | undefined | null) {
|
||||
return !path || path.includes("placeholder-cover");
|
||||
}
|
||||
|
||||
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({
|
||||
r,
|
||||
@@ -25,8 +25,8 @@ export function RecommendedCard({
|
||||
const cover = useMemo(() => {
|
||||
const original = r.coverImage || r.previewUrl;
|
||||
if (isPlaceholderAsset(original)) {
|
||||
return recommendationCoverFallbacks[
|
||||
visualIndex % recommendationCoverFallbacks.length
|
||||
return officialRecommendationCoverFallbacks[
|
||||
visualIndex % officialRecommendationCoverFallbacks.length
|
||||
];
|
||||
}
|
||||
return assetUrl(original);
|
||||
@@ -105,8 +105,8 @@ export function ComingSoonRecommendedCard({
|
||||
visualIndex?: number;
|
||||
}) {
|
||||
const cover =
|
||||
recommendationCoverFallbacks[
|
||||
visualIndex % recommendationCoverFallbacks.length
|
||||
officialRecommendationCoverFallbacks[
|
||||
visualIndex % officialRecommendationCoverFallbacks.length
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -77,7 +77,7 @@ export function PublicLayout() {
|
||||
return (
|
||||
<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">
|
||||
<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 (左對齊,可橫向滑動) | 搜尋 + 語言 */}
|
||||
<div className="flex h-10 items-center gap-2 lg:gap-4">
|
||||
<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"
|
||||
>
|
||||
<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")}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<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")}
|
||||
>
|
||||
<Link
|
||||
@@ -152,15 +152,15 @@ export function PublicLayout() {
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-end gap-2">
|
||||
<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="flex min-w-0 flex-1 items-center justify-end gap-2 min-[1200px]:flex-none">
|
||||
<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]" />
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && goSearch()}
|
||||
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 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>
|
||||
<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)}
|
||||
aria-label="menu"
|
||||
>
|
||||
@@ -193,7 +193,7 @@ export function PublicLayout() {
|
||||
</div>
|
||||
|
||||
{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">
|
||||
<SearchIcon size={16} className="shrink-0 text-[#c6c7cf]" />
|
||||
<input
|
||||
@@ -280,12 +280,12 @@ export function PublicLayout() {
|
||||
) : null}
|
||||
</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 />
|
||||
</main>
|
||||
|
||||
<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
|
||||
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"
|
||||
|
||||
21
src/main.tsx
@@ -1,11 +1,7 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { WagmiProvider } from "wagmi";
|
||||
import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit";
|
||||
import "./index.css";
|
||||
import "@rainbow-me/rainbowkit/styles.css";
|
||||
import { wagmiConfig } from "./wagmiConfig";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -27,20 +23,9 @@ void (async () => {
|
||||
const { default: App } = await import("./App");
|
||||
ReactDOM.createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<WagmiProvider config={wagmiConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RainbowKitProvider
|
||||
theme={darkTheme({
|
||||
accentColor: "#d4af37",
|
||||
accentColorForeground: "#0a0a0a",
|
||||
borderRadius: "medium",
|
||||
})}
|
||||
modalSize="wide"
|
||||
>
|
||||
<App />
|
||||
</RainbowKitProvider>
|
||||
</QueryClientProvider>
|
||||
</WagmiProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -58,7 +58,7 @@ export function Home() {
|
||||
}
|
||||
|
||||
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">
|
||||
<FigmaBanner />
|
||||
</section>
|
||||
@@ -69,7 +69,7 @@ export function Home() {
|
||||
viewAllTo="/browse"
|
||||
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) => {
|
||||
const { line1, line2 } = categoryCardLines(c.name);
|
||||
return (
|
||||
|
||||
@@ -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 { useI18n } from "../i18n";
|
||||
import { wagmiConfig } from "../wagmiConfig";
|
||||
|
||||
export function WalletPage() {
|
||||
const { t } = useI18n();
|
||||
@@ -16,7 +20,27 @@ export function WalletPage() {
|
||||
<li>{t("walletStepSign")}</li>
|
||||
</ul>
|
||||
<div className="rounded-2xl border border-ark-line bg-ark-panel p-6 space-y-4">
|
||||
<WalletLoginControls />
|
||||
{import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ? (
|
||||
<WagmiProvider config={wagmiConfig}>
|
||||
<RainbowKitProvider
|
||||
theme={darkTheme({
|
||||
accentColor: "#d4af37",
|
||||
accentColorForeground: "#0a0a0a",
|
||||
borderRadius: "medium",
|
||||
})}
|
||||
modalSize="wide"
|
||||
>
|
||||
<WalletLoginControls />
|
||||
</RainbowKitProvider>
|
||||
</WagmiProvider>
|
||||
) : (
|
||||
<p
|
||||
className="text-sm text-amber-500/90 leading-relaxed"
|
||||
title={t("walletMissingProjectId")}
|
||||
>
|
||||
{t("walletSetupNeeded")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||