wip: dashboard

This commit is contained in:
Derock 2023-11-05 21:37:59 -05:00
parent 66796503a5
commit b6c91e8bd0
No known key found for this signature in database
20 changed files with 604 additions and 20 deletions

View file

@ -14,6 +14,7 @@
"@prisma/migrate": "^5.5.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@t3-oss/env-nextjs": "^0.7.1",
"@tanstack/react-query": "^4.36.1",
@ -33,11 +34,13 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.11.0",
"recharts": "^2.9.2",
"sonner": "^1.2.0",
"superjson": "^2.2.0",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"ts-permissions": "^1.0.0",
"ua-parser-js": "^1.0.37",
"zod": "^3.22.4"
},
"devDependencies": {
@ -48,6 +51,7 @@
"@types/node": "^20.8.10",
"@types/react": "^18.2.35",
"@types/react-dom": "^18.2.14",
"@types/ua-parser-js": "^0.7.38",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"autoprefixer": "^10.4.16",

View file

@ -17,6 +17,9 @@ dependencies:
'@radix-ui/react-label':
specifier: ^2.0.2
version: 2.0.2(@types/react-dom@18.2.14)(@types/react@18.2.35)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-separator':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.35)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot':
specifier: ^1.0.2
version: 1.0.2(@types/react@18.2.35)(react@18.2.0)
@ -74,6 +77,9 @@ dependencies:
react-icons:
specifier: ^4.11.0
version: 4.11.0(react@18.2.0)
recharts:
specifier: ^2.9.2
version: 2.9.2(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
sonner:
specifier: ^1.2.0
version: 1.2.0(react-dom@18.2.0)(react@18.2.0)
@ -89,6 +95,9 @@ dependencies:
ts-permissions:
specifier: ^1.0.0
version: 1.0.0
ua-parser-js:
specifier: ^1.0.37
version: 1.0.37
zod:
specifier: ^3.22.4
version: 3.22.4
@ -115,6 +124,9 @@ devDependencies:
'@types/react-dom':
specifier: ^18.2.14
version: 18.2.14
'@types/ua-parser-js':
specifier: ^0.7.38
version: 0.7.38
'@typescript-eslint/eslint-plugin':
specifier: ^6.9.1
version: 6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.53.0)(typescript@5.2.2)
@ -1042,6 +1054,27 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.35)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.2
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.35)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.35
'@types/react-dom': 18.2.14
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-slot@1.0.2(@types/react@18.2.35)(react@18.2.0):
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
@ -1214,6 +1247,48 @@ packages:
'@types/node': 20.8.10
dev: false
/@types/d3-array@3.2.0:
resolution: {integrity: sha512-tjU8juPSfhMnu6mJZPOCVVGba4rZoE0tjHDPb81PYwA8CzbaFscGjgkUM7juUJu6iWA1cCVWNEVwxZ5HN9Jj8Q==}
dev: false
/@types/d3-color@3.1.2:
resolution: {integrity: sha512-At+Ski7dL8Bs58E8g8vPcFJc8tGcaC12Z4m07+p41+DRqnZQcAlp3NfYjLrhNYv+zEyQitU1CUxXNjqUyf+c0g==}
dev: false
/@types/d3-ease@3.0.1:
resolution: {integrity: sha512-VZofjpEt8HWv3nxUAosj5o/+4JflnJ7Bbv07k17VO3T2WRuzGdZeookfaF60iVh5RdhVG49LE5w6LIshVUC6rg==}
dev: false
/@types/d3-interpolate@3.0.3:
resolution: {integrity: sha512-6OZ2EIB4lLj+8cUY7I/Cgn9Q+hLdA4DjJHYOQDiHL0SzqS1K9DL5xIOVBSIHgF+tiuO9MU1D36qvdIvRDRPh+Q==}
dependencies:
'@types/d3-color': 3.1.2
dev: false
/@types/d3-path@3.0.1:
resolution: {integrity: sha512-blRhp7ki7pVznM8k6lk5iUU9paDbVRVq+/xpf0RRgSJn5gr6SE7RcFtxooYGMBOc1RZiGyqRpVdu5AD0z0ooMA==}
dev: false
/@types/d3-scale@4.0.6:
resolution: {integrity: sha512-lo3oMLSiqsQUovv8j15X4BNEDOsnHuGjeVg7GRbAuB2PUa1prK5BNSOu6xixgNf3nqxPl4I1BqJWrPvFGlQoGQ==}
dependencies:
'@types/d3-time': 3.0.2
dev: false
/@types/d3-shape@3.1.4:
resolution: {integrity: sha512-M2/xsWPsjaZc5ifMKp1EBp0gqJG0eO/zlldJNOC85Y/5DGsBQ49gDkRJ2h5GY7ZVD6KUumvZWsylSbvTaJTqKg==}
dependencies:
'@types/d3-path': 3.0.1
dev: false
/@types/d3-time@3.0.2:
resolution: {integrity: sha512-kbdRXTmUgNfw5OTE3KZnFQn6XdIc4QGroN5UixgdrXATmYsdlPQS6pEut9tVlIojtzuFD4txs/L+Rq41AHtLpg==}
dev: false
/@types/d3-timer@3.0.1:
resolution: {integrity: sha512-GGTvzKccVEhxmRfJEB6zhY9ieT4UhGVUIQaBzFpUO9OXy2ycAlnPCSJLzmGGgqt3KVjqN3QCQB4g1rsZnHsWhg==}
dev: false
/@types/debug@4.1.9:
resolution: {integrity: sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==}
dependencies:
@ -1286,6 +1361,10 @@ packages:
resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==}
dev: true
/@types/ua-parser-js@0.7.38:
resolution: {integrity: sha512-59CA5oavBEWSNLtS/BChj9xntiWMsIf9IytjxmBo9OuZEYuRzRf3K1ARzFPlXTOz5Zm2wXI38AP9RlLqDYMToQ==}
dev: true
/@types/webidl-conversions@7.0.2:
resolution: {integrity: sha512-uNv6b/uGRLlCVmelat2rA8bcVd3k/42mV2EmjhPh6JLkd35T5bgwR/t6xy7a9MWhd9sixIeBUzhBenvk3NO+DQ==}
dev: false
@ -1952,6 +2031,10 @@ packages:
clsx: 2.0.0
dev: false
/classnames@2.3.2:
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
dev: false
/clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
@ -2114,6 +2197,77 @@ packages:
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
/d3-array@3.2.4:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
dependencies:
internmap: 2.0.3
dev: false
/d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
dev: false
/d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
dev: false
/d3-format@3.1.0:
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
engines: {node: '>=12'}
dev: false
/d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
dependencies:
d3-color: 3.1.0
dev: false
/d3-path@3.1.0:
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
engines: {node: '>=12'}
dev: false
/d3-scale@4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
d3-format: 3.1.0
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
dev: false
/d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'}
dependencies:
d3-path: 3.1.0
dev: false
/d3-time-format@4.1.0:
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
engines: {node: '>=12'}
dependencies:
d3-time: 3.1.0
dev: false
/d3-time@3.1.0:
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
dev: false
/d@1.0.1:
resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==}
dependencies:
@ -2147,6 +2301,10 @@ packages:
dependencies:
ms: 2.1.2
/decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
dev: false
/decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@ -2260,6 +2418,12 @@ packages:
esutils: 2.0.3
dev: true
/dom-helpers@3.4.0:
resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==}
dependencies:
'@babel/runtime': 7.23.2
dev: false
/dotenv@16.0.3:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'}
@ -2891,6 +3055,10 @@ packages:
engines: {node: '>=6'}
dev: false
/eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: false
/events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
@ -2925,6 +3093,11 @@ packages:
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
/fast-equals@5.0.1:
resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==}
engines: {node: '>=6.0.0'}
dev: false
/fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
dev: false
@ -3429,6 +3602,11 @@ packages:
hasown: 2.0.0
side-channel: 1.0.4
/internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
dev: false
/ip@2.0.0:
resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==}
dev: false
@ -4920,7 +5098,6 @@ packages:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
dev: true
/pump@3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
@ -4970,7 +5147,35 @@ packages:
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
/react-lifecycles-compat@3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
dev: false
/react-resize-detector@8.1.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==}
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
lodash: 4.17.21
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-smooth@2.0.5(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==}
peerDependencies:
prop-types: ^15.6.0
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
fast-equals: 5.0.1
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-transition-group: 2.9.0(react-dom@18.2.0)(react@18.2.0)
dev: false
/react-ssr-prepass@1.5.0(react@18.2.0):
resolution: {integrity: sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==}
@ -4980,6 +5185,20 @@ packages:
react: 18.2.0
dev: false
/react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==}
peerDependencies:
react: '>=15.0.0'
react-dom: '>=15.0.0'
dependencies:
dom-helpers: 3.4.0
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-lifecycles-compat: 3.0.4
dev: false
/react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
@ -5055,6 +5274,34 @@ packages:
dependencies:
picomatch: 2.3.1
/recharts-scale@0.4.5:
resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
dependencies:
decimal.js-light: 2.5.1
dev: false
/recharts@2.9.2(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-ig0zYgO5nUP/896GW16b9yy2sHIRW1AHB90x48hypFTSjjxQt/J9rPzlLJjgNupzJKEHPCwMi1VnvN/k20K45w==}
engines: {node: '>=12'}
peerDependencies:
prop-types: ^15.6.0
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
classnames: 2.3.2
eventemitter3: 4.0.7
lodash: 4.17.21
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-is: 16.13.1
react-resize-detector: 8.1.0(react-dom@18.2.0)(react@18.2.0)
react-smooth: 2.0.5(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
recharts-scale: 0.4.5
tiny-invariant: 1.3.1
victory-vendor: 36.6.12
dev: false
/reflect.getprototypeof@1.0.4:
resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
engines: {node: '>= 0.4'}
@ -5705,6 +5952,10 @@ packages:
next-tick: 1.1.0
dev: true
/tiny-invariant@1.3.1:
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
dev: false
/tmp@0.2.1:
resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==}
engines: {node: '>=8.17.0'}
@ -5847,6 +6098,10 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
/ua-parser-js@1.0.37:
resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==}
dev: false
/unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
dependencies:
@ -5915,6 +6170,25 @@ packages:
spdx-expression-parse: 3.0.1
dev: false
/victory-vendor@36.6.12:
resolution: {integrity: sha512-pJrTkNHln+D83vDCCSUf0ZfxBvIaVrFHmrBOsnnLAbdqfudRACAj51He2zU94/IWq9464oTADcPVkmWAfNMwgA==}
dependencies:
'@types/d3-array': 3.2.0
'@types/d3-ease': 3.0.1
'@types/d3-interpolate': 3.0.3
'@types/d3-scale': 4.0.6
'@types/d3-shape': 3.1.4
'@types/d3-time': 3.0.2
'@types/d3-timer': 3.0.1
d3-array: 3.2.4
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-scale: 4.0.2
d3-shape: 3.2.0
d3-time: 3.1.0
d3-timer: 3.0.1
dev: false
/watchpack@2.4.0:
resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
engines: {node: '>=10.13.0'}

