diff --git a/index.html b/index.html index cb1909c..ea827e9 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ NewsNow -
- +
+ diff --git a/package.json b/package.json index 877f01e..958346e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "vite-project", + "name": "newsnow", "type": "module", "version": "0.0.0", "private": true, @@ -9,25 +9,31 @@ "build": "vite build", "nitro": "nitro build", "lint": "eslint .", + "test": "vitest", "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@tanstack/react-query": "^5.56.2", "@tanstack/react-query-devtools": "^5.58.0", "@tanstack/react-router": "^1.58.9", "@tanstack/router-devtools": "^1.58.9", - "@tanstack/virtual-file-routes": "^1.56.0", "@unocss/reset": "^0.62.4", + "array-differences": "^3.0.2", + "clsx": "^2.1.1", + "dayjs": "^1.11.13", + "framer-motion": "^11.8.0", "h3": "^1.12.0", "jotai": "^2.10.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-intersection-observer": "^9.13.1", "react-use": "^17.5.1", - "vite-tsconfig-paths": "^5.0.1", "zod": "^3.23.8" }, "devDependencies": { - "@analogjs/vite-plugin-nitro": "^1.8.1", "@eslint-react/eslint-plugin": "^1.14.2", "@iconify-json/ph": "^1.2.0", "@ourongxing/eslint-config": "3.2.3-beta.4", @@ -45,6 +51,8 @@ "typescript-eslint": "^8.7.0", "unocss": "^0.62.4", "vite": "^5.4.8", - "vite-plugin-with-nitro": "0.0.0-beta.4" + "vite-plugin-with-nitro": "0.0.0-beta.4", + "vite-tsconfig-paths": "^5.0.1", + "vitest": "^2.1.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5b0371..85900d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@dnd-kit/core': + specifier: ^6.1.0 + version: 6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.3.1) '@tanstack/react-query': specifier: ^5.56.2 version: 5.56.2(react@18.3.1) @@ -20,12 +29,21 @@ importers: '@tanstack/router-devtools': specifier: ^1.58.9 version: 1.58.9(@tanstack/react-router@1.58.9(@tanstack/router-generator@1.58.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/virtual-file-routes': - specifier: ^1.56.0 - version: 1.56.0 '@unocss/reset': specifier: ^0.62.4 version: 0.62.4 + array-differences: + specifier: ^3.0.2 + version: 3.0.2 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + framer-motion: + specifier: ^11.8.0 + version: 11.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) h3: specifier: ^1.12.0 version: 1.12.0 @@ -38,19 +56,16 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-intersection-observer: + specifier: ^9.13.1 + version: 9.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-use: specifier: ^17.5.1 version: 17.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - vite-tsconfig-paths: - specifier: ^5.0.1 - version: 5.0.1(typescript@5.6.2)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)) zod: specifier: ^3.23.8 version: 3.23.8 devDependencies: - '@analogjs/vite-plugin-nitro': - specifier: ^1.8.1 - version: 1.8.1(magicast@0.3.5) '@eslint-react/eslint-plugin': specifier: ^1.14.2 version: 1.14.2(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) @@ -59,7 +74,7 @@ importers: version: 1.2.0 '@ourongxing/eslint-config': specifier: 3.2.3-beta.4 - version: 3.2.3-beta.4(@eslint-react/eslint-plugin@1.14.2(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(@typescript-eslint/utils@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.8)(eslint-plugin-react-hooks@5.1.0-rc-fb9a90fa48-20240614(eslint@9.11.1(jiti@1.21.6)))(eslint-plugin-react-refresh@0.4.12(eslint@9.11.1(jiti@1.21.6)))(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) + version: 3.2.3-beta.4(@eslint-react/eslint-plugin@1.14.2(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(@typescript-eslint/utils@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.8)(eslint-plugin-react-hooks@5.1.0-rc-fb9a90fa48-20240614(eslint@9.11.1(jiti@1.21.6)))(eslint-plugin-react-refresh@0.4.12(eslint@9.11.1(jiti@1.21.6)))(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.6.1)(terser@5.33.0)) '@ourongxing/tsconfig': specifier: ^0.0.4 version: 0.0.4 @@ -105,6 +120,12 @@ importers: vite-plugin-with-nitro: specifier: 0.0.0-beta.4 version: 0.0.0-beta.4(magicast@0.3.5)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)) + vite-tsconfig-paths: + specifier: ^5.0.1 + version: 5.0.1(typescript@5.6.2)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)) + vitest: + specifier: ^2.1.1 + version: 2.1.1(@types/node@22.6.1)(terser@5.33.0) packages: @@ -112,9 +133,6 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@analogjs/vite-plugin-nitro@1.8.1': - resolution: {integrity: sha512-jgGajnU7+Q8miEiBXCBg9cGbbjqrYa+EBStTtdshq9uEXZmIZjSe40Rv+6R7S04dnWdVZtf/O1kiZ6aToo5dRQ==} - '@antfu/install-pkg@0.4.1': resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} @@ -224,6 +242,28 @@ packages: resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} engines: {node: '>=16.13'} + '@dnd-kit/accessibility@3.1.0': + resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.1.0': + resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@8.0.0': + resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@es-joy/jsdoccomment@0.48.0': resolution: {integrity: sha512-G6QUWIcC+KvSwXNsJyDTHvqUdNoAVJPPgkc3+Uk4WBKqZvoXhlvazOgm9aL0HwihJLQf0l+tOE2UFzXBqCqgDw==} engines: {node: '>=16'} @@ -1628,6 +1668,36 @@ packages: vitest: optional: true + '@vitest/expect@2.1.1': + resolution: {integrity: sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==} + + '@vitest/mocker@2.1.1': + resolution: {integrity: sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==} + peerDependencies: + '@vitest/spy': 2.1.1 + msw: ^2.3.5 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.1': + resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==} + + '@vitest/runner@2.1.1': + resolution: {integrity: sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==} + + '@vitest/snapshot@2.1.1': + resolution: {integrity: sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==} + + '@vitest/spy@2.1.1': + resolution: {integrity: sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==} + + '@vitest/utils@2.1.1': + resolution: {integrity: sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==} + '@vue/compiler-core@3.5.8': resolution: {integrity: sha512-Uzlxp91EPjfbpeO5KtC0KnXPkuTfGsNDeaKQJxQN718uz+RqDYarEf7UhQJGK+ZYloD2taUbHTI2J4WrUaZQNA==} @@ -1729,6 +1799,13 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-differences@3.0.2: + resolution: {integrity: sha512-raP8rW7DyS3AypmGHlvuSxXUdTXVvQxPHDlWFyQ0DLw4BMb//1qJtUlBRRK6ePLfQV3B3L5tLSe7qix+uEf14w==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} @@ -1817,6 +1894,10 @@ packages: caniuse-lite@1.0.30001663: resolution: {integrity: sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1838,6 +1919,10 @@ packages: character-reference-invalid@1.1.4: resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1979,6 +2064,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + db0@0.1.4: resolution: {integrity: sha512-Ft6eCwONYxlwLjBXSJxw0t0RYtA5gW9mq8JfBXn9TtC0nDPlqePAhpv9v4g9aONBi6JI1OXHTKKkUYGd+BOrCA==} peerDependencies: @@ -2018,6 +2106,10 @@ packages: supports-color: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2504,6 +2596,20 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + framer-motion@11.8.0: + resolution: {integrity: sha512-q/axN/PFRdKmzPK6PO2OhbLUMWXXZuiejdM1/3FhC2hm4YIetc+qeco2EvWm4u1/UTFmevclE492wGFNfSZ4eQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -2543,6 +2649,9 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-port-please@3.1.2: resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} @@ -2947,6 +3056,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3258,6 +3370,10 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -3335,6 +3451,15 @@ packages: peerDependencies: react: ^18.3.1 + react-intersection-observer@9.13.1: + resolution: {integrity: sha512-tSzDaTy0qwNPLJHg8XZhlyHTgGW6drFKTtvjdL+p6um12rcnp8Z5XstE+QNBJ7c64n5o0Lj4ilUleA41bmDoMw==} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react-dom: + optional: true + react-universal-interface@0.6.2: resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} peerDependencies: @@ -3525,6 +3650,9 @@ packages: resolution: {integrity: sha512-cMGfwNyfDZ/nzJ2k2M+ClthBIh//GlZl1JEf47Uoa9XR11bz8Pa2T2wQO4bVrRdH48LrIDWJahQziKo3MjhsWg==} hasBin: true + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3592,6 +3720,9 @@ packages: stack-generator@2.0.10: resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -3717,6 +3848,9 @@ packages: tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.0: resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} @@ -3724,6 +3858,18 @@ packages: resolution: {integrity: sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==} engines: {node: '>=12.0.0'} + tinypool@1.0.1: + resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -3953,6 +4099,11 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vite-node@2.1.1: + resolution: {integrity: sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-plugin-with-nitro@0.0.0-beta.4: resolution: {integrity: sha512-w8GvLPKxE3kEhqBxiopWIJtmOoOEj9XI37cxK+9E9OjnT/1WkDxJJLfu0rr8CMSdKM5D1ZoE0yjqF3xdIn1Mdg==} engines: {node: '>=16.0.0'} @@ -3998,6 +4149,31 @@ packages: terser: optional: true + vitest@2.1.1: + resolution: {integrity: sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.1 + '@vitest/ui': 2.1.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vue-eslint-parser@9.4.3: resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} engines: {node: ^14.17.0 || >=16.0.0} @@ -4018,6 +4194,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} @@ -4089,34 +4270,6 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@analogjs/vite-plugin-nitro@1.8.1(magicast@0.3.5)': - dependencies: - esbuild: 0.20.2 - nitropack: 2.9.7(magicast@0.3.5) - xmlbuilder2: 3.1.1 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@libsql/client' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/kv' - - better-sqlite3 - - drizzle-orm - - encoding - - idb-keyval - - magicast - - supports-color - - uWebSockets.js - - webpack-sources - - xml2js - '@antfu/install-pkg@0.4.1': dependencies: package-manager-detector: 0.2.0 @@ -4267,6 +4420,31 @@ snapshots: dependencies: mime: 3.0.0 + '@dnd-kit/accessibility@3.1.0(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.7.0 + + '@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.0(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.7.0 + + '@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.7.0 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.7.0 + '@es-joy/jsdoccomment@0.48.0': dependencies: comment-parser: 1.4.1 @@ -4821,7 +4999,7 @@ snapshots: '@oozcitak/util@8.3.8': {} - '@ourongxing/eslint-config@3.2.3-beta.4(@eslint-react/eslint-plugin@1.14.2(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(@typescript-eslint/utils@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.8)(eslint-plugin-react-hooks@5.1.0-rc-fb9a90fa48-20240614(eslint@9.11.1(jiti@1.21.6)))(eslint-plugin-react-refresh@0.4.12(eslint@9.11.1(jiti@1.21.6)))(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2)': + '@ourongxing/eslint-config@3.2.3-beta.4(@eslint-react/eslint-plugin@1.14.2(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(@typescript-eslint/utils@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.8)(eslint-plugin-react-hooks@5.1.0-rc-fb9a90fa48-20240614(eslint@9.11.1(jiti@1.21.6)))(eslint-plugin-react-refresh@0.4.12(eslint@9.11.1(jiti@1.21.6)))(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.6.1)(terser@5.33.0))': dependencies: '@antfu/install-pkg': 0.4.1 '@clack/prompts': 0.7.0 @@ -4829,7 +5007,7 @@ snapshots: '@stylistic/eslint-plugin': 2.8.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/eslint-plugin': 8.7.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/parser': 8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) - '@vitest/eslint-plugin': 1.1.4(@typescript-eslint/utils@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) + '@vitest/eslint-plugin': 1.1.4(@typescript-eslint/utils@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.6.1)(terser@5.33.0)) eslint: 9.11.1(jiti@1.21.6) eslint-config-flat-gitignore: 0.3.0(eslint@9.11.1(jiti@1.21.6)) eslint-flat-config-utils: 0.4.0 @@ -5514,12 +5692,53 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - '@vitest/eslint-plugin@1.1.4(@typescript-eslint/utils@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2)': + '@vitest/eslint-plugin@1.1.4(@typescript-eslint/utils@8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2))(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.6.1)(terser@5.33.0))': dependencies: eslint: 9.11.1(jiti@1.21.6) optionalDependencies: '@typescript-eslint/utils': 8.7.0(eslint@9.11.1(jiti@1.21.6))(typescript@5.6.2) typescript: 5.6.2 + vitest: 2.1.1(@types/node@22.6.1)(terser@5.33.0) + + '@vitest/expect@2.1.1': + dependencies: + '@vitest/spy': 2.1.1 + '@vitest/utils': 2.1.1 + chai: 5.1.1 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0))': + dependencies: + '@vitest/spy': 2.1.1 + estree-walker: 3.0.3 + magic-string: 0.30.11 + optionalDependencies: + vite: 5.4.8(@types/node@22.6.1)(terser@5.33.0) + + '@vitest/pretty-format@2.1.1': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.1': + dependencies: + '@vitest/utils': 2.1.1 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.1': + dependencies: + '@vitest/pretty-format': 2.1.1 + magic-string: 0.30.11 + pathe: 1.1.2 + + '@vitest/spy@2.1.1': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.1': + dependencies: + '@vitest/pretty-format': 2.1.1 + loupe: 3.1.1 + tinyrainbow: 1.2.0 '@vue/compiler-core@3.5.8': dependencies: @@ -5640,6 +5859,10 @@ snapshots: argparse@2.0.1: {} + array-differences@3.0.2: {} + + assertion-error@2.0.1: {} + async-sema@3.1.1: {} async@3.2.6: {} @@ -5731,6 +5954,14 @@ snapshots: caniuse-lite@1.0.30001663: {} + chai@5.1.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -5750,6 +5981,8 @@ snapshots: character-reference-invalid@1.1.4: {} + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -5877,6 +6110,8 @@ snapshots: csstype@3.1.3: {} + dayjs@1.11.13: {} + db0@0.1.4: {} debug@2.6.9: @@ -5891,6 +6126,8 @@ snapshots: dependencies: ms: 2.1.3 + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -6570,6 +6807,13 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + framer-motion@11.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + tslib: 2.7.0 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + fresh@0.5.2: {} front-matter@4.0.2: @@ -6609,6 +6853,8 @@ snapshots: get-caller-file@2.0.5: {} + get-func-name@2.0.2: {} + get-port-please@3.1.2: {} get-stream@8.0.1: {} @@ -7016,6 +7262,10 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.1.1: + dependencies: + get-func-name: 2.0.2 + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -7401,6 +7651,8 @@ snapshots: pathe@1.1.2: {} + pathval@2.0.0: {} + perfect-debounce@1.0.0: {} picocolors@1.1.0: {} @@ -7463,6 +7715,12 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-intersection-observer@9.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-universal-interface@0.6.2(react@18.3.1)(tslib@2.7.0): dependencies: react: 18.3.1 @@ -7693,6 +7951,8 @@ snapshots: short-unique-id@5.2.0: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -7751,6 +8011,8 @@ snapshots: dependencies: stackframe: 1.3.4 + stackback@0.0.2: {} + stackframe@1.3.4: {} stacktrace-gps@3.1.2: @@ -7881,6 +8143,8 @@ snapshots: tiny-warning@1.0.3: {} + tinybench@2.9.0: {} + tinyexec@0.3.0: {} tinyglobby@0.2.6: @@ -7888,6 +8152,12 @@ snapshots: fdir: 6.3.0(picomatch@4.0.2) picomatch: 4.0.2 + tinypool@1.0.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: @@ -8109,6 +8379,23 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vite-node@2.1.1(@types/node@22.6.1)(terser@5.33.0): + dependencies: + cac: 6.7.14 + debug: 4.3.7 + pathe: 1.1.2 + vite: 5.4.8(@types/node@22.6.1)(terser@5.33.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-plugin-with-nitro@0.0.0-beta.4(magicast@0.3.5)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)): dependencies: esbuild: 0.24.0 @@ -8162,6 +8449,40 @@ snapshots: fsevents: 2.3.3 terser: 5.33.0 + vitest@2.1.1(@types/node@22.6.1)(terser@5.33.0): + dependencies: + '@vitest/expect': 2.1.1 + '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)) + '@vitest/pretty-format': 2.1.1 + '@vitest/runner': 2.1.1 + '@vitest/snapshot': 2.1.1 + '@vitest/spy': 2.1.1 + '@vitest/utils': 2.1.1 + chai: 5.1.1 + debug: 4.3.7 + magic-string: 0.30.11 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinyexec: 0.3.0 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.4.8(@types/node@22.6.1)(terser@5.33.0) + vite-node: 2.1.1(@types/node@22.6.1)(terser@5.33.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.6.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vue-eslint-parser@9.4.3(eslint@9.11.1(jiti@1.21.6)): dependencies: debug: 4.3.7 @@ -8188,6 +8509,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wide-align@1.1.5: dependencies: string-width: 4.2.3 diff --git a/server/config.ts b/server/config.ts new file mode 100644 index 0000000..521e1fe --- /dev/null +++ b/server/config.ts @@ -0,0 +1,10 @@ +// 创建配置对象 +export const config = { + PORT: 6688, + DISALLOW_ROBOT: true, + CACHE_TTL: 3600, + REQUEST_TIMEOUT: 6000, + ALLOWED_DOMAIN: "*", + USE_LOG_FILE: true, + RSS_MODE: false, +} diff --git a/server/main.ts b/server/main.ts deleted file mode 100644 index fb7ab89..0000000 --- a/server/main.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function add(x: number, y: number) { - return x + y -} diff --git a/server/routes/[...id].get.ts b/server/routes/[...id].get.ts new file mode 100644 index 0000000..5afdec9 --- /dev/null +++ b/server/routes/[...id].get.ts @@ -0,0 +1,8 @@ +import { defineEventHandler, getQuery, getRouterParam, sendProxy } from "h3" + +export default defineEventHandler(async (event) => { + const id = getRouterParam(event, "id") + const { latest } = getQuery(event) + if (latest !== undefined) return await sendProxy(event, `https://smzdk.top/api/${id}/new`) + return await sendProxy(event, `https://smzdk.top/api/${id}/new`) +}) diff --git a/server/routes/hello.ts b/server/routes/hello.ts deleted file mode 100644 index a3444b0..0000000 --- a/server/routes/hello.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineEventHandler } from "h3" - -export default defineEventHandler(() => { - return { - message: "Hello World", - } -}) diff --git a/server/types.ts b/server/types.ts new file mode 100644 index 0000000..b906778 --- /dev/null +++ b/server/types.ts @@ -0,0 +1,41 @@ +import type { NewsItem, SourceInfo } from "@shared/types" + +// 榜单数据 +export interface ListItem extends NewsItem { } + +// 路由数据 +export interface RouterData extends SourceInfo { } + +// 请求类型 +export interface Get { + url: string + headers?: Record + params?: Record + timeout?: number + noCache?: boolean + ttl?: number + originaInfo?: boolean +} + +export interface Post { + url: string + headers?: Record + body?: string | object | import("node:buffer").Buffer | undefined + timeout?: number + noCache?: boolean + ttl?: number + originaInfo?: boolean +} + +export interface Web { + url: string + timeout?: number + noCache?: boolean + ttl?: number + userAgent?: string +} + +// 参数类型 +export interface Options { + [key: string]: string | number | undefined +} diff --git a/shared/data.ts b/shared/data.ts index 95d4aeb..ef524ae 100644 --- a/shared/data.ts +++ b/shared/data.ts @@ -1,24 +1,40 @@ -import type { Section } from "./types" +import type { Metadata } from "./types" -export const sections = [ - { +export const sectionIds = ["focus", "social", "china", "world", "digital"] as const + +export const sourceList = { + "36kr": "36氪", + "douyin": "抖音", + "hupu": "虎扑", + "zhihu": "知乎", + "weibo": "微博", + "tieba": "贴吧", + "zaobao": "联合早报", + "thepaper": "澎湃新闻", + "toutiao": "今日头条", + "cankaoxiaoxi": "参考消息", + "peopledaily": "人民日报", +} as const satisfies Record + +export const metadata: Metadata = { + focus: { name: "关注", - id: "focus", + sourceList: [], }, - { - name: "综合", - id: "main", + social: { + name: "社交媒体", + sourceList: ["douyin", "hupu", "tieba", "weibo"], }, - { + china: { name: "国内", - id: "china", + sourceList: ["peopledaily", "36kr", "toutiao"], }, - { + world: { name: "国外", - id: "world", + sourceList: ["zaobao"], }, - { + digital: { name: "数码", - id: "digital", + sourceList: ["36kr"], }, -] as const satisfies Section[] +} diff --git a/shared/types.ts b/shared/types.ts index 400c86e..8d68e1b 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -1,8 +1,34 @@ -import type { sections } from "./data" +import type { sectionIds, sourceList } from "./data" + +export type SourceID = keyof(typeof sourceList) +export type SectionID = (typeof sectionIds)[number] +export type Metadata = Record export interface Section { name: string - id: string + sourceList: SourceID[] } -export type SectionId = (typeof sections)[number]["id"] +export interface NewsItem { + title: string + cover?: string + author?: string + desc?: string + url: string + mobileUrl?: string +} + +// 路由数据 +export interface SourceInfo { + name: string + title: string + subtitle?: string + type: string + description?: string + params?: Record + total: number + link?: string + updateTime: string + fromCache: boolean + data: NewsItem[] +} diff --git a/shared/utils.ts b/shared/utils.ts new file mode 100644 index 0000000..ebc4a69 --- /dev/null +++ b/shared/utils.ts @@ -0,0 +1,21 @@ +export function formatTime(timestamp: string) { + const date = new Date(timestamp) + const now = new Date() + const diffInSeconds = (now.getTime() - date.getTime()) / 1000 + const diffInMinutes = diffInSeconds / 60 + const diffInHours = diffInMinutes / 60 + + if (diffInSeconds < 60) { + return "刚刚更新" + } else if (diffInMinutes < 60) { + const minutes = Math.floor(diffInMinutes) + return `${minutes}分钟前更新` + } else if (diffInHours < 24) { + const hours = Math.floor(diffInHours) + return `${hours}小时前更新` + } else { + const month = date.getMonth() + 1 + const day = date.getDate() + return `${month}月${day}日` + } +} diff --git a/src/app.tsx b/src/app.tsx new file mode 100644 index 0000000..410e531 --- /dev/null +++ b/src/app.tsx @@ -0,0 +1,15 @@ +import ReactDOM from "react-dom/client" +import { RouterProvider } from "@tanstack/react-router" +import { QueryClientProvider } from "@tanstack/react-query" +import { queryClient, router } from "./main" + +const rootElement = document.getElementById("app")! + +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement) + root.render( + + + , + ) +} diff --git a/src/atoms.ts b/src/atoms.ts new file mode 100644 index 0000000..8a6d30d --- /dev/null +++ b/src/atoms.ts @@ -0,0 +1,27 @@ +import { atom } from "jotai" +import type { SectionID, SourceID } from "@shared/types" +import { metadata, sourceList } from "@shared/data" +import { atomWithLocalStorage } from "./utils/atom" + +export const focusSourcesAtom = atomWithLocalStorage("focusSources", [], (stored) => { + return stored.filter(item => item in sourceList) +}) + +const currentSectionIDAtom = atom("focus") + +export const currentSectionAtom = atom((get) => { + const id = get(currentSectionIDAtom) + if (id === "focus") { + return { + id, + ...metadata[id], + sourceList: get(focusSourcesAtom), + } + } + return { + id, + ...metadata[id], + } +}, (_, set, update: SectionID) => { + set(currentSectionIDAtom, update) +}) diff --git a/src/atoms/news.ts b/src/atoms/news.ts deleted file mode 100644 index 03dacea..0000000 --- a/src/atoms/news.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { atom } from "jotai" -import { sections } from "@shared/data" -import type { SectionId } from "@shared/types" - -export const sectionsAtom = atom(sections) - -export const activeSectionAtom = atom("focus") diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 1692507..6d9cff8 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,6 +1,11 @@ import { Link } from "@tanstack/react-router" +import { useCallback } from "react" +import { useAtomValue } from "jotai" +import type { SourceID } from "@shared/types" +import { queryClient } from "~/main" import logo from "~/assets/react.svg" import { useDark } from "~/hooks/useDark" +import { currentSectionAtom } from "~/atoms" function ThemeToggle() { const { toggleDark } = useDark() @@ -14,15 +19,30 @@ function ThemeToggle() { ) } +function RefreshButton() { + const currentSection = useAtomValue(currentSectionAtom) + const refreshAll = useCallback(async () => { + await queryClient.refetchQueries({ + predicate(query) { + return currentSection.sourceList.includes(query.queryKey[0] as SourceID) + }, + }) + }, [currentSection]) + + return ( + + diff --git a/src/components/Main.tsx b/src/components/Main.tsx new file mode 100644 index 0000000..6dfe1a5 --- /dev/null +++ b/src/components/Main.tsx @@ -0,0 +1,70 @@ +import { useCallback, useMemo, useState } from "react" +import type { DragEndEvent, DragStartEvent } from "@dnd-kit/core" +import { + DndContext, + DragOverlay, + MouseSensor, + TouchSensor, + closestCenter, + useSensor, + useSensors, +} from "@dnd-kit/core" +import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable" +import type { SectionID } from "@shared/types" +import { metadata } from "@shared/data" +import { Item, SortableItem } from "./NewsCard" + +export function Main({ sectionId }: { sectionId: SectionID }) { + // const [items, setItems] = useState(metadata?.[sectionId]?.sourceList ?? []) + const items = useMemo(() => metadata?.[sectionId]?.sourceList ?? [], [sectionId]) + const [activeId, setActiveId] = useState(null) + const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor)) + + const handleDragStart = useCallback((event: DragStartEvent) => { + setActiveId(event.active.id as string) + }, []) + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event + + if (active.id !== over?.id) { + // setItems((items) => { + // const oldIndex = items.indexOf(active.id as any) + // const newIndex = items.indexOf(over!.id as any) + + // return arrayMove(items, oldIndex, newIndex) + // }) + } + + setActiveId(null) + }, []) + const handleDragCancel = useCallback(() => { + setActiveId(null) + }, []) + + return ( + + +
+ {items.map(id => ( + + ))} +
+
+ + {!!activeId && } + +
+ ) +} diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx deleted file mode 100644 index cf733e8..0000000 --- a/src/components/NavBar.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Link } from "@tanstack/react-router" -import { useAtomValue } from "jotai" -import { sectionsAtom } from "~/atoms/news" - -export function NavBar() { - const items = useAtomValue(sectionsAtom) - return ( -
- {items.map(nav => ( - - {nav.name} - - ))} -
- ) -} diff --git a/src/components/NewsCard.tsx b/src/components/NewsCard.tsx index 8e69bf8..9b9b415 100644 --- a/src/components/NewsCard.tsx +++ b/src/components/NewsCard.tsx @@ -1,6 +1,84 @@ -export function NewsCard() { +import type { CSSProperties, HTMLAttributes, PropsWithChildren } from "react" +import { useSortable } from "@dnd-kit/sortable" +import { CSS } from "@dnd-kit/utilities" +import { forwardRef } from "react" +import clsx from "clsx" +import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities" + +type ItemProps = HTMLAttributes & { + id: string + withOpacity?: boolean + isDragging?: boolean + listeners?: SyntheticListenerMap +} + +export function GridContainer({ children }: PropsWithChildren) { return ( -
+
+ {children}
) } + +export const Item = forwardRef(({ id, withOpacity, isDragging, listeners, style, ...props }, ref) => { + const inlineStyles: CSSProperties = { + transformOrigin: "50% 50%", + minHeight: "500px", + ...style, + } + + const css = [ + "border rounded-xl", + isDragging ? "scale-105" : "", + withOpacity && "op-50", + ] + + return ( +
+
+
+ 你好 +
+
+ {id} +
+
+
+ ) +}) + +export function SortableItem(props: ItemProps) { + const { id } = props + const { + isDragging, + attributes, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ id }) + + const style = { + transform: CSS.Transform.toString(transform), + transition: transition || undefined, + } + + return ( + + ) +} diff --git a/src/components/Pure.tsx b/src/components/Pure.tsx new file mode 100644 index 0000000..cdf4ce5 --- /dev/null +++ b/src/components/Pure.tsx @@ -0,0 +1,93 @@ +import type { SourceID, SourceInfo } from "@shared/types" +import { useQuery } from "@tanstack/react-query" +import { formatTime } from "@shared/utils" +import clsx from "clsx" +import { useInView } from "react-intersection-observer" +import { useAtom, useAtomValue } from "jotai" +import { useCallback } from "react" +import { currentSectionAtom, focusSourcesAtom } from "~/atoms" + +export function Main() { + const currentSection = useAtomValue(currentSectionAtom) + return ( +
+ {currentSection.sourceList.map(id => ( + + ))} +
+ ) +} + +function CardWrapper({ id }: { id: SourceID }) { + const { ref, inView } = useInView({ + threshold: 0, + }) + return ( +
+ +
+ ) +} + +function NewsCard({ id, inView }: { id: SourceID, inView: boolean }) { + const [focusSources, setFocusSources] = useAtom(focusSourcesAtom) + const addFocusList = useCallback(() => { + setFocusSources(focusSources.includes(id) ? focusSources.filter(i => i !== id) : [...focusSources, id]) + }, [setFocusSources, focusSources, id]) + const { isPending, error, isFetching, data, refetch } = useQuery({ + queryKey: [id], + queryFn: async () => { + const response = await fetch(`/api/${id}?latest`) + return await response.json() as SourceInfo + }, + enabled: inView, + refetchOnWindowFocus: false, + }) + if (isPending || !data) { + return ( + <> + {Array.from({ length: 18 }).map((_, i) => i).map(i =>
)} + + ) + } else if (error) { + return
Error:
+ } else if (data) { + return ( + <> +
+ + { data?.title } + + + { data?.subtitle} + +
+
+ {data?.data.slice(0, 20).map((item, index) => ( +
+ + { index + 1} + + + {item.title} + +
+ ))} +
+
+ + {formatTime(data!.updateTime)} + +
+ + ) + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..35b78ff --- /dev/null +++ b/src/main.ts @@ -0,0 +1,20 @@ +import { QueryClient } from "@tanstack/react-query" +import { createRouter } from "@tanstack/react-router" +import { routeTree } from "./routeTree.gen" + +export const queryClient = new QueryClient() + +export const router = createRouter({ + routeTree, + context: { + queryClient, + }, + defaultPreload: "intent", + defaultPreloadStaleTime: 0, +}) + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router + } +} diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index 7350874..0000000 --- a/src/main.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import ReactDOM from "react-dom/client" -import { RouterProvider, createRouter } from "@tanstack/react-router" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { routeTree } from "./routeTree.gen" - -const queryClient = new QueryClient() - -const router = createRouter({ - routeTree, - context: { - queryClient, - }, - defaultPreload: "intent", - defaultPreloadStaleTime: 0, -}) - -declare module "@tanstack/react-router" { - interface Register { - router: typeof router - } -} - -const rootElement = document.getElementById("root")! - -if (!rootElement.innerHTML) { - const root = ReactDOM.createRoot(rootElement) - root.render( - - - , - ) -} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index da3d470..56b0245 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -12,7 +12,6 @@ import { Route as rootRoute } from './routes/__root' import { Route as SettingImport } from './routes/setting' -import { Route as SectionImport } from './routes/section' import { Route as IndexImport } from './routes/index' // Create/Update Routes @@ -22,11 +21,6 @@ const SettingRoute = SettingImport.update({ getParentRoute: () => rootRoute, } as any) -const SectionRoute = SectionImport.update({ - path: '/section', - getParentRoute: () => rootRoute, -} as any) - const IndexRoute = IndexImport.update({ path: '/', getParentRoute: () => rootRoute, @@ -43,13 +37,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } - '/section': { - id: '/section' - path: '/section' - fullPath: '/section' - preLoaderRoute: typeof SectionImport - parentRoute: typeof rootRoute - } '/setting': { id: '/setting' path: '/setting' @@ -64,41 +51,36 @@ declare module '@tanstack/react-router' { export interface FileRoutesByFullPath { '/': typeof IndexRoute - '/section': typeof SectionRoute '/setting': typeof SettingRoute } export interface FileRoutesByTo { '/': typeof IndexRoute - '/section': typeof SectionRoute '/setting': typeof SettingRoute } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute - '/section': typeof SectionRoute '/setting': typeof SettingRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/section' | '/setting' + fullPaths: '/' | '/setting' fileRoutesByTo: FileRoutesByTo - to: '/' | '/section' | '/setting' - id: '__root__' | '/' | '/section' | '/setting' + to: '/' | '/setting' + id: '__root__' | '/' | '/setting' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute - SectionRoute: typeof SectionRoute SettingRoute: typeof SettingRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, - SectionRoute: SectionRoute, SettingRoute: SettingRoute, } @@ -115,16 +97,12 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", - "/section", "/setting" ] }, "/": { "filePath": "index.tsx" }, - "/section": { - "filePath": "section.tsx" - }, "/setting": { "filePath": "setting.tsx" } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index e940373..ec09f61 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,24 +1,26 @@ -import { Link, Outlet, createRootRouteWithContext } from "@tanstack/react-router" +import { Outlet, createRootRouteWithContext } from "@tanstack/react-router" import { TanStackRouterDevtools } from "@tanstack/router-devtools" import { ReactQueryDevtools } from "@tanstack/react-query-devtools" import "~/styles/globals.css" import "@unocss/reset/tailwind.css" import "virtual:uno.css" +import type { QueryClient } from "@tanstack/react-query" import { Header } from "~/components/Header" -export const Route = createRootRouteWithContext()({ +export const Route = createRootRouteWithContext<{ + queryClient: QueryClient +}>()({ component: RootComponent, notFoundComponent: () => { return (

