From 7dac5a8ff4102a5b22cf0515cb41f70b4eee354f Mon Sep 17 00:00:00 2001 From: LHgeek <1209384461@qq.com> Date: Wed, 11 Mar 2026 20:46:32 +0800 Subject: [PATCH] feat by vibe coding use siyu-0309-RL-S100 Signed-off-by: LHgeek <1209384461@qq.com> --- index.html | 812 ++++++++++++++++++++++++++ package-lock.json | 351 +++-------- package.json | 1 + src/App.tsx | 19 +- src/components/AnimatedPage.tsx | 196 +++++++ src/components/BottomNav.tsx | 59 ++ src/components/Header.tsx | 18 +- src/components/Layout.tsx | 19 +- src/components/PageTransition.tsx | 34 ++ src/components/SwipeBack.tsx | 180 ++++++ src/components/SwipeBackContainer.tsx | 199 +++++++ src/components/index.ts | 3 + src/data/mockData.ts | 272 +++++++++ src/data/mockTopics.ts | 76 +++ src/hooks/useSwipeBack.ts | 135 +++++ src/interfaces/index.d.ts | 11 + src/pages/topic/detail.tsx | 231 ++++++++ src/pages/topic/index.tsx | 212 +++++++ test-animate.ts | 10 + 19 files changed, 2537 insertions(+), 301 deletions(-) create mode 100644 src/components/AnimatedPage.tsx create mode 100644 src/components/BottomNav.tsx create mode 100644 src/components/PageTransition.tsx create mode 100644 src/components/SwipeBack.tsx create mode 100644 src/components/SwipeBackContainer.tsx create mode 100644 src/data/mockData.ts create mode 100644 src/data/mockTopics.ts create mode 100644 src/hooks/useSwipeBack.ts create mode 100644 src/pages/topic/detail.tsx create mode 100644 src/pages/topic/index.tsx create mode 100644 test-animate.ts diff --git a/index.html b/index.html index d49c56d..7650c49 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,818 @@ + diff --git a/package-lock.json b/package-lock.json index 02e8c03..1a43c4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@refinedev/simple-rest": "^4.5.0", "axios": "^0.26.1", "dayjs": "^1.10.7", + "framer-motion": "^12.35.2", "react": "^18.0.0", "react-dom": "^18.0.0", "react-router-dom": "^6.8.1" @@ -2337,34 +2338,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", @@ -2587,52 +2560,6 @@ } } }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "optional": true, - "peer": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "optional": true, - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", - "optional": true, - "peer": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "optional": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, "node_modules/@types/http-proxy": { "version": "1.17.9", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", @@ -2641,13 +2568,6 @@ "@types/node": "*" } }, - "node_modules/@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", - "optional": true, - "peer": true - }, "node_modules/@types/node": { "version": "18.17.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.1.tgz", @@ -2658,20 +2578,6 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "optional": true, - "peer": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "optional": true, - "peer": true - }, "node_modules/@types/react": { "version": "18.0.21", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.21.tgz", @@ -2695,17 +2601,6 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, - "node_modules/@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", - "optional": true, - "peer": true, - "dependencies": { - "@types/mime": "*", - "@types/node": "*" - } - }, "node_modules/@types/yoga-layout": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", @@ -2750,20 +2645,6 @@ "node": ">= 0.6" } }, - "node_modules/acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ajv-formats": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", @@ -4442,6 +4323,33 @@ "node": ">=0.10.0" } }, + "node_modules/framer-motion": { + "version": "12.35.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.2.tgz", + "integrity": "sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.35.2", + "motion-utils": "^12.29.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -5698,6 +5606,21 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/motion-dom": { + "version": "12.35.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.35.2.tgz", + "integrity": "sha512-pWXFMTwvGDbx1Fe9YL5HZebv2NhvGBzRtiNUv58aoK7+XrsuaydQ0JGRKK2r+bTKlwgSWwWxHbP5249Qr/BNpg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7515,34 +7438,6 @@ "rimraf": "bin.js" } }, - "node_modules/terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -9579,33 +9474,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" }, - "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - } - } - }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", @@ -9753,52 +9621,6 @@ "use-sync-external-store": "^1.2.0" } }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "optional": true, - "peer": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "optional": true, - "peer": true, - "requires": { - "@types/node": "*" - } - }, - "@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", - "optional": true, - "peer": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "optional": true, - "peer": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, "@types/http-proxy": { "version": "1.17.9", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", @@ -9807,13 +9629,6 @@ "@types/node": "*" } }, - "@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", - "optional": true, - "peer": true - }, "@types/node": { "version": "18.17.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.1.tgz", @@ -9824,20 +9639,6 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "optional": true, - "peer": true - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "optional": true, - "peer": true - }, "@types/react": { "version": "18.0.21", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.21.tgz", @@ -9861,17 +9662,6 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, - "@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", - "optional": true, - "peer": true, - "requires": { - "@types/mime": "*", - "@types/node": "*" - } - }, "@types/yoga-layout": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", @@ -9906,14 +9696,6 @@ "negotiator": "0.6.3" } }, - "acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true, - "optional": true, - "peer": true - }, "ajv-formats": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", @@ -11139,6 +10921,16 @@ "map-cache": "^0.2.2" } }, + "framer-motion": { + "version": "12.35.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.2.tgz", + "integrity": "sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA==", + "requires": { + "motion-dom": "^12.35.2", + "motion-utils": "^12.29.2", + "tslib": "^2.4.0" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -12042,6 +11834,19 @@ "minimist": "^1.2.6" } }, + "motion-dom": { + "version": "12.35.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.35.2.tgz", + "integrity": "sha512-pWXFMTwvGDbx1Fe9YL5HZebv2NhvGBzRtiNUv58aoK7+XrsuaydQ0JGRKK2r+bTKlwgSWwWxHbP5249Qr/BNpg==", + "requires": { + "motion-utils": "^12.29.2" + } + }, + "motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -13375,30 +13180,6 @@ } } }, - "terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - } - } - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index b260c2c..97acb69 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@refinedev/simple-rest": "^4.5.0", "axios": "^0.26.1", "dayjs": "^1.10.7", + "framer-motion": "^12.35.2", "react": "^18.0.0", "react-dom": "^18.0.0", "react-router-dom": "^6.8.1" diff --git a/src/App.tsx b/src/App.tsx index 09a4d53..f6ab721 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ -import { Authenticated, ErrorComponent, HttpError, Refine } from "@refinedev/core"; +import { ErrorComponent, HttpError, Refine } from "@refinedev/core"; import routerProvider from "@refinedev/react-router-v6"; -import { HashRouter, Routes, Route, Outlet, Navigate } from "react-router-dom"; +import { HashRouter, Routes, Route } from "react-router-dom"; import axios, { AxiosRequestConfig } from "axios"; import { authProvider } from "./authProvider"; @@ -13,6 +13,8 @@ import { ProfilePage } from "./pages/profile"; import { SettingsPage } from "./pages/settings"; import { EditorPage, EditArticlePage } from "./pages/editor"; import { ArticlePage } from "./pages/article"; +import { TopicListPage } from "./pages/topic"; +import { TopicDetailPage } from "./pages/topic/detail"; import { TOKEN_KEY } from "./constants"; @@ -60,25 +62,18 @@ function App() { authProvider={authProvider(axiosInstance)} > - - - - } - > + }> } /> - } /> } /> } /> } /> } /> } /> - + } /> + } /> } /> } /> - } /> diff --git a/src/components/AnimatedPage.tsx b/src/components/AnimatedPage.tsx new file mode 100644 index 0000000..6598896 --- /dev/null +++ b/src/components/AnimatedPage.tsx @@ -0,0 +1,196 @@ +import React, { useRef, useEffect, useCallback, useState } from "react"; +import { motion, useMotionValue, animate, useTransform } from "framer-motion"; +import { useNavigate } from "react-router-dom"; + +interface AnimatedPageProps { + children: React.ReactNode; +} + +const pageVariants = { + initial: { + opacity: 0, + x: 20, + }, + animate: { + opacity: 1, + x: 0, + }, + exit: { + opacity: 0, + x: -20, + }, +}; + +const pageTransition = { + type: "spring", + stiffness: 400, + damping: 35, + mass: 0.5, + duration: 0.25, +}; + +export const AnimatedPage: React.FC = ({ children }) => { + const navigate = useNavigate(); + const x = useMotionValue(0); + const [isSwipeBack, setIsSwipeBack] = useState(false); + const [canSwipeBack, setCanSwipeBack] = useState(false); + const isDragging = useRef(false); + const startX = useRef(0); + const startY = useRef(0); + const edgeWidth = 35; + const threshold = 100; + + useEffect(() => { + setCanSwipeBack(window.history.length > 1); + }, []); + + const shadowOpacity = useTransform(x, [0, 150], [0, 0.3]); + const prevPageScale = useTransform(x, [0, 150], [0.95, 1]); + const prevPageX = useTransform(x, [0, 150], [-50, 0]); + + const handleTouchStart = useCallback((e: TouchEvent) => { + const touch = e.touches[0]; + startX.current = touch.clientX; + startY.current = touch.clientY; + + if (startX.current < edgeWidth && window.history.length > 1) { + isDragging.current = true; + setIsSwipeBack(true); + } + }, []); + + const handleTouchMove = useCallback((e: TouchEvent) => { + if (!isDragging.current) return; + + const touch = e.touches[0]; + const deltaX = touch.clientX - startX.current; + const deltaY = touch.clientY - startY.current; + + if (Math.abs(deltaY) > Math.abs(deltaX) * 0.6) { + isDragging.current = false; + setIsSwipeBack(false); + return; + } + + if (deltaX > 0) { + e.preventDefault(); + const dragDistance = Math.min(deltaX * 0.7, window.innerWidth * 0.6); + x.set(dragDistance); + } + }, [x]); + + const handleTouchEnd = useCallback(() => { + if (!isDragging.current) return; + isDragging.current = false; + + const currentX = x.get(); + + if (currentX > threshold) { + animate(x, window.innerWidth, { + duration: 0.25, + ease: "outQuad", + onComplete: () => { + navigate(-1); + }, + }); + } else { + animate(x, 0, { + duration: 0.35, + ease: "outElastic(1, 0.7)", + onComplete: () => { + setIsSwipeBack(false); + }, + }); + } + }, [navigate, x]); + + useEffect(() => { + document.addEventListener("touchstart", handleTouchStart, { passive: true }); + document.addEventListener("touchmove", handleTouchMove, { passive: false }); + document.addEventListener("touchend", handleTouchEnd); + + return () => { + document.removeEventListener("touchstart", handleTouchStart); + document.removeEventListener("touchmove", handleTouchMove); + document.removeEventListener("touchend", handleTouchEnd); + }; + }, [handleTouchStart, handleTouchMove, handleTouchEnd]); + + return ( + <> + {isSwipeBack && canSwipeBack && ( + + )} + + {isSwipeBack && canSwipeBack && ( + +
+ +
+
+ )} + + + {children} + + + ); +}; + +export { pageVariants, pageTransition }; + diff --git a/src/components/BottomNav.tsx b/src/components/BottomNav.tsx new file mode 100644 index 0000000..fb1e9cb --- /dev/null +++ b/src/components/BottomNav.tsx @@ -0,0 +1,59 @@ +import { useGetIdentity } from "@refinedev/core"; +import { Link, useLocation } from "react-router-dom"; +import { IUser } from "../interfaces"; + +export const BottomNav: React.FC = () => { + const { data: user } = useGetIdentity(); + const location = useLocation(); + const isLoggedIn = !!user; + + const isActive = (path: string) => { + return location.pathname === path || location.pathname.startsWith(path); + }; + + return ( + + ); +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index aa88295..ea7cfaa 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,15 +1,26 @@ import { useGetIdentity } from "@refinedev/core"; import { Link } from "react-router-dom"; import { IUser } from "../interfaces"; +import { useEffect, useState } from "react"; export const Header: React.FC = () => { const { data: user } = useGetIdentity(); + const [isScrolled, setIsScrolled] = useState(false); const isLoggedIn = !!user; + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 10); + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + return ( <> -