View file

@ -24,9 +24,9 @@ export default function LoginForm() {
>();
const login = api.auth.login.useMutation({
onSuccess: () => {
onSuccess: (data) => {
toast.success("Successfully logged in!", { id: toastLoading });
router.push("/dashboard");
router.push("/home");
},
onError: (error) => {

View file

@ -1,11 +0,0 @@
import { api } from "~/trpc/server";
import Test from "./RSC";
export default async function DashboardHome() {
return (
<div>
rip
<Test />
</div>
);
}

View file

View file

@ -0,0 +1,9 @@
/* tailwind does not have text-shadow support, so we do it ourselves */
.stat-card > p {
/* -webkit-text-stroke: 1px theme("colors.card.DEFAULT"); */
text-shadow:
1px 1px 0 theme("colors.card.DEFAULT"),
-1px 1px 0 theme("colors.card.DEFAULT"),
1px -1px 0 theme("colors.card.DEFAULT"),
-1px -1px 0 theme("colors.card.DEFAULT");
}

52
src/app/home/StatCard.tsx Normal file
View file

@ -0,0 +1,52 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { RiPulseFill } from "react-icons/ri";
import { LineChart, Line, ResponsiveContainer } from "recharts";
import styles from "./StatCard.module.css";
const TEST_DATA = [
{ cpu: 0.05 },
{ cpu: 0.1 },
{ cpu: 0.08 },
{ cpu: 0.09 },
{ cpu: 0.2 },
{ cpu: 0.1 },
{ cpu: 0.12 },
{ cpu: 0.3 },
];
export function StatCard() {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>CPU Usage</CardTitle>
<RiPulseFill className="text-2xl text-muted-foreground" />
</CardHeader>
<CardContent className="relative">
{/* chart background */}
<div className="absolute inset-0 z-0 h-full w-full">
<ResponsiveContainer width={"100%"} height={"100%"}>
<LineChart data={TEST_DATA}>
<Line
type="monotone"
dataKey={"cpu"}
stroke="hsl(var(--border))"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
<div className={`relative z-10 ${styles["stat-card"]}`}>
<p className="stroke stroke-card text-2xl font-bold">4.5%</p>
<p className="stroke stroke-card text-sm text-muted-foreground">
of 8 CPUs
</p>
</div>
</CardContent>
</Card>
);
}

17
src/app/home/page.tsx Normal file
View file

@ -0,0 +1,17 @@
import { api } from "~/trpc/server";
import Test from "./RSC";
import { StatCard } from "./StatCard";
export default async function DashboardHome() {
return (
<div className="mx-auto max-w-[1500px]">
<Test />
<div className="m-8 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
<StatCard />
<StatCard />
<StatCard />
<StatCard />
</div>
</div>
);
}

View file

@ -0,0 +1,21 @@
"use client";
import { usePathname } from "next/navigation";
type SidebarNavItem = {
title: string;
description: string;
href: string;
};
export function SettingsHeader(props: { items: SidebarNavItem[] }) {
const path = usePathname();
const active = props.items.find((item) => item.href === path);
return (
<div>
<h3 className="text-lg font-medium">{active?.title}</h3>
<p className="text-muted-foreground">{active?.description}</p>
</div>
);
}

View file

@ -0,0 +1,5 @@
"use client";
export default function AccountSettings() {
return <></>;
}

View file

@ -0,0 +1,65 @@
import Image from "next/image";
import { Separator } from "~/components/ui/separator";
import { SidebarNav } from "~/components/SidebarNav";
import { SettingsHeader } from "./SettingsHeader";
const sidebarNavItems = [
{
title: "Account",
description: "Manage your account security and settings.",
href: "/settings/account",
},
{
title: "Sessions",
description: "Manage your active sessions and logout remotely.",
href: "/settings/sessions",
},
];
interface SettingsLayoutProps {
children: React.ReactNode;
}
export default function SettingsLayout({ children }: SettingsLayoutProps) {
return (
<>
<div className="md:hidden">
<Image
src="/examples/forms-light.png"
width={1280}
height={791}
alt="Forms"
className="block dark:hidden"
/>
<Image
src="/examples/forms-dark.png"
width={1280}
height={791}
alt="Forms"
className="hidden dark:block"
/>
</div>
<div className="hidden space-y-6 p-10 pb-16 md:block">
<div className="space-y-0.5">
<h2 className="text-2xl font-bold tracking-tight">Settings</h2>
<p className="text-muted-foreground">
Manage your account settings as well as the global instance
settings.
</p>
</div>
<Separator className="my-6" />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<aside className="-mx-4 lg:w-1/5">
<SidebarNav items={sidebarNavItems} />
</aside>
<div className="flex-1 space-y-6 lg:max-w-2xl">
<SettingsHeader items={sidebarNavItems} />
<Separator />
{children}
</div>
</div>
</div>
</>
);
}

View file

@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function Settings() {
redirect("/settings/account");
}

View file

@ -0,0 +1,23 @@
import { Card } from "~/components/ui/card";
import UAParser from "ua-parser-js";
type SessionData = {
lastUA: string | null;
lastIP: string | null;
lastAccessed: Date | null;
createdAt: number;
id: string;
};
export default function Session(props: { session: SessionData }) {
const ua = new UAParser(props.session.lastUA ?? "");
return (
<Card className="p-6">
<p className="text-lg font-bold">{ua.getBrowser().name}</p>
<p className="text-sm text-muted-foreground">
{ua.getOS().name} {ua.getOS().version}
</p>
</Card>
);
}

View file

@ -0,0 +1,15 @@
import { Card, CardContent } from "~/components/ui/card";
import { api } from "~/trpc/server";
import Session from "./Sessions";
export default async function SessionsPage() {
const sessions = await api.auth.sessions.list.query();
return (
<>
{sessions.map((session, idx) => (
<Session key={idx} session={session} />
))}
</>
);
}

View file

@ -0,0 +1,43 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { buttonVariants } from "~/components/ui/button";
import { cn } from "~/utils/utils";
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: {
href: string;
title: string;
}[];
}
export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
const pathname = usePathname();
return (
<nav
className={cn(
"flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
className,
)}
{...props}
>
{items.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
buttonVariants({ variant: "ghost" }),
pathname === item.href
? "bg-muted hover:bg-muted"
: "hover:bg-transparent hover:underline",
"justify-start",
)}
>
{item.title}
</Link>
))}
</nav>
);
}