This is the notFoundComponent configured on root route

- Start Over
) }, }) -function RootComponent() { +export function RootComponent() { return (
diff --git a/src/routes/index.tsx b/src/routes/index.tsx index cf02676..13807b3 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,9 +1,42 @@ +import { metadata, sectionIds } from "@shared/data" +import type { SectionID } from "@shared/types" import { Link, createFileRoute } from "@tanstack/react-router" +import clsx from "clsx" +import { useSetAtom } from "jotai" +import { useEffect } from "react" +import { currentSectionAtom } from "~/atoms" +// import { Main } from "~/components/Main" +import { Main } from "~/components/Pure" export const Route = createFileRoute("/")({ - component: Index, + validateSearch: (search: any) => ({ + section: (search.section as SectionID), + }), + component: IndexComponent, }) -function Index() { - return 关注 +function IndexComponent() { + const { section: id = "focus" } = Route.useSearch() + const setCurrentSectionAtom = useSetAtom(currentSectionAtom) + useEffect(() => { + setCurrentSectionAtom(id) + }, [setCurrentSectionAtom, id]) + + return id && ( +
+
+ {sectionIds.map(section => ( + + {metadata[section].name} + + ))} +
+
+
+ ) } diff --git a/src/routes/section.tsx b/src/routes/section.tsx deleted file mode 100644 index 30bd15c..0000000 --- a/src/routes/section.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router" -import type { SectionId } from "~/atoms/news" -import { NavBar } from "~/components/NavBar" - -export const Route = createFileRoute("/section")({ - validateSearch: (search: any) => ({ - n: search.n as SectionId, - }), - component: Section, -}) - -export function Section() { - return ( -
- -
- ) -} diff --git a/src/routes/setting.tsx b/src/routes/setting.tsx index d4e7d61..ddad364 100644 --- a/src/routes/setting.tsx +++ b/src/routes/setting.tsx @@ -1,13 +1,13 @@ import { createFileRoute } from "@tanstack/react-router" export const Route = createFileRoute("/setting")({ - component: Setting + component: SettingComponent, }) -function Setting() { +function SettingComponent() { return (

Setting

) -} \ No newline at end of file +} diff --git a/src/styles/globals.css b/src/styles/globals.css index c555843..aa41219 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,6 +1,7 @@ html, body, -#root { +#app { + --col-count: 5; height: 100vh; margin: 0; padding: 0; @@ -21,4 +22,4 @@ body { button:disabled { cursor: not-allowed; pointer-events: all !important; -} \ No newline at end of file +} diff --git a/src/utils/atom.ts b/src/utils/atom.ts new file mode 100644 index 0000000..f7c7148 --- /dev/null +++ b/src/utils/atom.ts @@ -0,0 +1,33 @@ +import type { PrimitiveAtom } from "jotai" +import { atom } from "jotai" + +export function atomWithLocalStorage( + key: string, + initialValue: T, + initFn?: ((stored: T) => T), +): PrimitiveAtom { + const getInitialValue = () => { + const item = localStorage.getItem(key) + try { + if (item !== null) { + const stored = JSON.parse(item) + if (initFn) return initFn(stored) + return stored + } + } catch { + // + } + return initialValue + } + const baseAtom = atom(getInitialValue()) + const derivedAtom = atom( + get => get(baseAtom), + (get, set, update) => { + const nextValue + = typeof update === "function" ? update(get(baseAtom)) : update + set(baseAtom, nextValue) + localStorage.setItem(key, JSON.stringify(nextValue)) + }, + ) + return derivedAtom +} diff --git a/test/data.test.ts b/test/data.test.ts new file mode 100644 index 0000000..454ae50 --- /dev/null +++ b/test/data.test.ts @@ -0,0 +1,11 @@ +import { metadata } from "@shared/data" +import { describe, expect, it } from "vitest" + +// 通过两次 diff 来找出数组的差异 +describe("data", () => { + it.for(Object.entries(metadata))(` "%s" source list shoule be unique`, ([, { sourceList }]) => { + if (sourceList) { + expect(new Set(sourceList).size).toBe(sourceList.length) + } + }) +}) diff --git a/tsconfig.app.json b/tsconfig.app.json index 6885eba..96dfa9f 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,5 +1,5 @@ { - "extends": "@ourongxing/tsconfig", + "extends": "./tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "lib": ["ES2020", "DOM", "DOM.Iterable"], diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..1a44dc0 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "composite": true, + "target": "ES2020", + "moduleDetection": "force", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noEmit": true, + "esModuleInterop": true, + "isolatedModules": true, + "skipLibCheck": true + } +} diff --git a/tsconfig.json b/tsconfig.json index 13a8dad..a2efd46 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,4 @@ { - "extends": "@ourongxing/tsconfig", "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } diff --git a/tsconfig.node.json b/tsconfig.node.json index c6576a5..62044b0 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,5 +1,5 @@ { - "extends": "@ourongxing/tsconfig", + "extends": ["./tsconfig.base.json", "./dist/.nitro/types/tsconfig.json"], "compilerOptions": { "lib": ["ES2020"], "baseUrl": ".", @@ -8,5 +8,5 @@ "@shared/*": ["shared/*"] } }, - "include": ["server", "*.config.*", "shared"] + "include": ["server", "*.config.*", "shared", "test"] } diff --git a/uno.config.ts b/uno.config.ts index a96b5e2..2db36aa 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -28,6 +28,7 @@ export default defineConfig({ "btn-action": "border border-base rounded flex gap-2 items-center px2 py1 op75 hover:op100 hover:bg-hover", "btn-action-sm": "btn-action text-sm", "btn-action-active": "color-active border-active! bg-active op100!", + "skeleton": "bg-gray-400/10 rounded-md h-5 w-full animate-pulse", }, theme: { colors: { diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..27d4523 --- /dev/null +++ b/vercel.json @@ -0,0 +1,11 @@ +{ + "rewrites": [ + { + "source": "/:path((?!api).*)", + "destination": "/" + } + ], + "github": { + "silent": true + } +} diff --git a/vite.config.ts b/vite.config.ts index 2985597..55a1535 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,5 @@ import process from "node:process" +import { fileURLToPath } from "node:url" import { defineConfig } from "vite" import react from "@vitejs/plugin-react-swc" import nitro from "vite-plugin-with-nitro" @@ -19,11 +20,17 @@ export default defineConfig({ react(), nitro({ ssr: false }, { srcDir: "server", + alias: { + "@shared": fileURLToPath(new URL("shared", import.meta.url)), + }, + runtimeConfig: { + // apiPrefix: "", + }, + typescript: { + generateTsConfig: true, + }, minify: false, preset: process.env.VERCEL ? "vercel-edge" : "node-server", - experimental: { - websocket: true, - }, }), ], })