diff --git a/src/components/LatestUpdateRow.tsx b/src/components/LatestUpdateRow.tsx index 9fd9a4d..273991e 100644 --- a/src/components/LatestUpdateRow.tsx +++ b/src/components/LatestUpdateRow.tsx @@ -27,11 +27,11 @@ export function LatestUpdateRow({ className="h-10 w-10 text-ark-gold" /> -
+
{r.title}
-
+
{r.categoryName} {resourceTypeLabel(t, r.type)} @@ -58,11 +58,11 @@ export function ComingSoonLatestUpdateRow({ index = 0 }: { index?: number }) {
-
+
即将到来
-
+
更多内容准备中 Coming soon
diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index 5774104..5654448 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -79,13 +79,13 @@ export function PublicLayout() {
{/* Single row (md+): logo | scrollable nav (左對齊,可橫向滑動) | 搜尋 + 語言 */} -
+
- + {t("brand")} diff --git a/src/pages/Browse.tsx b/src/pages/Browse.tsx index 36eaf8a..066d4f6 100644 --- a/src/pages/Browse.tsx +++ b/src/pages/Browse.tsx @@ -53,13 +53,40 @@ export function Browse() { useEffect(() => { setErr(null); + + if (sort === "recommended") { + const p = new URLSearchParams(); + p.set("lang", lang); + p.set("limit", "100"); + + getJSON<{ items: Resource[] }>(`/api/resources/recommended?${p}`) + .then((r) => { + const tagLower = tag.toLowerCase(); + const officialItems = itemsOrEmpty(r.items) + .filter((item) => item.isRecommended) + .filter((item) => type === "all" || item.type === type) + .filter((item) => !resourceLang || item.language === resourceLang) + .filter( + (item) => + !tagLower || + item.tags?.some( + (itemTag) => itemTag.toLowerCase() === tagLower, + ), + ); + setTotal(officialItems.length); + setItems(officialItems.slice((page - 1) * limit, page * limit)); + }) + .catch((e) => setErr(String(e))); + return; + } + getJSON<{ items: Resource[]; total?: number }>(`/api/resources?${query}`) .then((r) => { setItems(itemsOrEmpty(r.items)); setTotal(typeof r.total === "number" ? r.total : 0); }) .catch((e) => setErr(String(e))); - }, [query]); + }, [lang, limit, page, query, resourceLang, sort, tag, type]); const setPage = (next: number) => { const n = new URLSearchParams(sp); diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 23d415d..2b3dbfb 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -8,10 +8,7 @@ import { ComingSoonLatestUpdateRow, LatestUpdateRow, } from "../components/LatestUpdateRow"; -import { - ComingSoonRecommendedCard, - RecommendedCard, -} from "../components/RecommendedCard"; +import { RecommendedCard } from "../components/RecommendedCard"; import { SectionHeader } from "../components/SectionHeader"; import { useI18n } from "../i18n"; import { categoryCardLines } from "../utils/categoryDisplay"; @@ -23,6 +20,7 @@ export function Home() { const [latest, setLatest] = useState([]); const [err, setErr] = useState(null); const recRowRef = useRef(null); + const [canScrollRec, setCanScrollRec] = useState(false); useEffect(() => { const q = `?lang=${encodeURIComponent(lang)}`; @@ -42,11 +40,27 @@ export function Home() { const iconKeyForResource = (r: Resource) => cats.find((c) => c.id === r.categoryId)?.iconKey ?? "folder"; + useEffect(() => { + const row = recRowRef.current; + if (!row) { + setCanScrollRec(false); + return; + } + + const updateCanScroll = () => { + setCanScrollRec(row.scrollWidth > row.clientWidth + 1); + }; + + updateCanScroll(); + const resizeObserver = new ResizeObserver(updateCanScroll); + resizeObserver.observe(row); + return () => resizeObserver.disconnect(); + }, [rec.length]); + const scrollRec = (dir: 1 | -1) => { recRowRef.current?.scrollBy({ left: dir * 280, behavior: "smooth" }); }; - const recommendedPlaceholderCount = Math.max(0, 5 - rec.length); const latestPlaceholderCount = Math.max(0, 5 - latest.length); if (err) { @@ -115,28 +129,20 @@ export function Home() {
))} - {Array.from({ length: recommendedPlaceholderCount }).map( - (_, index) => ( -
- -
- ), - )}
- + {canScrollRec ? ( + + ) : null}
diff --git a/vite.config.ts b/vite.config.ts index d6005c8..4f8619d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,23 +1,28 @@ -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; -export default defineConfig({ - plugins: [react()], - build: { - rollupOptions: { - output: { - // Entry script at site root (/index-[hash].js); lazy chunks + CSS stay under /assets/. - entryFileNames: "[name]-[hash].js", - chunkFileNames: "assets/[name]-[hash].js", - assetFileNames: "assets/[name]-[hash][extname]", +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); + const apiProxyTarget = env.DEV_API_PROXY_TARGET || "http://127.0.0.1:8080"; + + return { + plugins: [react()], + build: { + rollupOptions: { + output: { + // Entry script at site root (/index-[hash].js); lazy chunks + CSS stay under /assets/. + entryFileNames: "[name]-[hash].js", + chunkFileNames: "assets/[name]-[hash].js", + assetFileNames: "assets/[name]-[hash][extname]", + }, }, }, - }, - server: { - port: 5173, - proxy: { - "/api": { target: "http://127.0.0.1:8080", changeOrigin: true }, - "/uploads": { target: "http://127.0.0.1:8080", changeOrigin: true }, + server: { + port: 5173, + proxy: { + "/api": { target: apiProxyTarget, changeOrigin: true }, + "/uploads": { target: apiProxyTarget, changeOrigin: true }, + }, }, - }, + }; });