View file

@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "~/utils/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

View file

@ -3,14 +3,17 @@ import {
authenticatedProcedure,
createTRPCRouter,
publicProcedure,
} from "../trpc";
} from "../../trpc";
import { users } from "~/server/db/schema";
import { eq } from "drizzle-orm";
import { TRPCError } from "@trpc/server";
import argon2 from "argon2";
import { Session } from "~/server/auth/Session";
import { sessionsRouter } from "./sessions";
export const authRouter = createTRPCRouter({
sessions: sessionsRouter,
me: authenticatedProcedure.query(async ({ ctx }) => {
const user = await ctx.session.getUser();

View file

@ -0,0 +1,21 @@
import { sessions } from "~/server/db/schema";
import { authenticatedProcedure, createTRPCRouter } from "../../trpc";
import { eq } from "drizzle-orm";
import { Session } from "~/server/auth/Session";
export const sessionsRouter = createTRPCRouter({
list: authenticatedProcedure.query(async ({ ctx }) => {
const activeSessions = ctx.db
.select({
lastUA: sessions.lastUA,
lastIP: sessions.lastIP,
lastAccessed: sessions.lastAccessed,
createdAt: sessions.createdAt,
id: sessions.token,
})
.from(sessions)
.where(eq(sessions.userId, ctx.session.data.userId));
return activeSessions;
}),
});

View file

@ -4,6 +4,7 @@ import { users, sessions } from "../db/schema";
import { randomBytes } from "crypto";
import assert from "assert";
import { NextRequest, userAgent } from "next/server";
import { hash } from "argon2";
export type SessionUpdateData = Partial<{
ua: string;
@ -90,13 +91,13 @@ export class Session {
.values({
lastUA: parsedContext.ua,
lastIP: parsedContext.ip,
token,
token: await hash(token),
userId,
})
.returning();
assert(sessionData, "Session should be created");
return new Session(sessionData);
return new Session(sessionData, token);
}
/**
@ -112,8 +113,12 @@ export class Session {
/**
* Create a new session instance from a user's session data.
* @param sessionData The user's session data.
* @param unhashedSessionToken The unhashed session token.
*/
constructor(public readonly data: typeof sessions.$inferSelect) {}
constructor(
public readonly data: typeof sessions.$inferSelect,
private readonly unhashedSessionToken?: string,
) {}
/**
* Get the user associated with this session.
@ -145,9 +150,11 @@ export class Session {
* Returns a cookie string for this session.
*/
getCookieString() {
assert(this.unhashedSessionToken, "Sessions cannot be unhashed");
const expire = new Date(this.data.createdAt + Session.EXPIRE_TIME);
return `sessionToken=${
this.data.token
this.unhashedSessionToken
}; Expires=${expire.toUTCString()}; Path=/; HttpOnly; SameSite=Strict`;
}
}