Rework theme, file organization, org flow, etc (#25)

This commit is contained in:
Chris Estreich 2025-05-17 01:38:43 -07:00 • committed by GitHub
parent b2b4e288d2
commit 910900027d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 1740 additions and 7289 deletions

View file

@ -1,4 +1,4 @@
import '../src/styles/globals.css';
import '../src/app/globals.css';
import type { Preview } from '@storybook/react';

View file

@ -5,7 +5,7 @@
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""

View file

@ -62,6 +62,7 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.53.0",
"react-use": "^17.6.0",
"sonner": "^2.0.3",
"stripe": "^18.1.0",
"tailwind-merge": "^3.3.0",

198
pnpm-lock.yaml generated
View file

@ -128,6 +128,9 @@ importers:
react-hook-form:
specifier: ^7.53.0
version: 7.56.3(react@19.1.0)
react-use:
specifier: ^17.6.0
version: 17.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
sonner:
specifier: ^2.0.3
version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@ -3586,6 +3589,9 @@ packages:
'@types/istanbul-reports@3.0.4':
resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
'@types/js-cookie@2.2.7':
resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@ -3803,6 +3809,9 @@ packages:
'@webassemblyjs/wast-printer@1.14.1':
resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==}
'@xobotyi/scrollbar-width@1.9.5':
resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==}
'@xtuc/ieee754@1.2.0':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@ -4431,6 +4440,9 @@ packages:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
copy-to-clipboard@3.3.3:
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
core-js-compat@3.42.0:
resolution: {integrity: sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==}
@ -4504,6 +4516,9 @@ packages:
resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==}
engines: {node: '>= 0.10'}
css-in-js-utils@3.1.0:
resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
css-loader@6.11.0:
resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==}
engines: {node: '>= 12.13.0'}
@ -4519,6 +4534,10 @@ packages:
css-select@4.3.0:
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
css-tree@1.1.3:
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
engines: {node: '>=8.0.0'}
css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
@ -5173,9 +5192,15 @@ packages:
fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
fast-shallow-equal@1.0.0:
resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==}
fast-uri@3.0.6:
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
fastest-stable-stringify@2.0.2:
resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==}
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
@ -5590,6 +5615,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
hyphenate-style-name@1.1.0:
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@ -5642,6 +5670,9 @@ packages:
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
inline-style-prefixer@7.0.1:
resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==}
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@ -6063,6 +6094,9 @@ packages:
js-base64@3.7.7:
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
js-cookie@2.2.1:
resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==}
js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
@ -6354,6 +6388,9 @@ packages:
md5.js@1.3.5:
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
mdn-data@2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
media-typer@1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
@ -6477,6 +6514,12 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nano-css@5.6.2:
resolution: {integrity: sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==}
peerDependencies:
react: '*'
react-dom: '*'
nano-spawn@1.0.1:
resolution: {integrity: sha512-BfcvzBlUTxSDWfT+oH7vd6CbUV+rThLLHCIym/QO6GGLBsyVXleZs00fto2i2jzC/wPiBYk5jyOmpXWg4YopiA==}
engines: {node: '>=20.18'}
@ -7222,6 +7265,18 @@ packages:
'@types/react':
optional: true
react-universal-interface@0.6.2:
resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==}
peerDependencies:
react: '*'
tslib: '*'
react-use@17.6.0:
resolution: {integrity: sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==}
peerDependencies:
react: '*'
react-dom: '*'
react@19.1.0:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'}
@ -7315,6 +7370,9 @@ packages:
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
resolve-cwd@3.0.0:
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
engines: {node: '>=8'}
@ -7396,6 +7454,9 @@ packages:
rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
rtl-css-js@1.16.1:
resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@ -7463,6 +7524,10 @@ packages:
resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==}
engines: {node: '>= 10.13.0'}
screenfull@5.2.0:
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
engines: {node: '>=0.10.0'}
secure-compare@3.0.1:
resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==}
@ -7511,6 +7576,10 @@ packages:
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
engines: {node: '>= 0.4'}
set-harmonic-interval@1.0.1:
resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==}
engines: {node: '>=6.9'}
set-proto@1.0.0:
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
engines: {node: '>= 0.4'}
@ -7646,6 +7715,10 @@ packages:
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.5.6:
resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==}
engines: {node: '>=0.10.0'}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
@ -7686,6 +7759,9 @@ packages:
sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
stack-generator@2.0.10:
resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==}
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
@ -7699,6 +7775,12 @@ packages:
stackframe@1.3.4:
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
stacktrace-gps@3.1.2:
resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==}
stacktrace-js@2.0.2:
resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==}
stacktrace-parser@0.1.11:
resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==}
engines: {node: '>=6'}
@ -7863,6 +7945,9 @@ packages:
babel-plugin-macros:
optional: true
stylis@4.3.6:
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@ -7944,6 +8029,10 @@ packages:
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
throttle-debounce@3.0.1:
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
engines: {node: '>=10'}
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
@ -7994,6 +8083,9 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
toggle-selection@1.0.6:
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
@ -8027,6 +8119,9 @@ packages:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
engines: {node: '>=6.10'}
ts-easing@0.2.0:
resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==}
ts-node@10.9.2:
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
hasBin: true
@ -12260,6 +12355,8 @@ snapshots:
dependencies:
'@types/istanbul-lib-report': 3.0.3
'@types/js-cookie@2.2.7': {}
'@types/json-schema@7.0.15': {}
'@types/mdx@2.0.13': {}
@ -12582,6 +12679,8 @@ snapshots:
'@webassemblyjs/ast': 1.14.1
'@xtuc/long': 4.2.2
'@xobotyi/scrollbar-width@1.9.5': {}
'@xtuc/ieee754@1.2.0': {}
'@xtuc/long@4.2.2': {}
@ -13263,6 +13362,10 @@ snapshots:
cookie@1.0.2: {}
copy-to-clipboard@3.3.3:
dependencies:
toggle-selection: 1.0.6
core-js-compat@3.42.0:
dependencies:
browserslist: 4.24.5
@ -13377,6 +13480,10 @@ snapshots:
randombytes: 2.1.0
randomfill: 1.0.4
css-in-js-utils@3.1.0:
dependencies:
hyphenate-style-name: 1.1.0
css-loader@6.11.0(webpack@5.99.8(@swc/core@1.11.24)(esbuild@0.25.4)):
dependencies:
icss-utils: 5.1.0(postcss@8.5.3)
@ -13398,6 +13505,11 @@ snapshots:
domutils: 2.8.0
nth-check: 2.1.1
css-tree@1.1.3:
dependencies:
mdn-data: 2.0.14
source-map: 0.6.1
css-what@6.1.0: {}
css.escape@1.5.1: {}
@ -14123,8 +14235,12 @@ snapshots:
fast-safe-stringify@2.1.1: {}
fast-shallow-equal@1.0.0: {}
fast-uri@3.0.6: {}
fastest-stable-stringify@2.0.2: {}
fastq@1.19.1:
dependencies:
reusify: 1.1.0
@ -14601,6 +14717,8 @@ snapshots:
husky@9.1.7: {}
hyphenate-style-name@1.1.0: {}
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@ -14647,6 +14765,10 @@ snapshots:
ini@1.3.8: {}
inline-style-prefixer@7.0.1:
dependencies:
css-in-js-utils: 3.1.0
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0
@ -15309,6 +15431,8 @@ snapshots:
js-base64@3.7.7:
optional: true
js-cookie@2.2.1: {}
js-cookie@3.0.5: {}
js-tokens@4.0.0: {}
@ -15588,6 +15712,8 @@ snapshots:
inherits: 2.0.4
safe-buffer: 5.2.1
mdn-data@2.0.14: {}
media-typer@1.1.0: {}
memfs@3.5.3:
@ -15676,6 +15802,19 @@ snapshots:
ms@2.1.3: {}
nano-css@5.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
css-tree: 1.1.3
csstype: 3.1.3
fastest-stable-stringify: 2.0.2
inline-style-prefixer: 7.0.1
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
rtl-css-js: 1.16.1
stacktrace-js: 2.0.2
stylis: 4.3.6
nano-spawn@1.0.1: {}
nanoid@3.3.11: {}
@ -16477,6 +16616,30 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.3
react-universal-interface@0.6.2(react@19.1.0)(tslib@2.8.1):
dependencies:
react: 19.1.0
tslib: 2.8.1
react-use@17.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@types/js-cookie': 2.2.7
'@xobotyi/scrollbar-width': 1.9.5
copy-to-clipboard: 3.3.3
fast-deep-equal: 3.1.3
fast-shallow-equal: 1.0.0
js-cookie: 2.2.1
nano-css: 5.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-universal-interface: 0.6.2(react@19.1.0)(tslib@2.8.1)
resize-observer-polyfill: 1.5.1
screenfull: 5.2.0
set-harmonic-interval: 1.0.1
throttle-debounce: 3.0.1
ts-easing: 0.2.0
tslib: 2.8.1
react@19.1.0: {}
read-pkg@3.0.0:
@ -16601,6 +16764,8 @@ snapshots:
requires-port@1.0.0: {}
resize-observer-polyfill@1.5.1: {}
resolve-cwd@3.0.0:
dependencies:
resolve-from: 5.0.0
@ -16730,6 +16895,10 @@ snapshots:
rrweb-cssom@0.8.0: {}
rtl-css-js@1.16.1:
dependencies:
'@babel/runtime': 7.27.1
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@ -16790,6 +16959,8 @@ snapshots:
ajv-formats: 2.1.1(ajv@8.17.1)
ajv-keywords: 5.1.0(ajv@8.17.1)
screenfull@5.2.0: {}
secure-compare@3.0.1: {}
secure-json-parse@2.7.0: {}
@ -16853,6 +17024,8 @@ snapshots:
functions-have-names: 1.2.3
has-property-descriptors: 1.0.2
set-harmonic-interval@1.0.1: {}
set-proto@1.0.0:
dependencies:
dunder-proto: 1.0.1
@ -17047,6 +17220,8 @@ snapshots:
buffer-from: 1.1.2
source-map: 0.6.1
source-map@0.5.6: {}
source-map@0.6.1: {}
source-map@0.7.4: {}
@ -17093,6 +17268,10 @@ snapshots:
sprintf-js@1.1.3: {}
stack-generator@2.0.10:
dependencies:
stackframe: 1.3.4
stack-trace@0.0.10: {}
stack-utils@2.0.6:
@ -17103,6 +17282,17 @@ snapshots:
stackframe@1.3.4: {}
stacktrace-gps@3.1.2:
dependencies:
source-map: 0.5.6
stackframe: 1.3.4
stacktrace-js@2.0.2:
dependencies:
error-stack-parser: 2.1.4
stack-generator: 2.0.10
stacktrace-gps: 3.1.2
stacktrace-parser@0.1.11:
dependencies:
type-fest: 0.7.1
@ -17289,6 +17479,8 @@ snapshots:
optionalDependencies:
'@babel/core': 7.27.1
stylis@4.3.6: {}
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
@ -17367,6 +17559,8 @@ snapshots:
dependencies:
real-require: 0.2.0
throttle-debounce@3.0.1: {}
through@2.3.8: {}
timers-browserify@2.0.12:
@ -17404,6 +17598,8 @@ snapshots:
dependencies:
is-number: 7.0.0
toggle-selection@1.0.6: {}
toidentifier@1.0.1: {}
totalist@3.0.1: {}
@ -17426,6 +17622,8 @@ snapshots:
ts-dedent@2.2.0: {}
ts-easing@0.2.0: {}
ts-node@10.9.2(@swc/core@1.11.24)(@types/node@22.15.17)(typescript@5.8.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 226 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 226 KiB

View file

@ -15,18 +15,15 @@ export async function validateAuth(): Promise<
{ userId: string; orgId: string } | ApiResponse
> {
const { userId, orgId } = await auth();
if (!userId) {
return {
success: false,
error: 'Unauthorized: User required',
};
return { success: false, error: 'Unauthorized: User required' };
}
if (!orgId) {
return {
success: false,
error: 'Unauthorized: Organization required',
};
return { success: false, error: 'Unauthorized: Organization required' };
}
return { userId, orgId };
}
@ -47,6 +44,7 @@ export function handleError(error: unknown, eventPrefix: string): ApiResponse {
event: `${eventPrefix}_update_error`,
error: error instanceof Error ? error.message : 'Unknown error',
});
return {
success: false,
error:

View file

@ -0,0 +1,96 @@
'use client';
import { useState } from 'react';
import { useOrganization } from '@clerk/nextjs';
import { useQuery } from '@tanstack/react-query';
import type { AuditLogType } from '@/db/schema';
import { timePeriods, type TimePeriod } from '@/schemas';
import { getAuditLogs } from '@/actions/auditLogs';
import {
Button,
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from '@/components/ui';
import { AuditLogDetails } from '@/components/audit-logs/AuditLogDetails';
import { AuditLogEntry } from '@/components/audit-logs/AuditLogEntry';
export const AuditLogs = () => {
const { organization } = useOrganization();
const [timePeriod, setTimePeriod] = useState<TimePeriod>(7);
const [selectedLog, setSelectedLog] = useState<AuditLogType | null>(null);
const { data: logs = [], isLoading } = useQuery({
queryKey: ['auditLogs', organization?.id, timePeriod],
queryFn: () =>
getAuditLogs({
orgId: organization?.id,
limit: 20,
nRecentDays: timePeriod,
}),
enabled: !!organization?.id,
});
return (
<>
<Card>
<CardHeader>
<CardTitle>All Activity</CardTitle>
<CardDescription>
Showing all audit logs for your organization
</CardDescription>
</CardHeader>
<CardContent>
<div className="mb-4 flex space-x-2">
{timePeriods.map((period) => (
<Button
key={period}
variant={timePeriod === period ? 'default' : 'secondary'}
size="sm"
onClick={() => setTimePeriod(period)}
>
Last {period} days
</Button>
))}
</div>
<div className="space-y-1">
{isLoading ? (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
) : logs.length > 0 ? (
logs.map((log: AuditLogType) => (
<AuditLogEntry
key={log.id}
log={log}
onClick={(log: AuditLogType) => setSelectedLog(log)}
/>
))
) : (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">
No activity found
</p>
</div>
)}
</div>
</CardContent>
</Card>
<Drawer open={!!selectedLog} onClose={() => setSelectedLog(null)}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Activity Details</DrawerTitle>
</DrawerHeader>
{selectedLog && <AuditLogDetails log={selectedLog} />}
</DrawerContent>
</Drawer>
</>
);
};

View file

@ -0,0 +1,5 @@
import { AuditLogs } from './AuditLogs';
export default function Page() {
return <AuditLogs />;
}

View file

@ -1,21 +1,9 @@
'use client';
import { UsageCard } from '@/components/usage/UsageCard';
import { AuditLogCard } from '@/components/audit-logs/AuditLogCard';
import { useTranslations } from 'next-intl';
import {
TitleBar,
UsageAnalyticsCard,
AuditLogCard,
} from '@/components/dashboard';
export const Dashboard = () => {
const t = useTranslations('DashboardIndex');
return (
<>
<TitleBar title={t('title_bar')} />
<UsageAnalyticsCard />
<AuditLogCard />
</>
);
};
export const Dashboard = () => (
<>
<UsageCard />
<AuditLogCard />
</>
);

View file

@ -1,33 +0,0 @@
import { X } from 'lucide-react';
import { Button } from '@/components/ui';
import type { Filter } from './types';
export const ActiveFilter = ({
filter,
onClear,
}: {
filter: Filter;
onClear: () => void;
}) => {
if (!filter) {
return null;
}
return (
<div className="mb-4 flex items-center rounded-md bg-muted p-2">
<span className="mr-2 text-sm">
Filtered by {filter.type}: <strong>{filter.name}</strong>
</span>
<Button
variant="ghost"
size="sm"
onClick={onClear}
className="size-6 p-0"
>
<X className="size-4" />
</Button>
</div>
);
};

View file

@ -1,64 +0,0 @@
'use client';
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui';
import { UsageAnalyticsCard } from '@/components/dashboard';
import type { Developer, Filter, Model, Task, ViewMode } from './types';
import { ViewModeToggle } from './ViewModeToggle';
import { ActiveFilter } from './ActiveFilter';
import { Developers } from './Developers';
import { Models } from './Models';
import { Tasks } from './Tasks';
import { TaskDetails } from './TaskDetails';
export const Analytics = () => {
const [viewMode, setViewMode] = useState<ViewMode>('tasks');
const [filter, setFilter] = useState<Filter>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const onDeveloperClick = (developer: Developer) => {
setFilter({ type: 'developer', id: developer.id, name: developer.name });
setViewMode('tasks');
};
const onModelClick = (model: Model) => {
setFilter({ type: 'model', id: model.id, name: model.name });
setViewMode('tasks');
};
return (
<div className="flex flex-col gap-2">
<UsageAnalyticsCard />
<ViewModeToggle viewMode={viewMode} setViewMode={setViewMode} />
{filter && (
<ActiveFilter filter={filter} onClear={() => setFilter(null)} />
)}
<Card>
<CardHeader>
<CardTitle>
{viewMode === 'developers'
? 'Developers'
: viewMode === 'models'
? 'Models'
: 'Tasks'}
</CardTitle>
</CardHeader>
<CardContent>
{viewMode === 'tasks' && (
<Tasks
filter={filter}
onTaskClick={(task: Task) => setSelectedTask(task)}
/>
)}
{viewMode === 'developers' && (
<Developers onDeveloperClick={onDeveloperClick} />
)}
{viewMode === 'models' && <Models onModelClick={onModelClick} />}
</CardContent>
</Card>
<TaskDetails task={selectedTask} onClose={() => setSelectedTask(null)} />
</div>
);
};

View file

@ -1,33 +0,0 @@
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui';
import { type ViewMode, viewModes } from './types';
export const ViewModeToggle = ({
viewMode,
setViewMode,
}: {
viewMode: ViewMode;
setViewMode: (mode: ViewMode) => void;
}) => {
const t = useTranslations('Analytics');
return (
<div className="flex flex-col gap-1">
<h3 className="text-sm font-medium">{t('view_mode_title')}</h3>
<div className="flex gap-2">
{viewModes.map((mode) => (
<Button
key={mode}
variant={viewMode === mode ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode(mode)}
>
{t(`view_mode_${mode}`)}
</Button>
))}
</div>
</div>
);
};

View file

@ -1,20 +0,0 @@
import { getLocale, getTranslations } from 'next-intl/server';
import { TitleBar } from '@/components/dashboard/TitleBar';
import { Analytics } from './Analytics';
export default async function Page() {
const locale = await getLocale();
const t = await getTranslations({ locale, namespace: 'Analytics' });
return (
<>
<TitleBar
title={t('title_bar')}
description={t('title_bar_description')}
/>
<Analytics />
</>
);
}

View file

@ -1,103 +0,0 @@
'use client';
import { useState } from 'react';
import { useOrganization } from '@clerk/nextjs';
import { useQuery } from '@tanstack/react-query';
import {
Button,
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from '@/components/ui';
import { TitleBar } from '@/components/dashboard/TitleBar';
import { AuditLogDetails } from '@/components/dashboard/AuditLogDetails';
import { AuditLogEntry } from '@/components/dashboard/AuditLogEntry';
import type { AuditLogType } from '@/db/schema';
import { getAuditLogs } from '@/actions/auditLogs';
type TimePeriod = '7' | '30' | '90';
export default function Page() {
const [timePeriod, setTimePeriod] = useState<TimePeriod>('7');
const [selectedLog, setSelectedLog] = useState<AuditLogType | null>(null);
const { organization } = useOrganization();
const { data: logs = [], isLoading } = useQuery({
queryKey: ['auditLogs', organization?.id, Number(timePeriod)],
queryFn: async () =>
await getAuditLogs({
orgId: organization?.id,
limit: 20,
nRecentDays: Number(timePeriod),
}),
enabled: !!organization?.id,
});
const handleLogClick = (log: AuditLogType) => setSelectedLog(log);
const handleCloseDrawer = () => setSelectedLog(null);
return (
<>
<TitleBar
title="Audit Logs"
description="View all organization activity and changes"
/>
<div className="w-2/3 rounded-md bg-card p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold">All Activity</h3>
<p className="text-sm text-muted-foreground">
Showing all audit logs for your organization
</p>
</div>
<div className="mb-4 flex space-x-2">
<Button
variant={timePeriod === '7' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('7')}
>
Last 7 days
</Button>
<Button
variant={timePeriod === '30' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('30')}
>
Last 30 days
</Button>
<Button
variant={timePeriod === '90' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('90')}
>
Last 90 days
</Button>
</div>
<div className="space-y-1">
{isLoading ? (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
) : logs.length > 0 ? (
logs.map((log: AuditLogType) => (
<AuditLogEntry key={log.id} log={log} onClick={handleLogClick} />
))
) : (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">No activity found</p>
</div>
)}
</div>
</div>
<Drawer open={!!selectedLog} onClose={handleCloseDrawer}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Activity Details</DrawerTitle>
</DrawerHeader>
{selectedLog && <AuditLogDetails log={selectedLog} />}
</DrawerContent>
</Drawer>
</>
);
}

View file

@ -1,18 +0,0 @@
import { DashboardHeader } from '@/components/dashboard/DashboardHeader';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<DashboardHeader />
<div className="min-h-[calc(100vh-72px)] bg-muted">
<div className="max-w-screen-lg mx-auto px-4 pb-16 pt-6">
{children}
</div>
</div>
</>
);
}

View file

@ -1,89 +0,0 @@
'use client';
import { useTranslations } from 'next-intl';
import { useTheme } from 'next-themes';
import { dark } from '@clerk/themes';
import { OrganizationProfile } from '@clerk/nextjs';
import DefaultParametersPage from '@/components/dashboard/DefaultParametersPage';
import ProviderWhitelistPage from '@/components/dashboard/ProviderWhitelistPage';
export const OrgProfile = () => {
const t = useTranslations('OrganizationProfile');
const { resolvedTheme } = useTheme();
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
return (
<OrganizationProfile
appearance={{ baseTheme }}
routing="path"
path="/dashboard/org"
afterLeaveOrganizationUrl="/onboarding/select-org"
>
<OrganizationProfile.Page
label={t('provider_whitelist')}
url="provider-whitelist"
labelIcon={<SettingsIcon />}
>
<ProviderWhitelistPage />
</OrganizationProfile.Page>
<OrganizationProfile.Page
label={t('default_parameters')}
url="default-parameters"
labelIcon={<ParametersIcon />}
>
<DefaultParametersPage />
</OrganizationProfile.Page>
</OrganizationProfile>
);
};
const SettingsIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-4"
>
<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z" />
<path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z" />
<path d="M12 2v2" />
<path d="M12 22v-2" />
<path d="m17 20.66-1-1.73" />
<path d="M11 10.27 7 3.34" />
<path d="m20.66 17-1.73-1" />
<path d="m3.34 7 1.73 1" />
<path d="M14 12h8" />
<path d="M2 12h2" />
<path d="m20.66 7-1.73 1" />
<path d="m3.34 17 1.73-1" />
<path d="m17 3.34-1 1.73" />
<path d="m7 20.66 1-1.73" />
</svg>
);
};
const ParametersIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-4"
>
<path d="M3 3v18h18" />
<path d="M18.4 9a9 9 0 0 0-9.4 9" />
<path d="M8 12l4-4 4 4" />
</svg>
);
};

