Compare commits
2 Commits
40143afc39
...
terry-stag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
353b387fbf | ||
|
|
e6bc212c4e |
@@ -72,8 +72,8 @@ src/
|
||||
adminRouteTree.tsx # admin routes
|
||||
components/ # reusable public components
|
||||
layouts/ # public/admin layout shells
|
||||
pages/ # public pages
|
||||
pages/admin/ # admin pages
|
||||
pages/ # public pages; one folder per page, e.g. Home/index.tsx
|
||||
pages/admin/ # admin pages; one folder per page, e.g. Resources/index.tsx
|
||||
utils/ # formatting/display helpers
|
||||
```
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ git pull --ff-only origin main
|
||||
|
||||
Use the existing structure:
|
||||
|
||||
- Public pages: `src/pages/`
|
||||
- Admin pages: `src/pages/admin/`
|
||||
- Public pages: `src/pages/<PageName>/index.tsx`
|
||||
- Admin pages: `src/pages/admin/<PageName>/index.tsx`
|
||||
- Shared components: `src/components/`
|
||||
- API helpers/types: `src/api.ts`
|
||||
- Translations/copy: `src/i18n.tsx`
|
||||
|
||||
10
src/App.tsx
10
src/App.tsx
@@ -4,17 +4,17 @@ import { I18nProvider } from "./i18n";
|
||||
import { PublicLayout } from "./layouts/PublicLayout";
|
||||
import { Home } from "./pages/Home";
|
||||
import { Browse } from "./pages/Browse";
|
||||
import { CategoryPage } from "./pages/CategoryPage";
|
||||
import { SearchPage } from "./pages/SearchPage";
|
||||
import { FavoritesPage } from "./pages/FavoritesPage";
|
||||
import { CategoryPage } from "./pages/Category";
|
||||
import { SearchPage } from "./pages/Search";
|
||||
import { FavoritesPage } from "./pages/Favorites";
|
||||
import { ResourceDetail } from "./pages/ResourceDetail";
|
||||
import { AboutPage } from "./pages/AboutPage";
|
||||
import { AboutPage } from "./pages/About";
|
||||
import { adminUiPrefix } from "./adminPaths";
|
||||
import { AdminRouteTree } from "./adminRouteTree";
|
||||
import { AdminRouterModeProvider } from "./adminRouterMode";
|
||||
|
||||
const WalletPage = lazy(() =>
|
||||
import("./pages/WalletPage").then((module) => ({
|
||||
import("./pages/Wallet").then((module) => ({
|
||||
default: module.WalletPage,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -3,11 +3,11 @@ import { I18nProvider } from "./i18n";
|
||||
import { adminUiPrefix } from "./adminPaths";
|
||||
import { AdminRouterModeProvider } from "./adminRouterMode";
|
||||
import { AdminLayout } from "./layouts/AdminLayout";
|
||||
import { AdminLogin } from "./pages/admin/AdminLogin";
|
||||
import { AdminDashboard } from "./pages/admin/AdminDashboard";
|
||||
import { AdminResources } from "./pages/admin/AdminResources";
|
||||
import { AdminResourceForm } from "./pages/admin/AdminResourceForm";
|
||||
import { AdminSearchLogs } from "./pages/admin/AdminSearchLogs";
|
||||
import { AdminLogin } from "./pages/admin/Login";
|
||||
import { AdminDashboard } from "./pages/admin/Dashboard";
|
||||
import { AdminResources } from "./pages/admin/Resources";
|
||||
import { AdminResourceForm } from "./pages/admin/ResourceForm";
|
||||
import { AdminSearchLogs } from "./pages/admin/SearchLogs";
|
||||
|
||||
function NotFound() {
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Route } from "react-router-dom";
|
||||
import { adminUiPrefix } from "./adminPaths";
|
||||
import { AdminLayout } from "./layouts/AdminLayout";
|
||||
import { AdminLogin } from "./pages/admin/AdminLogin";
|
||||
import { AdminDashboard } from "./pages/admin/AdminDashboard";
|
||||
import { AdminResources } from "./pages/admin/AdminResources";
|
||||
import { AdminResourceForm } from "./pages/admin/AdminResourceForm";
|
||||
import { AdminSearchLogs } from "./pages/admin/AdminSearchLogs";
|
||||
import { AdminLogin } from "./pages/admin/Login";
|
||||
import { AdminDashboard } from "./pages/admin/Dashboard";
|
||||
import { AdminResources } from "./pages/admin/Resources";
|
||||
import { AdminResourceForm } from "./pages/admin/ResourceForm";
|
||||
import { AdminSearchLogs } from "./pages/admin/SearchLogs";
|
||||
|
||||
/** Shared between full `App` (when admin enabled) and `AppAdminOnly`. */
|
||||
export function AdminRouteTree() {
|
||||
|
||||
@@ -27,11 +27,11 @@ export function LatestUpdateRow({
|
||||
className="h-10 w-10 text-ark-gold"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-0.5">
|
||||
<div className="flex min-w-0 flex-1 self-stretch py-0.5 flex-col">
|
||||
<div className="text-base font-bold leading-snug text-white line-clamp-2 md:text-lg">
|
||||
{r.title}
|
||||
</div>
|
||||
<div className="mt-4 grid gap-1 text-sm text-[#9b9ca6] md:mt-6">
|
||||
<div className="mt-auto grid gap-1 text-sm text-[#9b9ca6]">
|
||||
<span>{r.categoryName}</span>
|
||||
<span>
|
||||
{resourceTypeLabel(t, r.type)}
|
||||
@@ -58,11 +58,11 @@ export function ComingSoonLatestUpdateRow({ index = 0 }: { index?: number }) {
|
||||
<div className="flex shrink-0 items-center justify-center pt-0.5">
|
||||
<CategoryIcon iconKey={iconKey} className="h-10 w-10 text-ark-gold" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-0.5">
|
||||
<div className="flex min-w-0 flex-1 self-stretch py-0.5 flex-col">
|
||||
<div className="text-base font-bold leading-snug text-white line-clamp-2 md:text-lg">
|
||||
即将到来
|
||||
</div>
|
||||
<div className="mt-4 grid gap-1 text-sm text-[#9b9ca6] md:mt-6">
|
||||
<div className="mt-auto grid gap-1 text-sm text-[#9b9ca6]">
|
||||
<span>更多内容准备中</span>
|
||||
<span>Coming soon</span>
|
||||
</div>
|
||||
|
||||
@@ -79,13 +79,13 @@ export function PublicLayout() {
|
||||
<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] 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">
|
||||
<div className="flex h-10 items-center gap-2 min-[1200px]:gap-0 lg:gap-4">
|
||||
<Link
|
||||
to="/"
|
||||
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-[7.125rem] truncate text-ark-gold sm:inline">
|
||||
<span className="max-w-[8rem] truncate text-ark-gold sm:inline">
|
||||
{t("brand")}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useI18n } from "../i18n";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
||||
export function AboutPage() {
|
||||
const { t } = useI18n();
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { getJSON, itemsOrEmpty, type Resource } from "../api";
|
||||
import { ResourceCard } from "../components/ResourceCard";
|
||||
import { ResourceListFooter } from "../components/ResourceListFooter";
|
||||
import { useI18n } from "../i18n";
|
||||
import { typeFilterLabel } from "../resourceTypeLabels";
|
||||
import { getJSON, itemsOrEmpty, type Resource } from "../../api";
|
||||
import { ResourceCard } from "../../components/ResourceCard";
|
||||
import { ResourceListFooter } from "../../components/ResourceListFooter";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { typeFilterLabel } from "../../resourceTypeLabels";
|
||||
|
||||
const types = [
|
||||
"all",
|
||||
@@ -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);
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { getJSON, itemsOrEmpty, type Category, type Resource } from "../api";
|
||||
import { ResourceCard } from "../components/ResourceCard";
|
||||
import { ResourceListFooter } from "../components/ResourceListFooter";
|
||||
import { useI18n } from "../i18n";
|
||||
import { typeFilterLabel } from "../resourceTypeLabels";
|
||||
import { getJSON, itemsOrEmpty, type Category, type Resource } from "../../api";
|
||||
import { ResourceCard } from "../../components/ResourceCard";
|
||||
import { ResourceListFooter } from "../../components/ResourceListFooter";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { typeFilterLabel } from "../../resourceTypeLabels";
|
||||
|
||||
const TYPE_FILTERS = [
|
||||
"all",
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getJSON, type Resource } from "../api";
|
||||
import { ResourceCard } from "../components/ResourceCard";
|
||||
import { readFavorites } from "../favorites";
|
||||
import { useI18n } from "../i18n";
|
||||
import { getJSON, type Resource } from "../../api";
|
||||
import { ResourceCard } from "../../components/ResourceCard";
|
||||
import { readFavorites } from "../../favorites";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
||||
export function FavoritesPage() {
|
||||
const { t, lang } = useI18n();
|
||||
@@ -1,20 +1,17 @@
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { getJSON, itemsOrEmpty, type Category, type Resource } from "../api";
|
||||
import { CategoryIcon } from "../components/CategoryIcon";
|
||||
import { FigmaBanner } from "../components/FigmaBanner";
|
||||
import { getJSON, itemsOrEmpty, type Category, type Resource } from "../../api";
|
||||
import { CategoryIcon } from "../../components/CategoryIcon";
|
||||
import { FigmaBanner } from "../../components/FigmaBanner";
|
||||
import {
|
||||
ComingSoonLatestUpdateRow,
|
||||
LatestUpdateRow,
|
||||
} from "../components/LatestUpdateRow";
|
||||
import {
|
||||
ComingSoonRecommendedCard,
|
||||
RecommendedCard,
|
||||
} from "../components/RecommendedCard";
|
||||
import { SectionHeader } from "../components/SectionHeader";
|
||||
import { useI18n } from "../i18n";
|
||||
import { categoryCardLines } from "../utils/categoryDisplay";
|
||||
} from "../../components/LatestUpdateRow";
|
||||
import { RecommendedCard } from "../../components/RecommendedCard";
|
||||
import { SectionHeader } from "../../components/SectionHeader";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { categoryCardLines } from "../../utils/categoryDisplay";
|
||||
|
||||
export function Home() {
|
||||
const { t, lang } = useI18n();
|
||||
@@ -23,6 +20,7 @@ export function Home() {
|
||||
const [latest, setLatest] = useState<Resource[]>([]);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const recRowRef = useRef<HTMLDivElement>(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() {
|
||||
<RecommendedCard r={r} visualIndex={index} />
|
||||
</div>
|
||||
))}
|
||||
{Array.from({ length: recommendedPlaceholderCount }).map(
|
||||
(_, index) => (
|
||||
<div
|
||||
key={`recommended-coming-soon-${index}`}
|
||||
className="snap-start"
|
||||
>
|
||||
<ComingSoonRecommendedCard visualIndex={rec.length + index} />
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div className="h-1 rounded-full bg-black/80 md:hidden">
|
||||
<div className="h-full w-24 rounded-full bg-[#353740]" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => scrollRec(1)}
|
||||
className="absolute right-0 top-[45%] hidden h-9 w-9 -translate-y-1/2 items-center justify-center rounded-lg border border-ark-line bg-[#292a31]/95 text-neutral-200 shadow-lg backdrop-blur transition hover:border-ark-gold hover:text-ark-gold md:flex"
|
||||
aria-label={t("viewAll")}
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</button>
|
||||
{canScrollRec ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => scrollRec(1)}
|
||||
className="absolute right-0 top-[45%] hidden h-9 w-9 -translate-y-1/2 items-center justify-center rounded-lg border border-ark-line bg-[#292a31]/95 text-neutral-200 shadow-lg backdrop-blur transition hover:border-ark-gold hover:text-ark-gold md:flex"
|
||||
aria-label={t("viewAll")}
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -8,15 +8,15 @@ import {
|
||||
postJSON,
|
||||
postFavoriteDelta,
|
||||
type Resource,
|
||||
} from "../api";
|
||||
} from "../../api";
|
||||
import {
|
||||
resourceLanguageLabel,
|
||||
resourceTypeLabel,
|
||||
} from "../resourceTypeLabels";
|
||||
import { ResourceCard } from "../components/ResourceCard";
|
||||
import { isFavorite, toggleFavorite } from "../favorites";
|
||||
import { useI18n } from "../i18n";
|
||||
import { isLikelyVideoPath } from "../video";
|
||||
} from "../../resourceTypeLabels";
|
||||
import { ResourceCard } from "../../components/ResourceCard";
|
||||
import { isFavorite, toggleFavorite } from "../../favorites";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { isLikelyVideoPath } from "../../video";
|
||||
|
||||
export function ResourceDetail() {
|
||||
const { id } = useParams();
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { getJSON, itemsOrEmpty, postJSON, type Resource } from "../api";
|
||||
import { ResourceCard } from "../components/ResourceCard";
|
||||
import { ResourceListFooter } from "../components/ResourceListFooter";
|
||||
import { useI18n } from "../i18n";
|
||||
import { typeFilterLabel } from "../resourceTypeLabels";
|
||||
import { getJSON, itemsOrEmpty, postJSON, type Resource } from "../../api";
|
||||
import { ResourceCard } from "../../components/ResourceCard";
|
||||
import { ResourceListFooter } from "../../components/ResourceListFooter";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { typeFilterLabel } from "../../resourceTypeLabels";
|
||||
|
||||
const types = [
|
||||
"all",
|
||||
@@ -1,9 +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";
|
||||
import { WalletLoginControls } from "../../components/WalletLoginControls";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { wagmiConfig } from "../../wagmiConfig";
|
||||
|
||||
export function WalletPage() {
|
||||
const { t } = useI18n();
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getJSONAuth } from "../../api";
|
||||
import { getToken } from "../../admin/token";
|
||||
import { useAdminT } from "../../admin/useAdminT";
|
||||
import { getJSONAuth } from "../../../api";
|
||||
import { getToken } from "../../../admin/token";
|
||||
import { useAdminT } from "../../../admin/useAdminT";
|
||||
|
||||
type Dash = {
|
||||
totalResources: number;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { postJSON } from "../../api";
|
||||
import { setToken } from "../../admin/token";
|
||||
import { useAdminT } from "../../admin/useAdminT";
|
||||
import { useAdminRouterMode } from "../../adminRouterMode";
|
||||
import { adminUiPrefix } from "../../adminPaths";
|
||||
import { postJSON } from "../../../api";
|
||||
import { setToken } from "../../../admin/token";
|
||||
import { useAdminT } from "../../../admin/useAdminT";
|
||||
import { useAdminRouterMode } from "../../../adminRouterMode";
|
||||
import { adminUiPrefix } from "../../../adminPaths";
|
||||
|
||||
export function AdminLogin() {
|
||||
const t = useAdminT();
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
putJSON,
|
||||
uploadFile,
|
||||
type Category,
|
||||
} from "../../api";
|
||||
import { getToken } from "../../admin/token";
|
||||
import { useAdminT } from "../../admin/useAdminT";
|
||||
import { resourceTypeDisplay } from "../../resourceTypeLabels";
|
||||
import { adminUiPrefix } from "../../adminPaths";
|
||||
import { useAdminRouterMode } from "../../adminRouterMode";
|
||||
} from "../../../api";
|
||||
import { getToken } from "../../../admin/token";
|
||||
import { useAdminT } from "../../../admin/useAdminT";
|
||||
import { resourceTypeDisplay } from "../../../resourceTypeLabels";
|
||||
import { adminUiPrefix } from "../../../adminPaths";
|
||||
import { useAdminRouterMode } from "../../../adminRouterMode";
|
||||
|
||||
const types = [
|
||||
"image",
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
itemsOrEmpty,
|
||||
type AdminResource,
|
||||
type Category,
|
||||
} from "../../api";
|
||||
import { getToken } from "../../admin/token";
|
||||
import { resourceTypeDisplay } from "../../resourceTypeLabels";
|
||||
import { useAdminT } from "../../admin/useAdminT";
|
||||
import { adminUiPrefix } from "../../adminPaths";
|
||||
import { useAdminRouterMode } from "../../adminRouterMode";
|
||||
} from "../../../api";
|
||||
import { getToken } from "../../../admin/token";
|
||||
import { resourceTypeDisplay } from "../../../resourceTypeLabels";
|
||||
import { useAdminT } from "../../../admin/useAdminT";
|
||||
import { adminUiPrefix } from "../../../adminPaths";
|
||||
import { useAdminRouterMode } from "../../../adminRouterMode";
|
||||
|
||||
function statusLabel(t: (k: string) => string, s: string) {
|
||||
if (s === "published") return t("published");
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getJSONAuth } from "../../api";
|
||||
import { getToken } from "../../admin/token";
|
||||
import { useAdminT } from "../../admin/useAdminT";
|
||||
import { getJSONAuth } from "../../../api";
|
||||
import { getToken } from "../../../admin/token";
|
||||
import { useAdminT } from "../../../admin/useAdminT";
|
||||
|
||||
type Row = { id: number; query: string; createdAt: string };
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user