diff --git a/package.json b/package.json index b06d86c..407303a 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@atcute/client": "^2.0.7", "@atcute/oauth-browser-client": "^1.0.9", "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", @@ -26,6 +27,7 @@ "lexicons": "link:@atcute/bluesky/lexicons", "lucide-react": "^0.469.0", "preact": "^10.25.4", + "recharts": "^2.15.1", "simple-icons": "^13.21.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1821c58..9fcc974 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@radix-ui/react-dialog': specifier: ^1.1.4 version: 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-popover': + specifier: ^1.1.6 + version: 1.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-progress': specifier: ^1.1.1 version: 1.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -59,6 +62,9 @@ importers: preact: specifier: ^10.25.4 version: 10.25.4 + recharts: + specifier: ^2.15.1 + version: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) simple-icons: specifier: ^13.21.0 version: 13.21.0 @@ -207,6 +213,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.26.7': + resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.25.9': resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} @@ -605,6 +615,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-arrow@1.1.2': + resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.1': resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} peerDependencies: @@ -671,6 +694,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.5': + resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.1': resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: @@ -693,6 +729,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-scope@1.1.2': + resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-id@1.1.0': resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: @@ -702,6 +751,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-popover@1.1.6': + resolution: {integrity: sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.1': resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} peerDependencies: @@ -715,6 +777,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.2.2': + resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.3': resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} peerDependencies: @@ -728,6 +803,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.4': + resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.2': resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} peerDependencies: @@ -754,6 +842,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.2': + resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-progress@1.1.1': resolution: {integrity: sha512-6diOawA84f/eMxFHcWut0aE1C2kyE9dOyCTQOMRR2C/qPiXz/X0SaiA/RLbapQaXUCmy0/hLMf9meSccD1N0pA==} peerDependencies: @@ -802,6 +903,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.2': + resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-tabs@1.1.2': resolution: {integrity: sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==} peerDependencies: @@ -1143,6 +1253,33 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/diff@6.0.0': resolution: {integrity: sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==} @@ -1279,6 +1416,50 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -1288,6 +1469,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -1301,6 +1485,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -1347,6 +1534,13 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + fast-equals@5.2.2: + resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -1413,6 +1607,10 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1470,6 +1668,13 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -1615,6 +1820,9 @@ packages: engines: {node: '>=14'} hasBin: true + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1623,6 +1831,12 @@ packages: peerDependencies: react: ^19.0.0 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -1643,6 +1857,22 @@ packages: '@types/react': optional: true + react-remove-scroll@2.6.3: + resolution: {integrity: sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -1653,6 +1883,12 @@ packages: '@types/react': optional: true + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@19.0.0: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} @@ -1664,6 +1900,19 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.1: + resolution: {integrity: sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -1831,6 +2080,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite@6.0.7: resolution: {integrity: sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2031,6 +2283,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.26.7': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/template@7.25.9': dependencies: '@babel/code-frame': 7.26.2 @@ -2307,6 +2563,12 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-arrow@1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-collection@1.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) @@ -2357,6 +2619,16 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-dismissable-layer@1.1.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-focus-guards@1.1.1(react@19.0.0)': dependencies: react: 19.0.0 @@ -2369,11 +2641,39 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-focus-scope@1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-id@1.1.0(react@19.0.0)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(react@19.0.0) react: 19.0.0 + '@radix-ui/react-popover@1.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) + '@radix-ui/react-context': 1.1.1(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.1(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.0(react@19.0.0) + '@radix-ui/react-popper': 1.2.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.1.2(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(react@19.0.0) + aria-hidden: 1.2.4 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-remove-scroll: 2.6.3(react@19.0.0) + '@radix-ui/react-popper@1.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -2389,6 +2689,21 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-popper@1.2.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-arrow': 1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) + '@radix-ui/react-context': 1.1.1(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.0(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(react@19.0.0) + '@radix-ui/react-use-rect': 1.1.0(react@19.0.0) + '@radix-ui/react-use-size': 1.1.0(react@19.0.0) + '@radix-ui/rect': 1.1.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-portal@1.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-primitive': 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -2396,6 +2711,13 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-portal@1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-presence@1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) @@ -2409,6 +2731,12 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-primitive@2.0.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-slot': 1.1.2(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@radix-ui/react-progress@1.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-context': 1.1.1(react@19.0.0) @@ -2441,6 +2769,11 @@ snapshots: '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) react: 19.0.0 + '@radix-ui/react-slot@1.1.2(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(react@19.0.0) + react: 19.0.0 + '@radix-ui/react-tabs@1.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -2787,6 +3120,30 @@ snapshots: dependencies: '@babel/types': 7.26.5 + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/diff@6.0.0': {} '@types/estree@1.0.6': {} @@ -2918,10 +3275,50 @@ snapshots: csstype@3.1.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + debug@4.4.0: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + detect-node-es@1.1.0: {} didyoumean@1.2.2: {} @@ -2930,6 +3327,11 @@ snapshots: dlv@1.1.3: {} + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.26.7 + csstype: 3.1.3 + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -3017,6 +3419,10 @@ snapshots: estree-walker@2.0.2: {} + eventemitter3@4.0.7: {} + + fast-equals@5.2.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3082,6 +3488,8 @@ snapshots: he@1.2.0: {} + internmap@2.0.3: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -3122,6 +3530,12 @@ snapshots: lines-and-columns@1.2.4: {} + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -3238,6 +3652,12 @@ snapshots: prettier@3.4.2: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + queue-microtask@1.2.3: {} react-dom@19.0.0(react@19.0.0): @@ -3245,6 +3665,10 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-is@16.13.1: {} + + react-is@18.3.1: {} + react-remove-scroll-bar@2.3.8(react@19.0.0): dependencies: react: 19.0.0 @@ -3260,12 +3684,38 @@ snapshots: use-callback-ref: 1.3.3(react@19.0.0) use-sidecar: 1.1.3(react@19.0.0) + react-remove-scroll@2.6.3(react@19.0.0): + dependencies: + react: 19.0.0 + react-remove-scroll-bar: 2.3.8(react@19.0.0) + react-style-singleton: 2.2.3(react@19.0.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(react@19.0.0) + use-sidecar: 1.1.3(react@19.0.0) + + react-smooth@4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + fast-equals: 5.2.2 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react-style-singleton@2.2.3(react@19.0.0): dependencies: get-nonce: 1.0.1 react: 19.0.0 tslib: 2.8.1 + react-transition-group@4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/runtime': 7.26.7 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react@19.0.0: {} read-cache@1.0.0: @@ -3276,6 +3726,25 @@ snapshots: dependencies: picomatch: 2.3.1 + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + regenerator-runtime@0.14.1: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.10: @@ -3459,6 +3928,23 @@ snapshots: util-deprecate@1.0.2: {} + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite@6.0.7(@types/node@22.10.7)(jiti@1.21.7)(tsx@4.19.2)(yaml@2.7.0): dependencies: esbuild: 0.24.2 diff --git a/src/components/fontPicker.tsx b/src/components/fontPicker.tsx new file mode 100644 index 0000000..94a6cb1 --- /dev/null +++ b/src/components/fontPicker.tsx @@ -0,0 +1,59 @@ +// 2. Create Font Picker Component +import { useTheme, GOOGLE_FONTS } from "@/providers/themeProvider"; +import { Button } from "./ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; +import { Check, ChevronDown, Text } from "lucide-react"; +import { useState } from "preact/hooks"; + +export function FontPicker() { + const { font, setFont, loadFonts, loadedFonts } = useTheme(); + const [isOpen, setIsOpen] = useState(false); + + const handleOpenChange = (open: boolean) => { + setIsOpen(open); + console.log("Setting open", open); + if (open) { + // Load all fonts when picker opens + loadFonts(GOOGLE_FONTS); + } + }; + + return ( + + + + + +
+ {GOOGLE_FONTS.map((fontOption) => ( + + ))} +
+
+
+ ); +} diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index ad92ac0..9c8aab4 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -17,6 +17,7 @@ import { SmartSearchBar } from "./smartSearchBar"; import { ColorToggle } from "./themeSwitcher"; import { Link } from "@tanstack/react-router"; import { ForwardRefExoticComponent, ReactNode } from "preact/compat"; +import { FontPicker } from "./fontPicker"; type BaseMenuItem = { url: string; @@ -120,6 +121,7 @@ export function AppSidebar() { + diff --git a/src/components/smartSearchBar.tsx b/src/components/smartSearchBar.tsx index 1d32c35..6a8ff72 100644 --- a/src/components/smartSearchBar.tsx +++ b/src/components/smartSearchBar.tsx @@ -47,6 +47,8 @@ export function SmartSearchBar({ url: input.replace("https:/", "").replace("pds/", ""), }, }); + } else if (input === "typing") { + navigate({ to: "/rnfgrertt/typing" }); } else { navigate({ to: `/at:/${input.replace("at:/", "")}`, diff --git a/src/components/themeSwitcher.tsx b/src/components/themeSwitcher.tsx index 4f1216f..a9eba00 100644 --- a/src/components/themeSwitcher.tsx +++ b/src/components/themeSwitcher.tsx @@ -5,7 +5,7 @@ import { Circle, Moon, Sun } from "lucide-react"; import { Button } from "./ui/button"; export const ColorToggle = () => { - const { theme, toggleTheme } = useTheme(); + const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); @@ -20,7 +20,7 @@ export const ColorToggle = () => { className="flex items-center justify-center w-10 h-10 p-3 rounded-full bg-gray-200 dark:bg-gray-800 text-neutral-800 dark:text-neutral-300 cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-700" aria-label="button" onClick={() => { - toggleTheme(); + setTheme(theme === "dark" ? "light" : "dark"); }} > diff --git a/src/components/ui/kbdKey.tsx b/src/components/ui/kbdKey.tsx index bad87ff..a5910b0 100644 --- a/src/components/ui/kbdKey.tsx +++ b/src/components/ui/kbdKey.tsx @@ -50,7 +50,7 @@ const KbdKey = React.forwardRef<
, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/src/index.css b/src/index.css index 8f75c71..affb64f 100644 --- a/src/index.css +++ b/src/index.css @@ -3,171 +3,193 @@ @tailwind utilities; @layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; - } + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } } @layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } } .loader { - position: relative; - width: 54px; - height: 54px; - border-radius: 10px; + position: relative; + width: 54px; + height: 54px; + border-radius: 10px; } .loader div { - width: 8%; - height: 24%; - background: rgb(128, 128, 128); - position: absolute; - left: 50%; - top: 30%; - opacity: 0; - border-radius: 50px; - box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); - animation: fade458 1s linear infinite; + width: 8%; + height: 24%; + background: rgb(128, 128, 128); + position: absolute; + left: 50%; + top: 30%; + opacity: 0; + border-radius: 50px; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); + animation: fade458 1s linear infinite; } @keyframes fade458 { - from { - opacity: 1; - } + from { + opacity: 1; + } - to { - opacity: 0.25; - } + to { + opacity: 0.25; + } } .loader .bar1 { - transform: rotate(0deg) translate(0, -130%); - animation-delay: 0s; + transform: rotate(0deg) translate(0, -130%); + animation-delay: 0s; } .loader .bar2 { - transform: rotate(30deg) translate(0, -130%); - animation-delay: -1.1s; + transform: rotate(30deg) translate(0, -130%); + animation-delay: -1.1s; } .loader .bar3 { - transform: rotate(60deg) translate(0, -130%); - animation-delay: -1s; + transform: rotate(60deg) translate(0, -130%); + animation-delay: -1s; } .loader .bar4 { - transform: rotate(90deg) translate(0, -130%); - animation-delay: -0.9s; + transform: rotate(90deg) translate(0, -130%); + animation-delay: -0.9s; } .loader .bar5 { - transform: rotate(120deg) translate(0, -130%); - animation-delay: -0.8s; + transform: rotate(120deg) translate(0, -130%); + animation-delay: -0.8s; } .loader .bar6 { - transform: rotate(150deg) translate(0, -130%); - animation-delay: -0.7s; + transform: rotate(150deg) translate(0, -130%); + animation-delay: -0.7s; } .loader .bar7 { - transform: rotate(180deg) translate(0, -130%); - animation-delay: -0.6s; + transform: rotate(180deg) translate(0, -130%); + animation-delay: -0.6s; } .loader .bar8 { - transform: rotate(210deg) translate(0, -130%); - animation-delay: -0.5s; + transform: rotate(210deg) translate(0, -130%); + animation-delay: -0.5s; } .loader .bar9 { - transform: rotate(240deg) translate(0, -130%); - animation-delay: -0.4s; + transform: rotate(240deg) translate(0, -130%); + animation-delay: -0.4s; } .loader .bar10 { - transform: rotate(270deg) translate(0, -130%); - animation-delay: -0.3s; + transform: rotate(270deg) translate(0, -130%); + animation-delay: -0.3s; } .loader .bar11 { - transform: rotate(300deg) translate(0, -130%); - animation-delay: -0.2s; + transform: rotate(300deg) translate(0, -130%); + animation-delay: -0.2s; } .loader .bar12 { - transform: rotate(330deg) translate(0, -130%); - animation-delay: -0.1s; + transform: rotate(330deg) translate(0, -130%); + animation-delay: -0.1s; +} + +@keyframes invertBlink { + 0% { + filter: invert(100%); + } + 30% { + filter: invert(100%); + } + 50% { + filter: invert(0%); + } + 80% { + filter: invert(0%); + } + 100% { + filter: invert(100%); + } +} + +.animate-blink { + animation: invertBlink 1s infinite; } diff --git a/src/providers/themeProvider.tsx b/src/providers/themeProvider.tsx index 5d6d087..f4379af 100644 --- a/src/providers/themeProvider.tsx +++ b/src/providers/themeProvider.tsx @@ -1,52 +1,158 @@ -import React, { createContext, useContext, useEffect, useState } from "react"; +// 1. Create a Theme Context +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from "react"; -type Theme = "dark" | "light"; +export type FontConfig = { + name: string; + category: "sans" | "serif" | "mono"; + url: string; +}; + +export const GOOGLE_FONTS: FontConfig[] = [ + { + name: "Inter", + category: "sans", + url: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap", + }, + { + name: "Roboto", + category: "sans", + url: "https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap", + }, + { + name: "IBM Plex Mono", + category: "mono", + url: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;700&display=swap", + }, + { + name: "Space Mono", + category: "mono", + url: "https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap", + }, + { + name: "DM Mono", + category: "mono", + url: "https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;700&display=swap", + }, + { + name: "Fira Mono", + category: "mono", + url: "https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;700&display=swap", + }, + { + name: "Overpass Mono", + category: "mono", + url: "https://fonts.googleapis.com/css2?family=Overpass+Mono:wght@400;700&display=swap", + }, + { + name: "Atkinson Hyperlegible Mono", + category: "mono", + url: "https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:wght@400;700&display=swap", + }, + { + name: "Geist Mono", + category: "mono", + url: "https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;700&display=swap", + }, +]; -type ThemeContextType = { - theme: Theme; - toggleTheme: () => void; +export type ThemeState = { + theme: "light" | "dark"; + font: FontConfig; + loadedFonts: Set; + loadFonts: (fonts: FontConfig[]) => void; + setTheme: (theme: "light" | "dark") => void; + setFont: (font: FontConfig) => void; }; -const ThemeContext = createContext(undefined); +const ThemeContext = createContext({} as ThemeState); export function ThemeProvider({ children }: { children: React.ReactNode }) { - // Initialize with undefined to prevent hydration mismatch - const [theme, setTheme] = useState(undefined); + const [theme, setTheme] = useState<"light" | "dark">("light"); + const [font, setFont] = useState(GOOGLE_FONTS[0]); + const [loadedFonts, setLoadedFonts] = useState>(new Set()); + const loadFonts = useCallback( + (fonts: FontConfig[]) => { + console.log("Loading multiple fonts"); + fonts.forEach((fontConfig) => { + if (!loadedFonts.has(fontConfig.url)) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = fontConfig.url; + link.crossOrigin = "anonymous"; + link.dataset.font = fontConfig.name; + document.head.appendChild(link); + + setLoadedFonts((prev) => new Set(prev).add(fontConfig.url)); + } + }); + }, + [loadedFonts], + ); + + // Load saved theme/font from localStorage useEffect(() => { - // Get initial theme on mount - const root = window.document.documentElement; - const initialColorValue = root.className as Theme; - setTheme(initialColorValue); + const savedTheme = + (localStorage.getItem("theme") as "light" | "dark") || "light"; + const savedFont = + JSON.parse(localStorage.getItem("font") || "null") || GOOGLE_FONTS[0]; + setTheme(savedTheme); + setFont(savedFont); }, []); + // Manage font loading useEffect(() => { - if (theme) { - // Update class and localStorage when theme changes - const root = window.document.documentElement; - root.className = theme; - localStorage.setItem("theme", theme); + // Remove existing font links + const existingLinks = document.querySelectorAll("link[data-font]"); + existingLinks.forEach((link) => link.remove()); + + // Create new font link + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = font.url; + link.dataset.font = font.name; + document.head.appendChild(link); + + setLoadedFonts(new Set(font.url)); + + // Update CSS variables + document.documentElement.style.setProperty( + `--font-${font.category}`, + `'${font.name}', ${font.category === "sans" ? "sans-serif" : font.category}`, + ); + // assume we want everything to be this font + document.documentElement.style.setProperty( + `--font-sans`, + `'${font.name}', sans-serif`, + ); + + if (font.category !== "mono") { + document.documentElement.style.setProperty(`--font-mono`, `mono`); } - }, [theme]); - const toggleTheme = () => { - setTheme((prev) => (prev === "light" ? "dark" : "light")); - }; + // Save to localStorage + localStorage.setItem("font", JSON.stringify(font)); + localStorage.setItem("theme", theme); + }, [font, theme]); - // Only render children once theme is set - if (theme === undefined) return null; + // Update theme class + useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + }, [theme]); return ( - + {children} ); } -export function useTheme() { - const context = useContext(ThemeContext); - if (context === undefined) { - throw new Error("useTheme must be used within a ThemeProvider"); - } - return context; -} +export const useTheme = () => useContext(ThemeContext); diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 5cd417d..08579b6 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -14,7 +14,6 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as rootRoute } from './routes/__root' import { Route as FirehoseIndexImport } from './routes/firehose/index' -import { Route as PdsUrlIndexImport } from './routes/pds/$url.index' import { Route as AtHandleIndexImport } from './routes/at:/$handle.index' // Create Virtual Routes @@ -22,6 +21,9 @@ import { Route as AtHandleIndexImport } from './routes/at:/$handle.index' const CounterLazyImport = createFileRoute('/counter')() const AboutLazyImport = createFileRoute('/about')() const IndexLazyImport = createFileRoute('/')() +const RnfgrerttIndexLazyImport = createFileRoute('/rnfgrertt/')() +const RnfgrerttTypingLazyImport = createFileRoute('/rnfgrertt/typing')() +const PdsUrlIndexLazyImport = createFileRoute('/pds/$url/')() const AtHandleCollectionIndexLazyImport = createFileRoute( '/at:/$handle/$collection/', )() @@ -49,17 +51,35 @@ const IndexLazyRoute = IndexLazyImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) +const RnfgrerttIndexLazyRoute = RnfgrerttIndexLazyImport.update({ + id: '/rnfgrertt/', + path: '/rnfgrertt/', + getParentRoute: () => rootRoute, +} as any).lazy(() => + import('./routes/rnfgrertt/index.lazy').then((d) => d.Route), +) + const FirehoseIndexRoute = FirehoseIndexImport.update({ id: '/firehose/', path: '/firehose/', getParentRoute: () => rootRoute, } as any) -const PdsUrlIndexRoute = PdsUrlIndexImport.update({ +const RnfgrerttTypingLazyRoute = RnfgrerttTypingLazyImport.update({ + id: '/rnfgrertt/typing', + path: '/rnfgrertt/typing', + getParentRoute: () => rootRoute, +} as any).lazy(() => + import('./routes/rnfgrertt/typing.lazy').then((d) => d.Route), +) + +const PdsUrlIndexLazyRoute = PdsUrlIndexLazyImport.update({ id: '/pds/$url/', path: '/pds/$url/', getParentRoute: () => rootRoute, -} as any) +} as any).lazy(() => + import('./routes/pds/$url.index.lazy').then((d) => d.Route), +) const AtHandleIndexRoute = AtHandleIndexImport.update({ id: '/at:/$handle/', @@ -111,6 +131,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof CounterLazyImport parentRoute: typeof rootRoute } + '/rnfgrertt/typing': { + id: '/rnfgrertt/typing' + path: '/rnfgrertt/typing' + fullPath: '/rnfgrertt/typing' + preLoaderRoute: typeof RnfgrerttTypingLazyImport + parentRoute: typeof rootRoute + } '/firehose/': { id: '/firehose/' path: '/firehose' @@ -118,6 +145,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof FirehoseIndexImport parentRoute: typeof rootRoute } + '/rnfgrertt/': { + id: '/rnfgrertt/' + path: '/rnfgrertt' + fullPath: '/rnfgrertt' + preLoaderRoute: typeof RnfgrerttIndexLazyImport + parentRoute: typeof rootRoute + } '/at:/$handle/': { id: '/at:/$handle/' path: '/at:/$handle' @@ -129,7 +163,7 @@ declare module '@tanstack/react-router' { id: '/pds/$url/' path: '/pds/$url' fullPath: '/pds/$url' - preLoaderRoute: typeof PdsUrlIndexImport + preLoaderRoute: typeof PdsUrlIndexLazyImport parentRoute: typeof rootRoute } '/at:/$handle/$collection/$rkey': { @@ -155,9 +189,11 @@ export interface FileRoutesByFullPath { '/': typeof IndexLazyRoute '/about': typeof AboutLazyRoute '/counter': typeof CounterLazyRoute + '/rnfgrertt/typing': typeof RnfgrerttTypingLazyRoute '/firehose': typeof FirehoseIndexRoute + '/rnfgrertt': typeof RnfgrerttIndexLazyRoute '/at:/$handle': typeof AtHandleIndexRoute - '/pds/$url': typeof PdsUrlIndexRoute + '/pds/$url': typeof PdsUrlIndexLazyRoute '/at:/$handle/$collection/$rkey': typeof AtHandleCollectionRkeyLazyRoute '/at:/$handle/$collection': typeof AtHandleCollectionIndexLazyRoute } @@ -166,9 +202,11 @@ export interface FileRoutesByTo { '/': typeof IndexLazyRoute '/about': typeof AboutLazyRoute '/counter': typeof CounterLazyRoute + '/rnfgrertt/typing': typeof RnfgrerttTypingLazyRoute '/firehose': typeof FirehoseIndexRoute + '/rnfgrertt': typeof RnfgrerttIndexLazyRoute '/at:/$handle': typeof AtHandleIndexRoute - '/pds/$url': typeof PdsUrlIndexRoute + '/pds/$url': typeof PdsUrlIndexLazyRoute '/at:/$handle/$collection/$rkey': typeof AtHandleCollectionRkeyLazyRoute '/at:/$handle/$collection': typeof AtHandleCollectionIndexLazyRoute } @@ -178,9 +216,11 @@ export interface FileRoutesById { '/': typeof IndexLazyRoute '/about': typeof AboutLazyRoute '/counter': typeof CounterLazyRoute + '/rnfgrertt/typing': typeof RnfgrerttTypingLazyRoute '/firehose/': typeof FirehoseIndexRoute + '/rnfgrertt/': typeof RnfgrerttIndexLazyRoute '/at:/$handle/': typeof AtHandleIndexRoute - '/pds/$url/': typeof PdsUrlIndexRoute + '/pds/$url/': typeof PdsUrlIndexLazyRoute '/at:/$handle/$collection/$rkey': typeof AtHandleCollectionRkeyLazyRoute '/at:/$handle/$collection/': typeof AtHandleCollectionIndexLazyRoute } @@ -191,7 +231,9 @@ export interface FileRouteTypes { | '/' | '/about' | '/counter' + | '/rnfgrertt/typing' | '/firehose' + | '/rnfgrertt' | '/at:/$handle' | '/pds/$url' | '/at:/$handle/$collection/$rkey' @@ -201,7 +243,9 @@ export interface FileRouteTypes { | '/' | '/about' | '/counter' + | '/rnfgrertt/typing' | '/firehose' + | '/rnfgrertt' | '/at:/$handle' | '/pds/$url' | '/at:/$handle/$collection/$rkey' @@ -211,7 +255,9 @@ export interface FileRouteTypes { | '/' | '/about' | '/counter' + | '/rnfgrertt/typing' | '/firehose/' + | '/rnfgrertt/' | '/at:/$handle/' | '/pds/$url/' | '/at:/$handle/$collection/$rkey' @@ -223,9 +269,11 @@ export interface RootRouteChildren { IndexLazyRoute: typeof IndexLazyRoute AboutLazyRoute: typeof AboutLazyRoute CounterLazyRoute: typeof CounterLazyRoute + RnfgrerttTypingLazyRoute: typeof RnfgrerttTypingLazyRoute FirehoseIndexRoute: typeof FirehoseIndexRoute + RnfgrerttIndexLazyRoute: typeof RnfgrerttIndexLazyRoute AtHandleIndexRoute: typeof AtHandleIndexRoute - PdsUrlIndexRoute: typeof PdsUrlIndexRoute + PdsUrlIndexLazyRoute: typeof PdsUrlIndexLazyRoute AtHandleCollectionRkeyLazyRoute: typeof AtHandleCollectionRkeyLazyRoute AtHandleCollectionIndexLazyRoute: typeof AtHandleCollectionIndexLazyRoute } @@ -234,9 +282,11 @@ const rootRouteChildren: RootRouteChildren = { IndexLazyRoute: IndexLazyRoute, AboutLazyRoute: AboutLazyRoute, CounterLazyRoute: CounterLazyRoute, + RnfgrerttTypingLazyRoute: RnfgrerttTypingLazyRoute, FirehoseIndexRoute: FirehoseIndexRoute, + RnfgrerttIndexLazyRoute: RnfgrerttIndexLazyRoute, AtHandleIndexRoute: AtHandleIndexRoute, - PdsUrlIndexRoute: PdsUrlIndexRoute, + PdsUrlIndexLazyRoute: PdsUrlIndexLazyRoute, AtHandleCollectionRkeyLazyRoute: AtHandleCollectionRkeyLazyRoute, AtHandleCollectionIndexLazyRoute: AtHandleCollectionIndexLazyRoute, } @@ -254,7 +304,9 @@ export const routeTree = rootRoute "/", "/about", "/counter", + "/rnfgrertt/typing", "/firehose/", + "/rnfgrertt/", "/at:/$handle/", "/pds/$url/", "/at:/$handle/$collection/$rkey", @@ -270,14 +322,20 @@ export const routeTree = rootRoute "/counter": { "filePath": "counter.lazy.tsx" }, + "/rnfgrertt/typing": { + "filePath": "rnfgrertt/typing.lazy.tsx" + }, "/firehose/": { "filePath": "firehose/index.tsx" }, + "/rnfgrertt/": { + "filePath": "rnfgrertt/index.lazy.tsx" + }, "/at:/$handle/": { "filePath": "at:/$handle.index.tsx" }, "/pds/$url/": { - "filePath": "pds/$url.index.tsx" + "filePath": "pds/$url.index.lazy.tsx" }, "/at:/$handle/$collection/$rkey": { "filePath": "at:/$handle/$collection.$rkey.lazy.tsx" diff --git a/src/routes/pds/$url.index.tsx b/src/routes/pds/$url.index.lazy.tsx similarity index 57% rename from src/routes/pds/$url.index.tsx rename to src/routes/pds/$url.index.lazy.tsx index 4a3b5aa..5cb1d43 100644 --- a/src/routes/pds/$url.index.tsx +++ b/src/routes/pds/$url.index.lazy.tsx @@ -1,53 +1,53 @@ -import ShowError from "@/components/error"; -import { Loader } from "@/components/ui/loader"; -import { useDocumentTitle } from "@/hooks/useDocumentTitle"; -import { QtClient, useXrpc } from "@/providers/qtprovider"; -import { ComAtprotoSyncListRepos } from "@atcute/client/lexicons"; -import { createFileRoute, Link } from "@tanstack/react-router"; -import { ShieldX } from "lucide-react"; -import { useState, useEffect, useRef } from "preact/compat"; +import ShowError from '@/components/error' +import { Loader } from '@/components/ui/loader' +import { useDocumentTitle } from '@/hooks/useDocumentTitle' +import { QtClient, useXrpc } from '@/providers/qtprovider' +import { ComAtprotoSyncListRepos } from '@atcute/client/lexicons' +import { createLazyFileRoute, Link } from '@tanstack/react-router' +import { ShieldX } from 'lucide-react' +import { useState, useEffect, useRef } from 'preact/compat' interface PdsData { - records?: ComAtprotoSyncListRepos.Output["repos"]; - cursor?: string; - health?: PdsHealth; - isLoading: boolean; - error: Error | null; - fetchMore: (cursor: string) => Promise; + records?: ComAtprotoSyncListRepos.Output['repos'] + cursor?: string + health?: PdsHealth + isLoading: boolean + error: Error | null + fetchMore: (cursor: string) => Promise } interface PdsHealth { - version: string; + version: string } function useRepoData(baseUrl: string): PdsData { - const xrpc = useXrpc(); + const xrpc = useXrpc() const [state, setState] = useState({ isLoading: true, error: null, fetchMore: async () => {}, - }); + }) - useDocumentTitle(baseUrl ? `${baseUrl} | atp.tools` : "atp.tools"); - const abortController = new AbortController(); + useDocumentTitle(baseUrl ? `${baseUrl} | atp.tools` : 'atp.tools') + const abortController = new AbortController() async function fetchRepoData(cursor?: string) { try { - setState((prev) => ({ ...prev, isLoading: true })); + setState((prev) => ({ ...prev, isLoading: true })) // we dont use the main authenticated client here - const rpc = new QtClient(new URL("https://" + baseUrl)); + const rpc = new QtClient(new URL('https://' + baseUrl)) // get the PDS const response = await rpc .getXrpcClient() - .get("com.atproto.sync.listRepos", { + .get('com.atproto.sync.listRepos', { params: { limit: 1000, cursor }, signal: abortController.signal, - }); + }) const health = await rpc .getXrpcClient() - .request({ nsid: "_health", type: "get" }); + .request({ nsid: '_health', type: 'get' }) setState((prev) => ({ ...prev, @@ -58,78 +58,78 @@ function useRepoData(baseUrl: string): PdsData { health: health.data, isLoading: false, error: null, - })); + })) // todo: actual errors } catch (err: any) { - if (err.name === "AbortError") return; + if (err.name === 'AbortError') return setState((prev) => ({ ...prev, isLoading: false, - error: err instanceof Error ? err : new Error("An error occurred"), - })); + error: err instanceof Error ? err : new Error('An error occurred'), + })) } } useEffect(() => { - fetchRepoData(); + fetchRepoData() return () => { - abortController.abort(); - }; - }, [baseUrl, xrpc]); + abortController.abort() + } + }, [baseUrl, xrpc]) const fetchMore = async (cursor: string) => { if (cursor && !state.isLoading) { - await fetchRepoData(cursor); + await fetchRepoData(cursor) } - }; + } - return { ...state, fetchMore }; + return { ...state, fetchMore } } -export const Route = createFileRoute("/pds/$url/")({ +export const Route = createLazyFileRoute('/pds/$url/')({ component: RouteComponent, -}); +}) function RouteComponent() { - const { url } = Route.useParams(); - const { cursor, records, fetchMore, isLoading, error } = useRepoData(url); + const { url } = Route.useParams() + const { cursor, records, fetchMore, isLoading, error } = useRepoData(url) - useDocumentTitle(records ? `${url} (PDS) | atp.tools` : "atp.tools"); + useDocumentTitle(records ? `${url} (PDS) | atp.tools` : 'atp.tools') - const loaderRef = useRef(null); + const loaderRef = useRef(null) useEffect(() => { - if (!loaderRef.current) return; + if (!loaderRef.current) return const observer = new IntersectionObserver( (entries) => { - const target = entries[0]; + const target = entries[0] if (target.isIntersecting && !isLoading && cursor) { - fetchMore(cursor); + fetchMore(cursor) } }, - { threshold: 0.1, rootMargin: "50px" }, - ); + { threshold: 0.1, rootMargin: '50px' }, + ) - observer.observe(loaderRef.current); - return () => observer.disconnect(); - }, [cursor, isLoading, fetchMore]); + observer.observe(loaderRef.current) + return () => observer.disconnect() + }, [cursor, isLoading, fetchMore]) if (error) { - return ; + return } if ((isLoading && !cursor) || !records) { - return ; + return } return (
- PDS: {url.includes("bsky.network") && "🍄"} {url} + PDS: {url.includes('bsky.network') && '🍄'} {url}
@@ -143,7 +143,7 @@ function RouteComponent() { handle: c.did, }} > - {c.did}{" "} + {c.did}{' '} {!c.active && } @@ -163,5 +163,5 @@ function RouteComponent() {
- ); + ) } diff --git a/src/routes/rnfgrertt/index.lazy.tsx b/src/routes/rnfgrertt/index.lazy.tsx new file mode 100644 index 0000000..8fe1e1f --- /dev/null +++ b/src/routes/rnfgrertt/index.lazy.tsx @@ -0,0 +1,9 @@ +import { createLazyFileRoute } from '@tanstack/react-router' + +export const Route = createLazyFileRoute('/rnfgrertt/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/rnfrgrertt/"!
+} diff --git a/src/routes/rnfgrertt/typing.lazy.tsx b/src/routes/rnfgrertt/typing.lazy.tsx new file mode 100644 index 0000000..00634cf --- /dev/null +++ b/src/routes/rnfgrertt/typing.lazy.tsx @@ -0,0 +1,418 @@ +import { createLazyFileRoute } from "@tanstack/react-router"; +import { useState, useEffect, useRef } from "preact/hooks"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, +} from "recharts"; + +// Type definitions +type TypingError = { + position: number; + timestamp: number; + expected: string; + actual: string; +}; + +type WPMDataPoint = { + time: number; + wpm: number; + errorsPerSecond: number; +}; + +type TypingStats = { + accuracy: string; + wpm: string; + time: string; + errorCount: number; +}; + +// Constants +const SAMPLE_TEXT = "The quick brown fox jumps over the lazy dog"; +const UPDATE_INTERVAL = 500; +const CHART_MARGIN = { top: 5, right: 30, left: 20, bottom: 5 }; + +// Custom hooks +const useWpmTracker = ( + startTime: number | null, + isFinished: boolean, + userInput: string, +) => { + const [wpmData, setWpmData] = useState([]); + const userInputRef = useRef(userInput); + const lastUpdateRef = useRef(0); + const prevErrorsRef = useRef(0); + + useEffect(() => { + userInputRef.current = userInput; + if (userInputRef.current.length >= 5 && wpmData.length < 1) { + updateWPMData(); + } + }, [userInput]); + + const calculateMetrics = (currentTime: number) => { + if (startTime === null) + return { timeElapsed: 0, correctChars: 0, currentErrors: 0 }; + const timeElapsed = (currentTime - startTime) / 1000; + const currentInput = userInputRef.current; + + // Calculate correct characters and current errors + let correctChars = 0; + let currentErrors = 0; + + for (let i = 0; i < currentInput.length; i++) { + if (currentInput[i] === SAMPLE_TEXT[i]) { + correctChars++; + } else { + currentErrors++; + } + } + + return { timeElapsed, correctChars, currentErrors }; + }; + + const updateWPMData = () => { + const now = Date.now(); + const { timeElapsed, correctChars, currentErrors } = calculateMetrics(now); + + // Calculate errors per second + const timeDiff = (now - (lastUpdateRef.current || now)) / 1000; + const errorDelta = currentErrors - prevErrorsRef.current; + const errorsPerSecond = timeDiff > 0 ? errorDelta / timeDiff : 0; + + setWpmData((prev) => [ + ...prev, + { + time: Number(timeElapsed.toFixed(1)), + wpm: Math.round(correctChars / 5 / (timeElapsed / 60)), + errorsPerSecond: Number(errorsPerSecond.toFixed(1)), + }, + ]); + + lastUpdateRef.current = now; + prevErrorsRef.current = currentErrors; + }; + + useEffect(() => { + if (!startTime || isFinished) return; + + // Subsequent updates at interval + const intervalId = setInterval(updateWPMData, UPDATE_INTERVAL); + + return () => clearInterval(intervalId); + }, [startTime, isFinished]); + + const resetWpmData = () => setWpmData([]); + return { wpmData, resetWpmData }; +}; + +const useErrorTracker = (errors: TypingError[]) => { + const [errorDistribution, setErrorDistribution] = useState< + { position: number; count: number }[] + >([]); + + useEffect(() => { + const distribution = new Array(SAMPLE_TEXT.length).fill(0); + errors.forEach(({ position }) => distribution[position]++); + setErrorDistribution( + distribution + .map((count, position) => ({ position, count })) + .filter((d) => d.count > 0), + ); + }, [errors]); + + return errorDistribution; +}; + +const useTypingTest = () => { + const [state, setState] = useState({ + userInput: "", + isFinished: false, + startTime: null as number | null, + endTime: null as number | null, + errors: [] as TypingError[], + }); + + const handleInput = (input: string) => { + if (state.isFinished) return; + + const newInput = input.slice(0, SAMPLE_TEXT.length); + const newErrors = [...state.errors]; + + // Track new errors + if (newInput.length > state.userInput.length) { + const newCharIndex = newInput.length - 1; + if (newInput[newCharIndex] !== SAMPLE_TEXT[newCharIndex]) { + newErrors.push({ + position: newCharIndex, + timestamp: Date.now(), + expected: SAMPLE_TEXT[newCharIndex], + actual: newInput[newCharIndex], + }); + } + } + + setState((prev) => ({ + ...prev, + userInput: newInput, + errors: newErrors, + startTime: prev.startTime || (newInput.length > 0 ? Date.now() : null), + isFinished: newInput.length === SAMPLE_TEXT.length, + endTime: + newInput.length === SAMPLE_TEXT.length ? Date.now() : prev.endTime, + })); + }; + + const resetTest = () => { + setState({ + userInput: "", + isFinished: false, + startTime: null, + endTime: null, + errors: [], + }); + }; + + return { ...state, handleInput, resetTest }; +}; + +// UI Components +const ResultsView = ({ + stats, + wpmData, + resetTest, +}: { + stats: TypingStats; + wpmData: WPMDataPoint[]; + resetTest: () => void; +}) => ( +
+

Test Results

+ + + +
+); + +const StatsGrid = ({ stats }: { stats: TypingStats }) => ( +
+ {Object.entries(stats).map(([key, value]) => ( + + ))} +
+); + +const StatBox = ({ + label, + value, +}: { + label: string; + value: string | number; +}) => ( +
+
{label}
+
{value}
+
{getUnit(label)}
+
+); + +const PerformanceChart = ({ wpmData }: { wpmData: WPMDataPoint[] }) => ( +
+

Performance Over Time

+ + + + + d.errorsPerSecond), 1)]} + /> + } /> + + + + +
+); + +const CustomTooltip = ({ active, payload }: any) => { + if (active && payload?.length) { + const data = payload[0].payload; + return ( +
+

Time: {data.time}s

+

WPM: {data.wpm}

+

+ Errors/sec: {data.errorsPerSecond} +

+
+ ); + } + return null; +}; + +const TypingArea = ({ + userInput, + handleInput, +}: { + userInput: string; + handleInput: (e: React.ChangeEvent) => void; +}) => ( +
+ +