View file

@ -1,5 +0,0 @@
import { OrgProfile } from './OrgProfile';
export default function Page() {
return <OrgProfile />;
}

View file

@ -1,18 +0,0 @@
'use client';
import { useTheme } from 'next-themes';
import { dark } from '@clerk/themes';
import { UserProfile as ClerkUserProfile } from '@clerk/nextjs';
export const UserProfile = () => {
const { resolvedTheme } = useTheme();
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
return (
<ClerkUserProfile
appearance={{ baseTheme }}
routing="path"
path="/dashboard/user"
/>
);
};

View file

@ -1,5 +0,0 @@
import { UserProfile } from './UserProfile';
export default function Page() {
return <UserProfile />;
}

View file

@ -1,16 +1,26 @@
import { currentUser } from '@clerk/nextjs/server';
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
import { NavbarHeader, NavbarMenu, Section } from '@/components/layout';
export default async function AuthenticatedLayout({
children,
}: {
children: React.ReactNode;
}) {
const user = await currentUser();
const { orgId } = await auth();
if (!user) {
return redirect('/sign-in');
if (!orgId) {
redirect('/onboarding/select-org');
}
return children;
return (
<>
<NavbarHeader className="h-[72px]" />
<NavbarMenu className="h-[72px]" />
<Section divider={false} className="min-h-[calc(100vh-72px-72px-2px)]">
<div className="flex flex-col gap-8 py-8">{children}</div>
</Section>
</>
);
}

View file

@ -20,6 +20,7 @@ import {
Input,
Slider,
} from '@/components/ui';
import { CheckCheck, FlaskConical, SlidersHorizontal } from 'lucide-react';
type DefaultParamsFormValues = {
experimentalPowerSteering: boolean;
@ -46,7 +47,7 @@ type DefaultParamsFormValues = {
matchPrecision: number;
};
const DefaultParametersPage = () => {
export const DefaultParameters = () => {
const t = useTranslations('ProviderWhitelist');
const [isSaving, setIsSaving] = useState(false);
@ -109,32 +110,17 @@ const DefaultParametersPage = () => {
</h1>
</div>
</div>
<p className="mb-6 text-sm text-muted-foreground">
{t('parameters_section_description')}
</p>
<div className="space-y-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="space-y-4 rounded-lg p-4">
<div className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-5"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" />
<path d="m9 12 2 2 4-4" />
</svg>
<CheckCheck />
<h2 className="text-lg font-medium">Checkpoints</h2>
</div>
<div className="mt-4 space-y-4">
<FormField
control={form.control}
@ -160,8 +146,7 @@ const DefaultParametersPage = () => {
/>
</div>
</div>
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="space-y-4 rounded-lg p-4">
<div className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
@ -326,23 +311,9 @@ const DefaultParametersPage = () => {
</div>
</div>
{/* Model Settings Section */}
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="space-y-4 rounded-lg p-4">
<div className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-5"
>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<polyline points="3.29 7 12 12 20.71 7" />
<line x1="12" y1="22" x2="12" y2="12" />
</svg>
<SlidersHorizontal />
<h2 className="text-lg font-medium">Model Settings</h2>
</div>
@ -721,26 +692,11 @@ const DefaultParametersPage = () => {
</div>
</div>
{/* Experimental Section */}
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-5"
>
<path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2" />
<path d="M8.5 2h7" />
<path d="M7 16h10" />
</svg>
<FlaskConical />
<h2 className="text-lg font-medium">Experimental</h2>
</div>
<div className="mt-4 space-y-4">
<FormField
control={form.control}
@ -790,5 +746,3 @@ const DefaultParametersPage = () => {
</>
);
};
export default DefaultParametersPage;

View file

@ -0,0 +1,37 @@
'use client';
import { useTranslations } from 'next-intl';
import { OrganizationProfile } from '@clerk/nextjs';
import { ListTodo, SlidersHorizontal } from 'lucide-react';
import { DefaultParameters } from './DefaultParameters';
import { ProviderWhitelist } from './ProviderWhitelist';
export const Org = () => {
const t = useTranslations('OrganizationProfile');
return (
<div className="mx-auto">
<OrganizationProfile
routing="path"
path="/org"
afterLeaveOrganizationUrl="/onboarding/select-org"
>
<OrganizationProfile.Page
label={t('provider_whitelist')}
url="provider-whitelist"
labelIcon={<ListTodo className="size-4" />}
>
<ProviderWhitelist />
</OrganizationProfile.Page>
<OrganizationProfile.Page
label={t('default_parameters')}
url="default-parameters"
labelIcon={<SlidersHorizontal className="size-4" />}
>
<DefaultParameters />
</OrganizationProfile.Page>
</OrganizationProfile>
</div>
);
};

View file

@ -53,7 +53,7 @@ const initialProviders = [
},
];
const ProviderWhitelistPage = () => {
export const ProviderWhitelist = () => {
const t = useTranslations('ProviderWhitelist');
const [providers, setProviders] = useState(initialProviders);
@ -61,7 +61,7 @@ const ProviderWhitelistPage = () => {
const [allowAllProviders, setAllowAllProviders] = useState(true);
const [isUpdating, setIsUpdating] = useState(false);
// Common error handling function for API calls
// Common error handling function for API calls.
const handleApiError = (error: unknown, errorMessage: string) => {
console.error(errorMessage, error);
toast.error(errorMessage);
@ -75,26 +75,27 @@ const ProviderWhitelistPage = () => {
setAllowAllProviders(newAllowAllProviders);
let updatedProviders = providers;
if (newAllowAllProviders) {
// Enable all providers and their models
// Enable all providers and their models/
updatedProviders = providers.map((provider) => ({
...provider,
enabled: true,
models: provider.models.map((model) => ({
...model,
enabled: true,
})),
models: provider.models.map((model) => ({ ...model, enabled: true })),
}));
setProviders(updatedProviders);
}
// Call API to update backend with the changed allow all providers setting
// Call API to update backend with the changed allow all providers setting.
setIsUpdating(true);
try {
const result = await updateAllowAllProviders({
allowAllProviders: newAllowAllProviders,
policyVersion,
});
if (!result.success) {
throw new Error(result.error || 'Failed to update setting');
}
@ -115,7 +116,7 @@ const ProviderWhitelistPage = () => {
return;
}
// Find the provider to get its current state
// Find the provider to get its current state.
const provider = providers.find((p) => p.id === providerId);
if (!provider) return;
@ -166,7 +167,7 @@ const ProviderWhitelistPage = () => {
return;
}
// Find the model to get its current state
// Find the model to get its current state.
const provider = providers.find((p) => p.id === providerId);
if (!provider) return;
@ -177,24 +178,19 @@ const ProviderWhitelistPage = () => {
const newEnabled = !model.enabled;
const updatedProviders = providers.map((provider) => {
if (provider.id === providerId) {
return {
...provider,
models: provider.models.map((model) => {
if (model.id === modelId) {
return { ...model, enabled: newEnabled };
}
return model;
}),
};
}
return provider;
return provider.id === providerId
? {
...provider,
models: provider.models.map((model) =>
model.id === modelId ? { ...model, enabled: newEnabled } : model,
),
}
: provider;
});
setProviders(updatedProviders);
// Call API to update backend with the changed model status
setIsUpdating(true);
try {
const result = await updateModelStatus({
providerId,
@ -202,6 +198,7 @@ const ProviderWhitelistPage = () => {
enabled: newEnabled,
policyVersion,
});
if (!result.success) {
throw new Error(result.error || 'Failed to update model status');
}
@ -222,11 +219,9 @@ const ProviderWhitelistPage = () => {
</h1>
</div>
</div>
<p className="mb-6 text-sm text-muted-foreground">
{t('providers_section_description')}
</p>
<div className="space-y-8">
<div className="space-y-6">
<div className="mb-4 flex items-center space-x-2">
@ -242,7 +237,6 @@ const ProviderWhitelistPage = () => {
Allow All Providers
</Label>
</div>
<div className={allowAllProviders ? 'opacity-50' : ''}>
{providers.map((provider) => (
<div key={provider.id} className="mb-4">
@ -260,7 +254,6 @@ const ProviderWhitelistPage = () => {
{provider.name}
</Label>
</div>
<div className="ml-6 space-y-2">
{provider.models.map((model) => (
<div key={model.id} className="flex items-center space-x-2">
@ -284,7 +277,6 @@ const ProviderWhitelistPage = () => {
</div>
))}
</div>
<div className="flex items-center space-x-2 pt-2">
<Badge variant="outline" className="text-xs">
{`Policy v${policyVersion}`}
@ -300,5 +292,3 @@ const ProviderWhitelistPage = () => {
</>
);
};
export default ProviderWhitelistPage;

View file

@ -0,0 +1,5 @@
import { Org } from './Org';
export default function Page() {
return <Org />;
}

View file

@ -0,0 +1,21 @@
import { X } from 'lucide-react';
import { Button } from '@/components/ui';
import type { Filter } from './types';
type ActiveFilterProps = {
filter: Filter;
onClear: () => void;
};
export const ActiveFilter = ({ filter, onClear }: ActiveFilterProps) => (
<div className="mb-4 flex items-center rounded-md bg-muted p-2">
<span className="mr-2 text-sm">
Filtered by {filter.type}: <strong>{filter.name}</strong>
</span>
<Button variant="ghost" size="sm" onClick={onClear} className="size-6 p-0">
<X className="size-4" />
</Button>
</div>
);

View file

@ -1,6 +1,6 @@
import type { ColumnDef } from '@tanstack/react-table';
import { DataTable } from '@/components/data-table';
import { DataTable } from '@/components/layout/DataTable';
import type { Developer } from './types';
@ -53,9 +53,9 @@ const mockDevelopers: Developer[] = [
];
export const Developers = ({
onDeveloperClick,
onClick,
}: {
onDeveloperClick: (developer: Developer) => void;
onClick: (developer: Developer) => void;
}) => {
const developerColumns: ColumnDef<Developer>[] = [
{
@ -66,7 +66,7 @@ export const Developers = ({
return (
<button
onClick={() => onDeveloperClick(developer)}
onClick={() => onClick(developer)}
className="text-left font-medium text-primary hover:underline"
>
{developer.name}

View file

@ -1,6 +1,6 @@
import type { ColumnDef } from '@tanstack/react-table';
import { DataTable } from '@/components/data-table';
import { DataTable } from '@/components/layout/DataTable';
import type { Model } from './types';
@ -47,11 +47,7 @@ const mockModels: Model[] = [
},
];
export const Models = ({
onModelClick,
}: {
onModelClick: (model: Model) => void;
}) => {
export const Models = ({ onClick }: { onClick: (model: Model) => void }) => {
const modelColumns: ColumnDef<Model>[] = [
{
accessorKey: 'name',
@ -61,7 +57,7 @@ export const Models = ({
return (
<button
onClick={() => onModelClick(model)}
onClick={() => onClick(model)}
className="text-left font-medium text-primary hover:underline"
>
{model.name}

View file

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import { DataTable } from '@/components/data-table';
import { DataTable } from '@/components/layout/DataTable';
import type { Filter, Task, ConversationMessage } from './types';
@ -202,10 +202,10 @@ const mockTasks: Task[] = [
export const Tasks = ({
filter,
onTaskClick,
onClick,
}: {
filter: Filter;
onTaskClick: (task: Task) => void;
filter: Filter | null;
onClick: (task: Task) => void;
}) => {
const filteredTasks = useMemo(() => {
if (!filter) {
@ -228,7 +228,7 @@ export const Tasks = ({
return (
<button
onClick={() => onTaskClick(task)}
onClick={() => onClick(task)}
className="text-left font-medium text-primary hover:underline"
>
{task.id}

View file

@ -0,0 +1,52 @@
'use client';
import React, { useState } from 'react';
import { UsageCard } from '@/components/usage/UsageCard';
import type { Developer, Filter, Model, Task, ViewMode } from './types';
import { ViewModeToggle } from './ViewModeToggle';
import { ActiveFilter } from './ActiveFilter';
import { Developers } from './Developers';
import { Models } from './Models';
import { Tasks } from './Tasks';
import { TaskDetails } from './TaskDetails';
export const Usage = () => {
const [viewMode, setViewMode] = useState<ViewMode>('tasks');
const [filter, setFilter] = useState<Filter | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
return (
<>
<div className="flex flex-col gap-4">
<UsageCard />
<ViewModeToggle viewMode={viewMode} setViewMode={setViewMode} />
{filter && (
<ActiveFilter filter={filter} onClear={() => setFilter(null)} />
)}
{viewMode === 'tasks' ? (
<Tasks
filter={filter}
onClick={(task: Task) => setSelectedTask(task)}
/>
) : viewMode === 'developers' ? (
<Developers
onClick={({ id, name }: Developer) => {
setFilter({ type: 'developer', id, name });
setViewMode('tasks');
}}
/>
) : (
<Models
onClick={({ id, name }: Model) => {
setFilter({ type: 'model', id, name });
setViewMode('tasks');
}}
/>
)}
</div>
<TaskDetails task={selectedTask} onClose={() => setSelectedTask(null)} />
</>
);
};

View file

@ -0,0 +1,30 @@
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui';
import { type ViewMode, viewModes } from './types';
export const ViewModeToggle = ({
viewMode,
setViewMode,
}: {
viewMode: ViewMode;
setViewMode: (mode: ViewMode) => void;
}) => {
const t = useTranslations('Analytics');
return (
<div className="flex gap-2">
{viewModes.map((mode) => (
<Button
key={mode}
variant={viewMode === mode ? 'default' : 'secondary'}
size="sm"
onClick={() => setViewMode(mode)}
>
{t(`view_mode_${mode}`)}
</Button>
))}
</div>
);
};

View file

@ -0,0 +1,5 @@
import { Usage } from './Usage';
export default async function Page() {
return <Usage />;
}

View file

@ -53,4 +53,4 @@ export type Filter = {
type: 'developer' | 'model';
id: string;
name: string;
} | null;
};

View file

@ -0,0 +1,7 @@
import { UserButton as ClerkUserButton } from '@clerk/nextjs';
export const UserButton = () => (
<div className="absolute top-8 right-8">
<ClerkUserButton />
</div>
);

View file

@ -0,0 +1,18 @@
import { Logo } from '@/components/layout';
import { UserButton } from './UserButton';
export default async function OnboardingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="flex flex-col gap-8">
<Logo className="self-center" />
{children}
</div>
<UserButton />
</div>
);
}

View file

@ -0,0 +1,5 @@
import { CreateOrganization } from '@clerk/nextjs';
export default async function Page() {
return <CreateOrganization afterCreateOrganizationUrl="/dashboard" />;
}

View file

@ -0,0 +1,11 @@
import { OrganizationList } from '@clerk/nextjs';
export default async function Page() {
return (
<OrganizationList
afterSelectOrganizationUrl="/dashboard"
afterCreateOrganizationUrl="/dashboard"
hidePersonal
/>
);
}

View file

@ -0,0 +1,5 @@
import { UserProfile } from '@clerk/nextjs';
export default function Page() {
return <UserProfile routing="path" path="/user" />;
}

16
src/app/Home.tsx Normal file
View file

@ -0,0 +1,16 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@clerk/nextjs';
export const Home = () => {
const { isSignedIn } = useAuth();
const router = useRouter();
useEffect(() => {
router.push(isSignedIn ? '/dashboard' : '/sign-in');
}, [router, isSignedIn]);
return null;
};

View file

@ -32,10 +32,10 @@ export const DeepLink = ({ vsCodeUrl, cursorUrl }: DeepLinkProps) => {
open VSCode with your credentials.
</div>
<div className="flex flex-row justify-between gap-4">
<Button variant="outline" className="flex-1" asChild>
<Button variant="secondary" className="flex-1" asChild>
<Link href={vsCodeUrl}>Open in VSCode</Link>
</Button>
<Button variant="outline" className="flex-1" asChild>
<Button variant="secondary" className="flex-1" asChild>
<Link href={cursorUrl}>Open in Cursor</Link>
</Button>
</div>

219
src/app/globals.css Normal file
View file

@ -0,0 +1,219 @@
@import 'tailwindcss';
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
--radius: 0.3rem;
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
--tracking-normal: var(--tracking-normal);
--spacing: var(--spacing);
--letter-spacing: var(--letter-spacing);
--shadow-offset-y: var(--shadow-offset-y);
--shadow-offset-x: var(--shadow-offset-x);
--shadow-spread: var(--shadow-spread);
--shadow-blur: var(--shadow-blur);
--shadow-opacity: var(--shadow-opacity);
--color-shadow-color: var(--shadow-color);
--animate-hop: hop 0.8s ease-in-out infinite;
@keyframes hop {
0%,
100% {
transform: none;
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: translateY(-8px);
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
}
:root {
--background: oklch(0.99 0 0);
--foreground: oklch(0 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0 0 0);
--popover: oklch(0.99 0 0);
--popover-foreground: oklch(0 0 0);
--primary: oklch(0 0 0);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.94 0 0);
--secondary-foreground: oklch(0 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.44 0 0);
--accent: oklch(0.94 0 0);
--accent-foreground: oklch(0 0 0);
--destructive: oklch(0.63 0.19 23.03);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.92 0 0);
--input: oklch(0.94 0 0);
--ring: oklch(0 0 0);
--chart-1: oklch(0.81 0.17 75.35);
--chart-2: oklch(0.55 0.22 264.53);
--chart-3: oklch(0.72 0 0);
--chart-4: oklch(0.92 0 0);
--chart-5: oklch(0.56 0 0);
--sidebar: oklch(0.99 0 0);
--sidebar-foreground: oklch(0 0 0);
--sidebar-primary: oklch(0 0 0);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.94 0 0);
--sidebar-accent-foreground: oklch(0 0 0);
--sidebar-border: oklch(0.94 0 0);
--sidebar-ring: oklch(0 0 0);
--radius: 0.3rem;
--shadow-2xs: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.01);
--shadow-xs: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.01);
--shadow-sm:
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
0px 1px 2px -0.5px hsl(0 0% 0% / 0.03);
--shadow:
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
0px 1px 2px -0.5px hsl(0 0% 0% / 0.03);
--shadow-md:
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
0px 2px 4px -0.5px hsl(0 0% 0% / 0.03);
--shadow-lg:
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
0px 4px 6px -0.5px hsl(0 0% 0% / 0.03);
--shadow-xl:
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
0px 8px 10px -0.5px hsl(0 0% 0% / 0.03);
--shadow-2xl: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.07);
--shadow-color: hsl(0 0% 0%);
--shadow-opacity: 0.03;
--shadow-blur: 9.5px;
--shadow-spread: 0.5px;
--shadow-offset-x: 0px;
--shadow-offset-y: 1.5px;
--letter-spacing: 0em;
--spacing: 0.25rem;
--tracking-normal: 0em;
}
.dark {
--background: oklch(0 0 0);
--foreground: oklch(1 0 0);
--card: oklch(0.14 0 0);
--card-foreground: oklch(1 0 0);
--popover: oklch(0.18 0 0);
--popover-foreground: oklch(1 0 0);
--primary: oklch(1 0 0);
--primary-foreground: oklch(0 0 0);
--secondary: oklch(0.25 0 0);
--secondary-foreground: oklch(1 0 0);
--muted: oklch(0.23 0 0);
--muted-foreground: oklch(0.72 0 0);
--accent: oklch(0.32 0 0);
--accent-foreground: oklch(1 0 0);
--destructive: oklch(0.69 0.2 23.91);
--destructive-foreground: oklch(0 0 0);
--border: oklch(0.26 0 0);
--input: oklch(0.32 0 0);
--ring: oklch(0.72 0 0);
--chart-1: oklch(0.81 0.17 75.35);
--chart-2: oklch(0.58 0.21 260.84);
--chart-3: oklch(0.56 0 0);
--chart-4: oklch(0.44 0 0);
--chart-5: oklch(0.92 0 0);
--sidebar: oklch(0.18 0 0);
--sidebar-foreground: oklch(1 0 0);
--sidebar-primary: oklch(1 0 0);
--sidebar-primary-foreground: oklch(0 0 0);
--sidebar-accent: oklch(0.32 0 0);
--sidebar-accent-foreground: oklch(1 0 0);
--sidebar-border: oklch(0.32 0 0);
--sidebar-ring: oklch(0.72 0 0);
--font-sans: Geist, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono: IBM Plex Mono, monospace;
--radius: 0.3rem;
--shadow-2xs: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.01);
--shadow-xs: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.01);
--shadow-sm:
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
0px 1px 2px -0.5px hsl(0 0% 0% / 0.03);
--shadow:
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
0px 1px 2px -0.5px hsl(0 0% 0% / 0.03);
--shadow-md:
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
0px 2px 4px -0.5px hsl(0 0% 0% / 0.03);
--shadow-lg:
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
0px 4px 6px -0.5px hsl(0 0% 0% / 0.03);
--shadow-xl:
0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.03),
0px 8px 10px -0.5px hsl(0 0% 0% / 0.03);
--shadow-2xl: 0px 1.5px 9.5px 0.5px hsl(0 0% 0% / 0.07);
--shadow-color: hsl(0 0% 0%);
--shadow-opacity: 0.03;
--shadow-blur: 9.5px;
--shadow-spread: 0.5px;
--shadow-offset-x: 0px;
--shadow-offset-y: 1.5px;
--letter-spacing: 0em;
--spacing: 0.25rem;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
letter-spacing: var(--tracking-normal);
}
}

View file

@ -1,13 +1,18 @@
import '@/styles/globals.css';
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getTranslations, setRequestLocale } from 'next-intl/server';
import { ClerkProvider } from '@clerk/nextjs';
import { getClerkLocale } from '@/i18n/locale';
import { ThemeProvider, ReactQueryProvider } from '@/components/layout';
import { cn } from '@/lib/utils';
import { Toaster } from '@/components/ui';
import {
ThemeProvider,
AuthProvider,
ReactQueryProvider,
} from '@/components/layout';
import './globals.css';
export async function generateMetadata(): Promise<Metadata> {
const locale = await getLocale();
@ -35,6 +40,11 @@ export async function generateMetadata(): Promise<Metadata> {
};
}
const geist = Geist({ subsets: ['latin'] });
const fontSans = Geist({ variable: '--font-sans', subsets: ['latin'] });
const fontMono = Geist_Mono({ variable: '--font-mono', subsets: ['latin'] });
export default async function RootLayout({
children,
}: {
@ -44,19 +54,23 @@ export default async function RootLayout({
setRequestLocale(locale);
return (
<html lang={locale} suppressHydrationWarning>
<html
lang={locale}
className={cn(geist.className)}
suppressHydrationWarning
>
<body
className="bg-background text-foreground antialiased"
className={`${fontSans.variable} ${fontMono.variable} font-sans antialiased`}
suppressHydrationWarning
>
<NextIntlClientProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ClerkProvider localization={getClerkLocale(locale)}>
<AuthProvider localization={getClerkLocale(locale)}>
<ReactQueryProvider>
{children}
<Toaster />
</ReactQueryProvider>
</ClerkProvider>
</AuthProvider>
</ThemeProvider>
</NextIntlClientProvider>
</body>

View file

@ -1,17 +0,0 @@
'use client';
import { useTheme } from 'next-themes';
import { CreateOrganization } from '@clerk/nextjs';
import { dark } from '@clerk/themes';
export const CreateOrg = () => {
const { resolvedTheme } = useTheme();
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
return (
<CreateOrganization
appearance={{ baseTheme }}
afterCreateOrganizationUrl="/dashboard"
/>
);
};

View file

@ -1,7 +0,0 @@
import { CreateOrg } from './CreateOrg';
export const dynamic = 'force-dynamic';
export default async function Page() {
return <CreateOrg />;
}

View file

@ -1,20 +0,0 @@
import { redirect } from 'next/navigation';
import { auth as clerkAuth } from '@clerk/nextjs/server';
export default async function OnboardingLayout({
children,
}: {
children: React.ReactNode;
}) {
const auth = await clerkAuth();
if (!auth.userId) {
redirect('/');
}
return (
<div className="flex min-h-screen items-center justify-center">
{children}
</div>
);
}

View file

@ -1,18 +0,0 @@
'use client';
import { useTheme } from 'next-themes';
import { dark } from '@clerk/themes';
import { OrganizationList } from '@clerk/nextjs';
export const SelectOrg = () => {
const { resolvedTheme } = useTheme();
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
return (
<OrganizationList
appearance={{ baseTheme }}
afterSelectOrganizationUrl="/dashboard"
afterCreateOrganizationUrl="/dashboard"
/>
);
};

View file

@ -1,7 +0,0 @@
import { SelectOrg } from './SelectOrg';
export const dynamic = 'force-dynamic';
export default async function Page() {
return <SelectOrg />;
}

View file

@ -1,7 +1,5 @@
import { redirect } from 'next/navigation';
import { currentUser } from '@clerk/nextjs/server';
import { Home } from './Home';
export default async function Page() {
const user = await currentUser();
redirect(user ? '/dashboard' : '/sign-in');
return <Home />;
}

View file

@ -3,15 +3,11 @@
import { useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useTheme } from 'next-themes';
import { dark } from '@clerk/themes';
import { SignIn as ClerkSignIn } from '@clerk/nextjs';
import { Logo } from '@/components/layout';
export const SignIn = () => {
const { resolvedTheme } = useTheme();
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
const searchParams = useSearchParams();
const forceRedirectUrl = useMemo(() => {
@ -24,10 +20,7 @@ export const SignIn = () => {
<Link href="/">
<Logo />
</Link>
<ClerkSignIn
appearance={{ baseTheme }}
forceRedirectUrl={forceRedirectUrl}
/>
<ClerkSignIn forceRedirectUrl={forceRedirectUrl} />
</div>
);
};

View file

@ -3,15 +3,11 @@
import { useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useTheme } from 'next-themes';
import { dark } from '@clerk/themes';
import { SignUp as ClerkSignUp } from '@clerk/nextjs';
import { Logo } from '@/components/layout';
export const SignUp = () => {
const { resolvedTheme } = useTheme();
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
const searchParams = useSearchParams();
const forceRedirectUrl = useMemo(() => {
@ -24,10 +20,7 @@ export const SignUp = () => {
<Link href="/">
<Logo />
</Link>
<ClerkSignUp
appearance={{ baseTheme }}
forceRedirectUrl={forceRedirectUrl}
/>
<ClerkSignUp forceRedirectUrl={forceRedirectUrl} />
</div>
);
};

View file

@ -0,0 +1,97 @@
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { useState } from 'react';
import { useOrganization } from '@clerk/nextjs';
import { useQuery } from '@tanstack/react-query';
import { ArrowRightIcon } from 'lucide-react';
import type { AuditLogType } from '@/db/schema';
import { getAuditLogs } from '@/actions/auditLogs';
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from '@/components/ui';
import { Button as EnhancedButton } from '@/components/ui/ecosystem';
import { AuditLogDetails, AuditLogEntry } from '@/components/audit-logs';
export function AuditLogCard() {
const [selectedLog, setSelectedLog] = useState<AuditLogType | null>(null);
const { organization } = useOrganization();
const path = usePathname();
const { data: logs = [], isLoading } = useQuery({
queryKey: ['auditLogs', organization?.id, 5],
queryFn: () => getAuditLogs({ orgId: organization?.id, limit: 5 }),
enabled: !!organization?.id,
});
return (
<>
<Card>
<CardHeader className="relative">
<CardTitle>Recent Activity</CardTitle>
<CardDescription>Organization audit logs and changes</CardDescription>
{path !== '/audit-logs' && (
<EnhancedButton
variant="ghost"
size="sm"
effect="expandIcon"
icon={ArrowRightIcon}
iconPlacement="right"
className="absolute top-0 right-6"
asChild
>
<Link
href="/audit-logs"
className="text-sm text-primary hover:underline"
>
See all logs
</Link>
</EnhancedButton>
)}
</CardHeader>
<CardContent>
<div className="space-y-1">
{isLoading ? (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
) : logs.length > 0 ? (
logs.map((log: AuditLogType) => (
<AuditLogEntry
key={log.id}
log={log}
onClick={(log: AuditLogType) => setSelectedLog(log)}
/>
))
) : (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">
No recent activity
</p>
</div>
)}
</div>
</CardContent>
</Card>
<Drawer open={!!selectedLog} onClose={() => setSelectedLog(null)}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Activity Details</DrawerTitle>
</DrawerHeader>
{selectedLog && <AuditLogDetails log={selectedLog} />}
</DrawerContent>
</Drawer>
</>
);
}

View file

@ -0,0 +1,80 @@
'use client';
import { ArrowRight, Calendar, Clock, User } from 'lucide-react';
import Link from 'next/link';
import type { AuditLogType } from '@/db/schema';
type AuditLogDetailsProps = {
log: AuditLogType;
};
const formatValue = (value: unknown) => {
if (value === null || value === undefined) {
return <span className="italic text-muted-foreground">None</span>;
}
if (Array.isArray(value)) {
return (
<ul className="list-disc pl-5">
{value.map((item, index) => (
<li key={index}>{String(item)}</li>
))}
</ul>
);
}
if (typeof value === 'object') {
return (
<div className="space-y-1">
{Object.entries(value).map(([key, val]) => (
<div key={key} className="grid grid-cols-2 gap-2">
<span className="text-sm font-medium">{key}:</span>
<span className="text-sm">{String(val)}</span>
</div>
))}
</div>
);
}
return String(value);
};
export const AuditLogDetails = ({ log }: AuditLogDetailsProps) => (
<div className="space-y-6">
<div className="space-y-4">
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<Calendar className="size-4" />
<span>{log.createdAt.toLocaleDateString()}</span>
<Clock className="ml-2 size-4" />
<span>{log.createdAt.toLocaleTimeString()}</span>
</div>
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<User className="size-4" />
<span>{log.userId}</span>
</div>
{log.targetId && (
<Link
href={log.targetId}
className="mt-2 inline-flex items-center text-sm font-medium text-primary hover:underline"
>
View in settings
<ArrowRight className="ml-1 size-4" />
</Link>
)}
</div>
<div className="rounded-md border p-4">
<h3 className="mb-4 text-sm font-medium">Changes</h3>
<div className="space-y-4">
<div className="space-y-2">
<h4 className="text-xs font-medium uppercase text-muted-foreground">
New Value
</h4>
<div className="rounded-md bg-muted p-3">
{formatValue(log.newValue)}
</div>
</div>
</div>
</div>
</div>
);

View file

@ -0,0 +1,46 @@
import { Settings, Sliders, Users } from 'lucide-react';
import { formatDistance } from 'date-fns';
import { type AuditLogType, AuditLogTargetType } from '@/db/schema';
import { cn } from '@/lib/utils';
type AuditLogEntryProps = {
log: AuditLogType;
onClick: (log: AuditLogType) => void;
};
const getIconByType = (type: AuditLogTargetType) => {
switch (type) {
case AuditLogTargetType.PROVIDER_WHITELIST:
return <Settings className="size-4 text-purple-500" />;
case AuditLogTargetType.DEFAULT_PARAMETERS:
return <Sliders className="size-4 text-green-500" />;
case AuditLogTargetType.MEMBER_CHANGE:
return <Users className="size-4 text-amber-500" />;
default:
return <Settings className="size-4 text-gray-500" />;
}
};
export const AuditLogEntry = ({ log, onClick }: AuditLogEntryProps) => (
<button
onClick={() => onClick(log)}
className={cn(
'w-full rounded-md p-3 text-left transition-colors',
'hover:bg-muted focus:bg-muted focus:outline-none',
)}
>
<div className="flex items-start gap-3">
<div className="mt-0.5 shrink-0">{getIconByType(log.targetType)}</div>
<div className="flex-1 space-y-1">
<p className="text-sm text-foreground">{log.description}</p>
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">{log.userId}</p>
<p className="text-xs text-muted-foreground">
{formatDistance(log.createdAt, new Date(), { addSuffix: true })}
</p>
</div>
</div>
</div>
</button>
);

View file

@ -0,0 +1,3 @@
export { AuditLogCard } from './AuditLogCard';
export { AuditLogDetails } from './AuditLogDetails';
export { AuditLogEntry } from './AuditLogEntry';

View file

@ -1,80 +0,0 @@
'use client';
import Link from 'next/link';
import React, { useState } from 'react';
import { useOrganization } from '@clerk/nextjs';
import { useQuery } from '@tanstack/react-query';
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from '@/components/ui/drawer';
import { AuditLogDetails } from './AuditLogDetails';
import { AuditLogEntry } from './AuditLogEntry';
import type { AuditLogType } from '@/db/schema';
import { getAuditLogs } from '@/actions/auditLogs';
export function AuditLogCard() {
const [selectedLog, setSelectedLog] = useState<AuditLogType | null>(null);
const { organization } = useOrganization();
const { data: logs = [], isLoading } = useQuery({
queryKey: ['auditLogs', organization?.id, 5],
queryFn: async () =>
await getAuditLogs({ orgId: organization?.id, limit: 5 }),
enabled: !!organization?.id,
});
const handleLogClick = (log: AuditLogType) => setSelectedLog(log);
const handleCloseDrawer = () => setSelectedLog(null);
return (
<div className="mb-6 w-2/3 rounded-md bg-card p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold">Recent Activity</h3>
<p className="text-sm text-muted-foreground">
Organization audit logs and changes
</p>
</div>
{/* Log entries */}
<div className="space-y-1">
{isLoading ? (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
) : logs.length > 0 ? (
logs.map((log: AuditLogType) => (
<AuditLogEntry key={log.id} log={log} onClick={handleLogClick} />
))
) : (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">No recent activity</p>
</div>
)}
</div>
{/* "See all logs" link */}
<div className="mt-4 text-right">
<Link
href="/dashboard/audit-logs"
className="text-sm text-primary hover:underline"
>
See all logs
</Link>
</div>
<Drawer open={!!selectedLog} onClose={handleCloseDrawer}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Activity Details</DrawerTitle>
</DrawerHeader>
{selectedLog && <AuditLogDetails log={selectedLog} />}
</DrawerContent>
</Drawer>
</div>
);
}

View file

@ -1,102 +0,0 @@
'use client';
import { ArrowRight, Calendar, Clock, User } from 'lucide-react';
import Link from 'next/link';
import React from 'react';
import { AuditLogTargetType, type AuditLogType } from '@/db/schema';
import { getBaseUrl } from '@/lib/metadata';
type AuditLogDetailsProps = {
log: AuditLogType;
};
const getSettingsUrl = (type: AuditLogTargetType): string => {
const baseUrl = `${getBaseUrl()}/dashboard/org`;
switch (type) {
case AuditLogTargetType.MEMBER_CHANGE:
return `${baseUrl}/organization-members`;
case AuditLogTargetType.PROVIDER_WHITELIST:
return `${baseUrl}/provider-whitelist`;
case AuditLogTargetType.DEFAULT_PARAMETERS:
return `${baseUrl}/default-parameters`;
default:
return baseUrl;
}
};
const formatValue = (value: unknown): React.ReactNode => {
if (value === null || value === undefined) {
return <span className="italic text-muted-foreground">None</span>;
}
if (Array.isArray(value)) {
return (
<ul className="list-disc pl-5">
{value.map((item, index) => (
<li key={index}>{String(item)}</li>
))}
</ul>
);
}
if (typeof value === 'object') {
return (
<div className="space-y-1">
{Object.entries(value).map(([key, val]) => (
<div key={key} className="grid grid-cols-2 gap-2">
<span className="text-sm font-medium">{key}:</span>
<span className="text-sm">{String(val)}</span>
</div>
))}
</div>
);
}
return String(value);
};
export function AuditLogDetails({ log }: AuditLogDetailsProps) {
return (
<div className="space-y-6">
{/* Header information */}
<div className="space-y-4">
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<Calendar className="size-4" />
<span>{log.createdAt.toLocaleDateString()}</span>
<Clock className="ml-2 size-4" />
<span>{log.createdAt.toLocaleTimeString()}</span>
</div>
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<User className="size-4" />
<span>{log.userId}</span>
</div>
<Link
href={getSettingsUrl(log.targetType)}
className="mt-2 inline-flex items-center text-sm font-medium text-primary hover:underline"
>
View in settings
<ArrowRight className="ml-1 size-4" />
</Link>
</div>
{/* Change details */}
<div className="rounded-md border p-4">
<h3 className="mb-4 text-sm font-medium">Changes</h3>
<div className="space-y-4">
<div className="space-y-2">
<h4 className="text-xs font-medium uppercase text-muted-foreground">
New Value
</h4>
<div className="rounded-md bg-muted p-3">
{formatValue(log.newValue)}
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,52 +0,0 @@
'use client';
import { Settings, Sliders, Users } from 'lucide-react';
import React from 'react';
import { cn } from '@/lib/utils';
import { formatDistance } from 'date-fns';
import { type AuditLogType, AuditLogTargetType } from '@/db/schema';
type AuditLogEntryProps = {
log: AuditLogType;
onClick: (log: AuditLogType) => void;
};
const getIconByType = (type: AuditLogTargetType) => {
switch (type) {
case AuditLogTargetType.PROVIDER_WHITELIST:
return <Settings className="size-4 text-purple-500" />;
case AuditLogTargetType.DEFAULT_PARAMETERS:
return <Sliders className="size-4 text-green-500" />;
case AuditLogTargetType.MEMBER_CHANGE:
return <Users className="size-4 text-amber-500" />;
default:
return <Settings className="size-4 text-gray-500" />;
}
};
export function AuditLogEntry({ log, onClick }: AuditLogEntryProps) {
return (
<button
onClick={() => onClick(log)}
className={cn(
'w-full rounded-md p-3 text-left transition-colors',
'hover:bg-muted focus:bg-muted focus:outline-none',
)}
>
<div className="flex items-start gap-3">
<div className="mt-0.5 shrink-0">{getIconByType(log.targetType)}</div>
<div className="flex-1 space-y-1">
<p className="text-sm text-foreground">{log.description}</p>
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">{log.userId}</p>
<p className="text-xs text-muted-foreground">
{formatDistance(log.createdAt, new Date(), { addSuffix: true })}
</p>
</div>
</div>
</div>
</button>
);
}

View file

@ -1,65 +0,0 @@
'use client';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useTheme } from 'next-themes';
import { OrganizationSwitcher, UserButton } from '@clerk/nextjs';
import { dark } from '@clerk/themes';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui';
import { LocaleSwitcher } from '@/components/layout/LocaleSwitcher';
import { ThemeSwitcher } from '@/components/layout/ThemeSwitcher';
import { Logo } from '@/components/layout/Logo';
export const DashboardHeader = () => {
const { resolvedTheme } = useTheme();
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
const router = useRouter();
return (
<div className="p-4 border-b">
<div className="flex justify-between max-w-screen-lg mx-auto">
<div className="flex items-center gap-2">
<Link href="/dashboard">
<Logo />
</Link>
<OrganizationSwitcher
appearance={{ baseTheme }}
organizationProfileMode="navigation"
organizationProfileUrl="/dashboard/org"
afterCreateOrganizationUrl="/dashboard"
createOrganizationUrl="/onboarding/create-org"
/>
<Tabs
onValueChange={(value) => router.push(value)}
defaultValue="/dashboard"
>
<TabsList>
<TabsTrigger value="/dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="/dashboard/analytics">Analytics</TabsTrigger>
<TabsTrigger value="/dashboard/audit-logs">
Audit Logs
</TabsTrigger>
<TabsTrigger value="/dashboard/org">Settings</TabsTrigger>
</TabsList>
</Tabs>
</div>
<ul className="flex items-center gap-2 [&_li[data-fade]:hover]:opacity-100 [&_li[data-fade]]:opacity-60">
<li data-fade>
<LocaleSwitcher />
</li>
<li data-fade>
<ThemeSwitcher />
</li>
<li>
<UserButton
appearance={{ baseTheme }}
userProfileMode="navigation"
userProfileUrl="/dashboard/user"
/>
</li>
</ul>
</div>
</div>
);
};

View file

@ -1,105 +0,0 @@
'use client';
import React, { useState } from 'react';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { useAuth } from '@clerk/nextjs';
import { useQuery } from '@tanstack/react-query';
import { TelemetryEventName } from '@roo-code/types';
import { type TimePeriod, timePeriods } from '@/schemas';
import { getUsage } from '@/actions/analytics';
import { Button } from '@/components/ui';
export const UsageAnalyticsCard = () => {
const t = useTranslations('DashboardIndex');
const { orgId } = useAuth();
const [timePeriod, setTimePeriod] = useState<TimePeriod>(7);
const path = usePathname();
const usage = useQuery({
queryKey: ['usage', orgId, timePeriod],
queryFn: () => getUsage({ orgId, timePeriod }),
});
return (
<div className="mb-6 rounded-md bg-card p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold">{t('analytics_title')}</h3>
<p className="text-sm text-muted-foreground">
{t('analytics_description')}
</p>
</div>
<div className="mb-4 flex space-x-2">
{timePeriods.map((period) => (
<Button
key={period}
variant={period === timePeriod ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod(period)}
>
{t(`analytics_period_${period}_days`)}
</Button>
))}
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_active_developers')}
</div>
<div className="mt-1 text-2xl font-semibold">
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.userCount ?? '-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_tasks_started')}
</div>
<div className="mt-1 text-2xl font-semibold">
{usage.data?.[TelemetryEventName.TASK_CREATED]?.eventCount ?? '-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_tasks_completed')}
</div>
<div className="mt-1 text-2xl font-semibold">
{usage.data?.[TelemetryEventName.TASK_COMPLETED]?.eventCount ?? '-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_tokens_consumed')}
</div>
<div className="mt-1 text-2xl font-semibold">
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.inputTokens ??
'-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_llm_costs')}
</div>
<div className="mt-1 text-2xl font-semibold">
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.cost?.toFixed(
2,
) ?? '-'}
</div>
</div>
</div>
{path !== '/dashboard/analytics' && (
<div className="mt-4 text-right">
<Link
href="/dashboard/analytics"
className="text-sm text-primary hover:underline"
>
{t('analytics_view_details')}
</Link>
</div>
)}
</div>
);
};

View file

@ -1,3 +0,0 @@
export { TitleBar } from './TitleBar';
export { UsageAnalyticsCard } from './UsageAnalyticsCard';
export { AuditLogCard } from './AuditLogCard';

View file

@ -0,0 +1,30 @@
'use client';
import { ClerkProvider } from '@clerk/nextjs';
import { dark } from '@clerk/themes';
import { getClerkLocale } from '@/i18n/locale';
import { useTheme } from 'next-themes';
type Localization = ReturnType<typeof getClerkLocale>;
export const AuthProvider = ({
localization,
children,
}: {
localization: Localization;
children: React.ReactNode;
}) => {
const { resolvedTheme } = useTheme();
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
return (
<ClerkProvider
appearance={{ baseTheme }}
localization={localization}
afterSignOutUrl={'/sign-in'}
>
{children}
</ClerkProvider>
);
};

View file

@ -32,7 +32,7 @@ export function DataTable<TData, TValue>({
});
return (
<div className="rounded-md border">
<div className="bg-card border shadow-md rounded-md">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (

View file

@ -1,49 +0,0 @@
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { Section } from '@/components/layout';
export const Footer = () => {
const t = useTranslations('Footer');
return (
<Section>
<div className="flex flex-col items-center text-center">
<ul className="mt-4 flex flex-row gap-x-5 text-muted-foreground [&_svg:hover]:text-primary [&_svg:hover]:opacity-100 [&_svg]:size-5 [&_svg]:fill-current [&_svg]:opacity-60">
<li>
<Link href="/">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
</Link>
</li>
<li>
<Link href="/">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M23.954 4.569a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.691 8.094 4.066 6.13 1.64 3.161a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.061a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.937 4.937 0 004.604 3.417 9.868 9.868 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63a9.936 9.936 0 002.46-2.548l-.047-.02z" />
</svg>
</Link>
</li>
<li>
<Link href="/">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
</Link>
</li>
</ul>
<div className="mt-6 flex w-full items-center justify-between gap-y-2 border-t pt-3 text-sm text-muted-foreground max-md:flex-col">
<div>© Copyright {new Date().getFullYear()} Roo Code.</div>
<ul className="flex gap-x-4 font-medium [&_a:hover]:opacity-100 [&_a]:opacity-60">
<li>
<Link href="/sign-up">{t('terms_of_service')}</Link>
</li>
<li>
<Link href="/sign-up">{t('privacy_policy')}</Link>
</li>
</ul>
</div>
</div>
</Section>
);
};

View file

@ -1,22 +1,71 @@
import Image from 'next/image';
'use client';
export const Logo = () => (
<div className="flex items-center text-xl font-semibold">
<Image
src="/logo.svg"
alt="Roo Code Logo"
width={120}
height={26}
className="mr-2 dark:hidden"
style={{ height: 'auto' }}
/>
<Image
src="/logo-dark.svg"
alt="Roo Code Logo"
width={120}
height={26}
className="mr-2 hidden dark:block"
style={{ height: 'auto' }}
/>
</div>
);
import { SVGProps, useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useTheme } from 'next-themes';
import { useHover } from 'react-use';
import { cn } from '@/lib/utils';
type LogoProps = Omit<
SVGProps<SVGSVGElement>,
'xmlns' | 'viewBox' | 'onClick' | 'fill'
>;
export const Logo = ({
width = 50,
height = 32,
className,
...props
}: LogoProps) => {
const router = useRouter();
const { resolvedTheme } = useTheme();
const [fill, setFill] = useState('#000');
useEffect(() => {
setFill(resolvedTheme === 'dark' ? '#fff' : '#000');
}, [resolvedTheme]);
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox="90 12 100 64"
onClick={() => router.push('/')}
className={cn('logo cursor-pointer', className)}
{...props}
>
<path
d="M171.633,15.8336l-1.7284,6.2499c-.0915.3309-.4369.5221-.7659.4239l-28.9937-8.6507c-.1928-.0575-.4016-.0167-.5586.1092l-28.7143,23.0269c-.0838.0672-.1839.1112-.2901.1276l-17.0849,2.6329c-.3163.0488-.5419.3327-.5178.6519l.0742.9817c.0237.3136.2809.5583.5953.5664l19.8448.513.2263.0063,14.6634-7.8328c.2053-.1097.455-.0936.6445.0415l10.3884,7.4053c.1629.1161.2589.3045.2571.5045l-.0876,9.826c-.0011.1272.0373.2515.11.3559l14.6133,20.9682c.1146.1644.3024.2624.5028.2624h4.626c.4615,0,.7574-.4908.542-.8989l-10.4155-19.7312c-.1019-.193-.0934-.4255.0221-.6106l5.4305-8.6994c.0591-.0947.143-.1715.2425-.222l19.415-9.8522c.1973-.1001.4332-.0861.6172.0366l5.5481,3.6981c.1007.0671.2189.1029.3399.1029h5.0407c.4881,0,.7804-.5429.5116-.9503l-13.9967-21.2171c-.2898-.4393-.962-.3331-1.1022.1741Z"
fill={fill}
strokeWidth="0"
/>
</svg>
);
};
export const HoppingLogo = (props: LogoProps) => {
const ref = useRef<SVGSVGElement>(null);
const logo = <Logo ref={ref} {...props} />;
const [hoverable, hovered] = useHover(logo);
useEffect(() => {
const element = ref.current;
const isHopping =
element !== null && element.classList.contains('animate-hop');
if (hovered && element && !isHopping) {
element.classList.add('animate-hop');
} else if (element && isHopping) {
const onAnimationEnd = () => {
element.classList.remove('animate-hop');
element.removeEventListener('animationiteration', onAnimationEnd);
};
element.addEventListener('animationiteration', onAnimationEnd);
}
}, [hovered]);
return hoverable;
};

View file

@ -0,0 +1,41 @@
import Link from 'next/link';
import { OrganizationSwitcher, UserButton } from '@clerk/nextjs';
import {
LocaleSwitcher,
ThemeSwitcher,
HoppingLogo,
} from '@/components/layout';
import { Section } from './Section';
type NavbarHeaderProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>;
export const NavbarHeader = (props: NavbarHeaderProps) => (
<Section {...props}>
<div className="flex justify-between items-center h-full">
<div className="flex items-center gap-2">
<Link href="/dashboard">
<HoppingLogo />
</Link>
<OrganizationSwitcher
organizationProfileMode="navigation"
organizationProfileUrl="/org"
afterCreateOrganizationUrl="/dashboard"
hidePersonal
/>
</div>
<ul className="flex items-center gap-2 [&_li[data-fade]:hover]:opacity-100 [&_li[data-fade]]:opacity-60">
<li data-fade>
<LocaleSwitcher />
</li>
<li data-fade>
<ThemeSwitcher />
</li>
<li>
<UserButton />
</li>
</ul>
</div>
</Section>
);

View file

@ -0,0 +1,62 @@
'use client';
import { useEffect, useState } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/ecosystem';
import { Section } from './Section';
type NavbarMenuProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>;
const tabValues = [
'/dashboard',
'/usage',
'/audit-logs',
'/org',
'/hidden',
] as const;
type TabValue = (typeof tabValues)[number];
const isTabValue = (value: string): value is TabValue =>
tabValues.includes(value as TabValue);
export const NavbarMenu = (props: NavbarMenuProps) => {
const router = useRouter();
const pathname = usePathname();
const [tabValue, setTabValue] = useState<TabValue | undefined>(undefined);
useEffect(() => {
setTabValue(isTabValue(pathname) ? pathname : '/hidden');
}, [pathname]);
return (
<Section {...props}>
<div className="flex justify-between items-center h-full">
<div className="flex items-center gap-2">
<Tabs
onValueChange={(value) => {
if (isTabValue(value)) {
setTabValue(value);
if (value !== '/hidden') {
router.push(value);
}
}
}}
value={tabValue}
>
<TabsList>
<TabsTrigger value="/dashboard">Dashboard</TabsTrigger>
<TabsTrigger value="/usage">Usage</TabsTrigger>
<TabsTrigger value="/audit-logs">Audit Logs</TabsTrigger>
<TabsTrigger value="/org">Organization</TabsTrigger>
<TabsTrigger value="/hidden" className="hidden" />
</TabsList>
</Tabs>
</div>
</div>
</Section>
);
};

View file

@ -1,9 +1,21 @@
import { cn } from '@/lib/utils';
type SectionProps = React.HTMLAttributes<HTMLDivElement>;
type SectionProps = React.HTMLAttributes<HTMLDivElement> & {
divider?: boolean;
};
export const Section = ({ className, children, ...rest }: SectionProps) => (
<div className={cn('px-4 py-16', className)} {...rest}>
<div className="mx-auto max-w-screen-lg">{children}</div>
export const Section = ({
className,
divider = true,
...rest
}: SectionProps) => (
<div className={cn({ 'border-b border-dashed': divider })}>
<div
className={cn(
'max-w-screen-lg border-l border-r border-dashed mx-auto px-8',
className,
)}
{...rest}
/>
</div>
);

View file

@ -4,7 +4,7 @@ type TitleBarProps = {
};
export const TitleBar = ({ title, description }: TitleBarProps) => (
<div className="mb-8">
<div>
<div className="text-2xl font-semibold">{title}</div>
{description && (
<div className="text-sm font-medium text-muted-foreground">

View file

@ -1,7 +1,10 @@
export { Footer } from './Footer';
export { Section } from './Section';
export { LocaleSwitcher } from './LocaleSwitcher';
export { ThemeProvider } from './ThemeProvider';
export { ThemeSwitcher } from './ThemeSwitcher';
export { Logo, HoppingLogo } from './Logo';
export { NavbarHeader } from './NavbarHeader';
export { NavbarMenu } from './NavbarMenu';
export { ThemeProvider } from './ThemeProvider';
export { AuthProvider } from './AuthProvider';
export { ReactQueryProvider } from './ReactQueryProvider';
export { Logo } from './Logo';

View file

@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
{
variants: {
variant: {

View file

@ -8,6 +8,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
data-slot="card"
className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
'dark:border-0',
className,
)}
{...props}

View file

@ -0,0 +1,121 @@
import * as React from 'react';
import { Slot, Slottable } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
effect: {
expandIcon: 'group gap-0 relative',
ringHover:
'transition-all duration-300 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2',
shine:
'before:animate-shine relative overflow-hidden before:absolute before:inset-0 before:rounded-[inherit] before:bg-[linear-gradient(45deg,transparent_25%,rgba(255,255,255,0.5)_50%,transparent_75%,transparent_100%)] before:bg-[length:250%_250%,100%_100%] before:bg-no-repeat background-position_0s_ease',
shineHover:
'relative overflow-hidden before:absolute before:inset-0 before:rounded-[inherit] before:bg-[linear-gradient(45deg,transparent_25%,rgba(255,255,255,0.5)_50%,transparent_75%,transparent_100%)] before:bg-[length:250%_250%,100%_100%] before:bg-[position:200%_0,0_0] before:bg-no-repeat before:transition-[background-position_0s_ease] hover:before:bg-[position:-100%_0,0_0] before:duration-1000',
gooeyRight:
'relative z-0 overflow-hidden transition-all duration-500 before:absolute before:inset-0 before:-z-10 before:translate-x-[150%] before:translate-y-[150%] before:scale-[2.5] before:rounded-[100%] before:bg-gradient-to-r from-white/40 before:transition-transform before:duration-1000 hover:before:translate-x-[0%] hover:before:translate-y-[0%]',
gooeyLeft:
'relative z-0 overflow-hidden transition-all duration-500 after:absolute after:inset-0 after:-z-10 after:translate-x-[-150%] after:translate-y-[150%] after:scale-[2.5] after:rounded-[100%] after:bg-gradient-to-l from-white/40 after:transition-transform after:duration-1000 hover:after:translate-x-[0%] hover:after:translate-y-[0%]',
underline:
'relative !no-underline after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300',
hoverUnderline:
'relative !no-underline after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300',
gradientSlideShow:
'bg-[size:400%] bg-[linear-gradient(-45deg,var(--gradient-lime),var(--gradient-ocean),var(--gradient-wine),var(--gradient-rust))] animate-gradient-flow',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
interface IconProps {
icon: React.ElementType;
iconPlacement: 'left' | 'right';
}
interface IconRefProps {
icon?: never;
iconPlacement?: undefined;
}
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export type ButtonIconProps = IconProps | IconRefProps;
const Button = React.forwardRef<
HTMLButtonElement,
ButtonProps & ButtonIconProps
>(
(
{
className,
variant,
effect,
size,
icon: Icon,
iconPlacement,
asChild = false,
...props
},
ref,
) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, effect, size, className }))}
ref={ref}
{...props}
>
{Icon &&
iconPlacement === 'left' &&
(effect === 'expandIcon' ? (
<div className="w-0 translate-x-[0%] pr-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-100 group-hover:pr-2 group-hover:opacity-100">
<Icon />
</div>
) : (
<Icon />
))}
<Slottable>{props.children}</Slottable>
{Icon &&
iconPlacement === 'right' &&
(effect === 'expandIcon' ? (
<div className="w-0 translate-x-[100%] pl-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-0 group-hover:pl-2 group-hover:opacity-100">
<Icon />
</div>
) : (
<Icon />
))}
</Comp>
);
},
);
Button.displayName = 'Button';
export { Button, buttonVariants };

View file

@ -0,0 +1,2 @@
export * from './button';
export * from './tabs';

View file

@ -0,0 +1,124 @@
'use client';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '@/lib/utils';
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => {
const [indicatorStyle, setIndicatorStyle] = useState({
left: 0,
top: 0,
width: 0,
height: 0,
});
const tabsListRef = useRef<HTMLDivElement | null>(null);
const updateIndicator = React.useCallback(() => {
if (!tabsListRef.current) {
return;
}
const activeTab = tabsListRef.current.querySelector<HTMLElement>(
'[data-state="active"]',
);
if (!activeTab) {
return;
}
const activeRect = activeTab.getBoundingClientRect();
const tabsRect = tabsListRef.current.getBoundingClientRect();
requestAnimationFrame(() => {
setIndicatorStyle({
left: activeRect.left - tabsRect.left,
top: activeRect.top - tabsRect.top,
width: activeRect.width,
height: activeRect.height,
});
});
}, []);
useEffect(() => {
const timeoutId = setTimeout(updateIndicator, 0);
window.addEventListener('resize', updateIndicator);
const observer = new MutationObserver(updateIndicator);
if (tabsListRef.current) {
observer.observe(tabsListRef.current, {
attributes: true,
childList: true,
subtree: true,
});
}
return () => {
clearTimeout(timeoutId);
window.removeEventListener('resize', updateIndicator);
observer.disconnect();
};
}, [updateIndicator]);
return (
<div className="relative overflow-hidden" ref={tabsListRef}>
<TabsPrimitive.List
ref={ref}
className={cn(
'relative inline-flex items-center justify-center rounded-sm',
className,
)}
{...props}
/>
<div
className={cn(
'absolute rounded transition-all duration-300 ease-in-out pointer-events-none',
'bg-primary',
)}
style={indicatorStyle}
/>
</div>
);
});
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 z-10',
'transition-colors duration-500 data-[state=active]:text-primary-foreground cursor-pointer',
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsContent, TabsList, TabsTrigger };

View file

@ -0,0 +1,13 @@
import { cn } from '@/lib/utils';
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="skeleton"
className={cn('bg-accent animate-pulse rounded-md', className)}
{...props}
/>
);
}
export { Skeleton };

View file

@ -1,124 +1,66 @@
'use client';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '@/lib/utils';
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => {
const [indicatorStyle, setIndicatorStyle] = useState({
left: 0,
top: 0,
width: 0,
height: 0,
});
const tabsListRef = useRef<HTMLDivElement | null>(null);
const updateIndicator = React.useCallback(() => {
if (!tabsListRef.current) {
return;
}
const activeTab = tabsListRef.current.querySelector<HTMLElement>(
'[data-state="active"]',
);
if (!activeTab) {
return;
}
const activeRect = activeTab.getBoundingClientRect();
const tabsRect = tabsListRef.current.getBoundingClientRect();
requestAnimationFrame(() => {
setIndicatorStyle({
left: activeRect.left - tabsRect.left,
top: activeRect.top - tabsRect.top,
width: activeRect.width,
height: activeRect.height,
});
});
}, []);
useEffect(() => {
const timeoutId = setTimeout(updateIndicator, 0);
window.addEventListener('resize', updateIndicator);
const observer = new MutationObserver(updateIndicator);
if (tabsListRef.current) {
observer.observe(tabsListRef.current, {
attributes: true,
childList: true,
subtree: true,
});
}
return () => {
clearTimeout(timeoutId);
window.removeEventListener('resize', updateIndicator);
observer.disconnect();
};
}, [updateIndicator]);
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<div className="relative" ref={tabsListRef}>
<TabsPrimitive.List
ref={ref}
className={cn(
'relative inline-flex items-center justify-center rounded-sm bg-primary p-0.5 text-muted-foreground',
className,
)}
{...props}
/>
<div
className={cn(
'absolute rounded-sm transition-all duration-300 ease-in-out pointer-events-none',
'bg-accent/5',
)}
style={indicatorStyle}
/>
</div>
<TabsPrimitive.Root
data-slot="tabs"
className={cn('flex flex-col gap-2', className)}
{...props}
/>
);
});
TabsList.displayName = TabsPrimitive.List.displayName;
}
const TabsTrigger = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 z-10',
'data-[state=active]:text-accent data-[state=active]:font-medium cursor-pointer',
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
className,
)}
{...props}
/>
);
}
const TabsContent = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
export { Tabs, TabsContent, TabsList, TabsTrigger };
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn('flex-1 outline-none', className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };

View file

@ -0,0 +1,128 @@
'use client';
import React, { useState } from 'react';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { useAuth } from '@clerk/nextjs';
import { useQuery } from '@tanstack/react-query';
import { ArrowRightIcon } from 'lucide-react';
import { TelemetryEventName } from '@roo-code/types';
import { type TimePeriod, timePeriods } from '@/schemas';
import { getUsage } from '@/actions/analytics';
import {
Button,
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from '@/components/ui';
import { Button as EnhancedButton } from '@/components/ui/ecosystem';
export const UsageCard = () => {
const t = useTranslations('DashboardIndex');
const { orgId } = useAuth();
const [timePeriod, setTimePeriod] = useState<TimePeriod>(7);
const path = usePathname();
const usage = useQuery({
queryKey: ['usage', orgId, timePeriod],
queryFn: () => getUsage({ orgId, timePeriod }),
});
return (
<Card>
<CardHeader className="relative">
<CardTitle>{t('analytics_title')}</CardTitle>
<CardDescription>{t('analytics_description')}</CardDescription>
{path !== '/usage' && (
<EnhancedButton
variant="ghost"
size="sm"
effect="expandIcon"
icon={ArrowRightIcon}
iconPlacement="right"
className="absolute top-0 right-6"
asChild
>
<Link
href="/usage"
className="text-sm text-primary hover:underline"
>
See all usage
</Link>
</EnhancedButton>
)}
</CardHeader>
<CardContent>
<div className="flex flex-col gap-4">
<div className="flex flex-row gap-2">
{timePeriods.map((period) => (
<Button
key={period}
variant={period === timePeriod ? 'default' : 'secondary'}
size="sm"
onClick={() => setTimePeriod(period)}
>
{t(`analytics_period_${period}_days`)}
</Button>
))}
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_active_developers')}
</div>
<div className="mt-1 text-2xl font-semibold">
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.userCount ??
'-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_tasks_started')}
</div>
<div className="mt-1 text-2xl font-semibold">
{usage.data?.[TelemetryEventName.TASK_CREATED]?.eventCount ??
'-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_tasks_completed')}
</div>
<div className="mt-1 text-2xl font-semibold">
{usage.data?.[TelemetryEventName.TASK_COMPLETED]?.eventCount ??
'-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_tokens_consumed')}
</div>
<div className="mt-1 text-2xl font-semibold">
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.inputTokens ??
'-'}
</div>
</div>
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_llm_costs')}
</div>
<div className="mt-1 text-2xl font-semibold">
{usage.data?.[TelemetryEventName.LLM_COMPLETION]?.cost?.toFixed(
2,
) ?? '-'}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
};

View file

@ -23,9 +23,7 @@ export const auditLogs = pgTable('audit_logs', {
export enum AuditLogTargetType {
PROVIDER_WHITELIST = 1,
DEFAULT_PARAMETERS = 2,
MEMBER_CHANGE = 3, // TODO: Currently no logs of this type are collected
MEMBER_CHANGE = 3, // TODO: Currently no logs of this type are collected.
}
export type AuditLogType = Omit<typeof auditLogs.$inferSelect, 'targetType'> & {
targetType: AuditLogTargetType;
};
export type AuditLogType = typeof auditLogs.$inferSelect;

View file

@ -27,8 +27,7 @@
"analytics_tokens_consumed": "Tokens Consumed",
"analytics_costs": "Costs (USD)",
"analytics_llm_costs": "LLM Model Costs (USD)",
"analytics_active_developers": "Active Developers",
"analytics_view_details": "View detailed analytics"
"analytics_active_developers": "Active Developers"
},
"UserProfile": {
"title_bar": "User Profile",

View file

@ -1,15 +1,18 @@
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/onboarding(.*)',
'/api(.*)',
const isUnprotectedRoute = createRouteMatcher([
'/sign-in(.*)',
'/sign-up(.*)',
'/',
]);
export default clerkMiddleware(
async (auth, req) => {
if (isProtectedRoute(req)) {
if (!isUnprotectedRoute(req)) {
console.log(`clerkMiddleware: PROTECTED -> ${req.nextUrl.pathname}`);
await auth.protect();
} else {
console.log(`clerkMiddleware: UNPROTECTED -> ${req.nextUrl.pathname}`);
}
},
{ debug: false },

View file

@ -1,119 +0,0 @@
@import 'tailwindcss';
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}