diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9b8d514 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 2ae8ba4..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,87 +0,0 @@ -/** - * This is intended to be a basic starting point for linting in your app. - * It relies on recommended configs out of the box for simplicity, but you can - * and should modify this configuration to best suit your team's needs. - */ - -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true, - }, - }, - env: { - browser: true, - commonjs: true, - es6: true, - }, - - // Base config - extends: ["eslint:recommended"], - - overrides: [ - // React - { - files: ["**/*.{js,jsx,ts,tsx}"], - plugins: ["react", "jsx-a11y"], - extends: [ - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "plugin:jsx-a11y/recommended", - ], - rules: { - "jsx-a11y/media-has-caption": "off", - "jsx-a11y/click-events-have-key-events": "off", - }, - settings: { - react: { - version: "detect", - }, - formComponents: ["Form"], - linkComponents: [ - { name: "Link", linkAttribute: "to" }, - { name: "NavLink", linkAttribute: "to" }, - ], - "import/resolver": { - typescript: {}, - }, - }, - }, - - // Typescript - { - files: ["**/*.{ts,tsx}"], - plugins: ["@typescript-eslint", "import"], - parser: "@typescript-eslint/parser", - settings: { - "import/internal-regex": "^~/", - "import/resolver": { - node: { - extensions: [".ts", ".tsx"], - }, - typescript: { - alwaysTryTypes: true, - }, - }, - }, - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - }, - - // Node - { - files: [".eslintrc.js"], - env: { - node: true, - }, - }, - ], -}; diff --git a/.gitignore b/.gitignore index 80ec311..039ee62 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -node_modules - -/.cache -/build +.DS_Store .env +/node_modules/ + +# React Router +/.react-router/ +/build/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..207bf93 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm ci + +FROM node:20-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev + +FROM node:20-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:20-alpine +COPY ./package.json package-lock.json /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/README.md b/README.md index 07a150a..d795828 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ ## 感谢 -本项目采用 Remix + React + TypeScript + Less + Tailwind 作为主要的技术栈,感谢所有开源库作者提供的解决方案! +本项目采用 React 19 + TypeScript + React Router 7 + Tailwind 4 作为主要的技术栈,感谢所有开源库作者提供的解决方案! diff --git a/app/app.css b/app/app.css new file mode 100644 index 0000000..adb54cd --- /dev/null +++ b/app/app.css @@ -0,0 +1,66 @@ +@import "tailwindcss"; + +@theme { + --font-mi: MiSans, ui-sans-serif, system-ui, sans-serif; + + --animate-spinner-bar: spinner-bar 6s linear infinite; + --animate-fade-in: fade-in 0.3s both; + --animate-fade-out: fade-out 0.3s both; + --animate-fade-in-left: fade-in-left 0.3s backwards; + --animate-fade-off-right: fade-off-right 0.3s forwards; + + @keyframes spinner-bar { + 0% { + width: 0%; + } + 100% { + width: 100%; + } + } + + @keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + @keyframes fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + + @keyframes fade-in-left { + 0% { + opacity: 0; + transform: translateX(1.5rem); + } + 100% { + opacity: 1; + transform: translateX(0); + } + } + + @keyframes fade-off-right { + 0% { + opacity: 1; + transform: translateX(0); + } + 100% { + opacity: 0; + transform: translateX(1.5rem); + } + } +} + +@media screen and (max-width: 639px) { + :root { + font-size: 14px; + } +} diff --git a/app/components/biz/gallery/image-box/index.tsx b/app/components/biz/gallery/image-box/index.tsx index 641b138..cf0e036 100644 --- a/app/components/biz/gallery/image-box/index.tsx +++ b/app/components/biz/gallery/image-box/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ /* eslint-disable jsx-a11y/click-events-have-key-events */ -import React, { useState, useRef, useImperativeHandle, forwardRef, Ref } from 'react'; +import React, { useState, useRef, useImperativeHandle, forwardRef, type Ref } from "react"; import { createPortal } from 'react-dom'; import { clsn } from '~/utils'; diff --git a/app/components/common/article/article.module.less b/app/components/common/article/article.module.css similarity index 85% rename from app/components/common/article/article.module.less rename to app/components/common/article/article.module.css index feed626..5647ffd 100644 --- a/app/components/common/article/article.module.less +++ b/app/components/common/article/article.module.css @@ -1,3 +1,5 @@ +@reference "tailwindcss"; + .article { line-height: 1.7; @@ -36,7 +38,8 @@ height: 4px; display: block; border-radius: 4px; - background-color: rgb(244 114 182 / var(--tw-text-opacity)); + + @apply bg-pink-400; } } @@ -50,7 +53,7 @@ } a { - color:rgb(244 114 182 / var(--tw-bg-opacity)); + @apply text-pink-400; } img { @@ -77,7 +80,7 @@ font-size: smaller; padding: .15rem .5rem; border-radius: .75rem; - background-color: rgb(165 243 252); + @apply bg-cyan-200; } ul { @@ -86,7 +89,7 @@ margin-left: 1.25rem; ::marker { - color: rgb(244 114 182 / var(--tw-bg-opacity)); + @apply text-pink-400; } } } diff --git a/app/components/common/article/index.tsx b/app/components/common/article/index.tsx index edd1dd5..dee08fd 100644 --- a/app/components/common/article/index.tsx +++ b/app/components/common/article/index.tsx @@ -1,5 +1,5 @@ import { clsn } from "~/utils"; -import styles from "./article.module.less"; +import styles from "./article.module.css"; interface ArticleProps { className?: string; diff --git a/app/components/layout/header.tsx b/app/components/layout/header.tsx index 2e32a26..a1f63c7 100644 --- a/app/components/layout/header.tsx +++ b/app/components/layout/header.tsx @@ -1,4 +1,4 @@ -import { NavLink } from "@remix-run/react"; +import { NavLink } from "react-router"; const navItems = [ { name: "首页", to: "/" }, diff --git a/app/components/common/spinner.tsx b/app/components/layout/spinner.tsx similarity index 82% rename from app/components/common/spinner.tsx rename to app/components/layout/spinner.tsx index cd5fe68..b2df4f2 100644 --- a/app/components/common/spinner.tsx +++ b/app/components/layout/spinner.tsx @@ -1,4 +1,4 @@ -import { useNavigation } from "@remix-run/react"; +import { useNavigation } from "react-router"; export default function SpinnerBar() { const navigation = useNavigation(); diff --git a/app/components/common/icons.tsx b/app/components/ui/icons.tsx similarity index 99% rename from app/components/common/icons.tsx rename to app/components/ui/icons.tsx index edb3821..543c7e0 100644 --- a/app/components/common/icons.tsx +++ b/app/components/ui/icons.tsx @@ -1,4 +1,4 @@ -import { SVGProps } from "react"; +import type { SVGProps } from "react"; type IconProps = SVGProps; diff --git a/app/components/common/pagination.tsx b/app/components/ui/pagination.tsx similarity index 85% rename from app/components/common/pagination.tsx rename to app/components/ui/pagination.tsx index b714733..d86024b 100644 --- a/app/components/common/pagination.tsx +++ b/app/components/ui/pagination.tsx @@ -55,16 +55,16 @@ function Pagination({ current = 3, total, size, onClick }: PaginationProps) { } return ( - onClick(item)} > {item} - + ); })} diff --git a/app/index.css b/app/index.css deleted file mode 100644 index 42e4801..0000000 --- a/app/index.css +++ /dev/null @@ -1,9 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@media screen and (max-width: 639px) { - :root { - font-size: 14px; - } -} diff --git a/app/root.tsx b/app/root.tsx index edf7b99..77f7215 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,82 +1,90 @@ -import type { LinksFunction } from "@remix-run/node"; import { + isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration, - isRouteErrorResponse, + useNavigate, useRouteError, -} from "@remix-run/react"; -import Header from "./components/layout/header"; -import Spinner from "./components/common/spinner"; -import Footer from "./components/layout/footer"; +} from "react-router"; + +import type { Route } from "./+types/root"; import { siteTitle } from "~/utils"; -import "./index.css"; +import "./app.css"; -export function ErrorBoundary() { - const error = useRouteError(); - const isRouteError = isRouteErrorResponse(error); - - const [statusCode, message] = (() => { - if (isRouteError) { - return [error.status, error.statusText]; - } - - if (error instanceof Error) { - return [500, error.message]; - } - - return [500, "未知异常"]; - })(); - - return ( - - - - - {siteTitle(statusCode)} - - - - -
-
-

- {statusCode} -

-

{message}

-
-