admin dashboard user managment ui; set users permission actions
This commit is contained in:
parent
5c09688f1a
commit
dd3c321350
@ -24,9 +24,11 @@
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
@ -50,12 +52,14 @@
|
||||
"lucide-react": "^0.477.0",
|
||||
"next": "^15.0.1",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"next-themes": "^0.4.4",
|
||||
"postgres": "^3.4.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-textarea-autosize": "^8.5.7",
|
||||
"server-only": "^0.0.1",
|
||||
"sonner": "^2.0.1",
|
||||
"superjson": "^2.2.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
165
pnpm-lock.yaml
generated
165
pnpm-lock.yaml
generated
@ -20,6 +20,9 @@ importers:
|
||||
'@radix-ui/react-avatar':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-checkbox':
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -29,6 +32,9 @@ importers:
|
||||
'@radix-ui/react-popover':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-select':
|
||||
specifier: ^2.1.6
|
||||
version: 2.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-separator':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -98,6 +104,9 @@ importers:
|
||||
next-auth:
|
||||
specifier: 5.0.0-beta.25
|
||||
version: 5.0.0-beta.25(next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
next-themes:
|
||||
specifier: ^0.4.4
|
||||
version: 0.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
postgres:
|
||||
specifier: ^3.4.4
|
||||
version: 3.4.5
|
||||
@ -116,6 +125,9 @@ importers:
|
||||
server-only:
|
||||
specifier: ^0.0.1
|
||||
version: 0.0.1
|
||||
sonner:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
superjson:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.2
|
||||
@ -931,6 +943,9 @@ packages:
|
||||
'@popperjs/core@2.11.8':
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
|
||||
'@radix-ui/number@1.1.0':
|
||||
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
|
||||
|
||||
'@radix-ui/primitive@1.1.1':
|
||||
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
|
||||
|
||||
@ -960,6 +975,32 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-checkbox@1.1.4':
|
||||
resolution: {integrity: sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-collection@1.1.2':
|
||||
resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.1':
|
||||
resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==}
|
||||
peerDependencies:
|
||||
@ -991,6 +1032,15 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-direction@1.1.0':
|
||||
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.5':
|
||||
resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==}
|
||||
peerDependencies:
|
||||
@ -1113,6 +1163,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-select@2.1.6':
|
||||
resolution: {integrity: sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-separator@1.1.2':
|
||||
resolution: {integrity: sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==}
|
||||
peerDependencies:
|
||||
@ -1184,6 +1247,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.0':
|
||||
resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.0':
|
||||
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
||||
peerDependencies:
|
||||
@ -2573,6 +2645,12 @@ packages:
|
||||
nodemailer:
|
||||
optional: true
|
||||
|
||||
next-themes@0.4.4:
|
||||
resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next@15.2.1:
|
||||
resolution: {integrity: sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
@ -3089,6 +3167,12 @@ packages:
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
sonner@2.0.1:
|
||||
resolution: {integrity: sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -3896,6 +3980,8 @@ snapshots:
|
||||
|
||||
'@popperjs/core@2.11.8': {}
|
||||
|
||||
'@radix-ui/number@1.1.0': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.1': {}
|
||||
|
||||
'@radix-ui/react-arrow@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
@ -3919,6 +4005,34 @@ snapshots:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-checkbox@1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-use-size': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-collection@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-slot': 1.1.2(@types/react@18.3.18)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.18)(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
@ -3953,6 +4067,12 @@ snapshots:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-direction@1.1.0(@types/react@18.3.18)(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
|
||||
'@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
@ -4069,6 +4189,35 @@ snapshots:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-select@2.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.0
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
'@radix-ui/react-collection': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-popper': 1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-portal': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-slot': 1.1.2(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
aria-hidden: 1.2.4
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-remove-scroll: 2.6.3(@types/react@18.3.18)(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-separator@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -4131,6 +4280,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.0(@types/react@18.3.18)(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.0(@types/react@18.3.18)(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/rect': 1.1.0
|
||||
@ -5683,6 +5838,11 @@ snapshots:
|
||||
next: 15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
|
||||
next-themes@0.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@next/env': 15.2.1
|
||||
@ -6228,6 +6388,11 @@ snapshots:
|
||||
is-arrayish: 0.3.2
|
||||
optional: true
|
||||
|
||||
sonner@2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
|
||||
@ -22,10 +22,10 @@ const data = {
|
||||
title: "Benutzer",
|
||||
url: "/admin",
|
||||
items: [
|
||||
{
|
||||
title: "Team Mitglieder",
|
||||
url: "/admin/team",
|
||||
},
|
||||
// {
|
||||
// title: "Team Mitglieder",
|
||||
// url: "/admin/team",
|
||||
// },
|
||||
{
|
||||
title: "Alle Benutzer",
|
||||
url: "/admin/benutzer",
|
||||
|
||||
112
src/app/admin/_components/user-permissions-form.tsx
Normal file
112
src/app/admin/_components/user-permissions-form.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { z } from "zod";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
|
||||
import {
|
||||
hasPermission,
|
||||
roleDefinitionsArray,
|
||||
} from "@/lib/validation/permissions";
|
||||
import React from "react";
|
||||
import { setUserPermissions } from "@/server/actions/user";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const formSchema = z.object({
|
||||
permission: z.number().min(1),
|
||||
});
|
||||
const dynamicRoles = roleDefinitionsArray.filter(
|
||||
({ role: { name } }) => name !== "User",
|
||||
);
|
||||
|
||||
export type UserPermissionsFormProps = {
|
||||
server_permissions: number;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
function UserPermissionsForm({
|
||||
server_permissions,
|
||||
userId,
|
||||
cb,
|
||||
}: UserPermissionsFormProps & {
|
||||
cb?: () => void;
|
||||
}) {
|
||||
// const userRoles = getUserPermissions(permissions);
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
permission: server_permissions,
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Define a submit handler.
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
const result = await setUserPermissions(userId, values.permission);
|
||||
if (result.success) toast.success("Benutzerrechte gespeichert");
|
||||
else toast.error("Fehler beim Speichern der Benutzerrechte");
|
||||
cb?.();
|
||||
}
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="permission"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Benutzerrechte</FormLabel>
|
||||
<FormControl>
|
||||
<ul className="space-y-1">
|
||||
{dynamicRoles.map(({ key, role }) => {
|
||||
const hasRole = hasPermission(field.value, Number(key));
|
||||
return (
|
||||
<li
|
||||
key={key}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-md bg-muted px-2 py-1",
|
||||
Object.values(role.color).join(" "),
|
||||
)}
|
||||
>
|
||||
<span>{role.name}</span>
|
||||
<Checkbox
|
||||
id={key}
|
||||
checked={hasRole}
|
||||
onCheckedChange={() => {
|
||||
const newPermissionFlag = hasRole
|
||||
? field.value - Number(key)
|
||||
: field.value + Number(key);
|
||||
field.onChange(newPermissionFlag);
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{form.formState.isDirty && (
|
||||
<Button type="submit" className="w-full">
|
||||
Speichern
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserPermissionsForm;
|
||||
35
src/app/admin/_components/user-permissions-popover.tsx
Normal file
35
src/app/admin/_components/user-permissions-popover.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Settings } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import UserPermissionsForm, {
|
||||
UserPermissionsFormProps,
|
||||
} from "./user-permissions-form";
|
||||
|
||||
function UserPermissionsPopover(props: UserPermissionsFormProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size={"icon"}
|
||||
variant={"outline"}
|
||||
className="size-max gap-1 bg-muted p-px text-muted-foreground"
|
||||
>
|
||||
<Settings className="size-3" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="left" align="start">
|
||||
<UserPermissionsForm {...props} cb={() => setOpen(false)} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserPermissionsPopover;
|
||||
113
src/app/admin/benutzer/columns.tsx
Normal file
113
src/app/admin/benutzer/columns.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getUserPermissions } from "@/lib/validation/permissions";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ArrowUpDown, Copy, Mail } from "lucide-react";
|
||||
import { User } from "next-auth";
|
||||
|
||||
import UserPermissionsPopover from "../_components/user-permissions-popover";
|
||||
import CopyToClip from "@/components/copy-to-clip";
|
||||
|
||||
export const columns: ColumnDef<User>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Name
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "email",
|
||||
cell: ({ row }) => {
|
||||
const email = String(row.getValue("email"));
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<CopyToClip text={email} targetName="Email" />
|
||||
<span>{email}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
<Mail className="size-4" />
|
||||
<span>Email</span>
|
||||
<ArrowUpDown className="size-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Benutzerrechte
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const roles = getUserPermissions(Number(row.getValue("role")));
|
||||
const userId = String(row.getValue("id"));
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<UserPermissionsPopover
|
||||
server_permissions={Number(row.getValue("role"))}
|
||||
userId={userId}
|
||||
/>
|
||||
{roles
|
||||
.filter(({ name }) => name !== "User")
|
||||
.map((role) => (
|
||||
<Badge
|
||||
key={role.name}
|
||||
variant={"outline"}
|
||||
className={`${Object.values(role.color).join(" ")}`}
|
||||
>
|
||||
{role.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: () => <div className="text-end">Benutzer ID</div>,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<CopyToClip
|
||||
text={row.getValue("id")}
|
||||
targetName="Benutzer ID"
|
||||
buttonProps={{
|
||||
size: "default",
|
||||
variant: "outline",
|
||||
}}
|
||||
>
|
||||
<Copy className="size-4" />
|
||||
<span>Kopieren</span>
|
||||
</CopyToClip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -10,7 +10,7 @@ async function Page() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Team Mitglieder</h1>
|
||||
<h1 className="text-2xl font-bold">Benutzer Manager</h1>
|
||||
<Button variant={"outline"}>Mitglied einladen</Button>
|
||||
</div>
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getUserPermissions } from "@/lib/validation/permissions";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Plus } from "lucide-react";
|
||||
import { User } from "next-auth";
|
||||
|
||||
export const columns: ColumnDef<User>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: "Email",
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: "Rollen",
|
||||
cell: ({ row }) => {
|
||||
const roles = getUserPermissions(Number(row.getValue("role")));
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{roles
|
||||
.filter(({ name }) => name !== "User")
|
||||
.map((role) => (
|
||||
<Badge
|
||||
key={role.name}
|
||||
variant={"outline"}
|
||||
className={`${Object.values(role.color).join(" ")}`}
|
||||
>
|
||||
{role.name}
|
||||
</Badge>
|
||||
))}
|
||||
<Button
|
||||
size={"icon"}
|
||||
variant={"outline"}
|
||||
className="size-max gap-1 bg-muted p-px text-muted-foreground"
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -4,6 +4,7 @@ import { GeistSans } from "geist/font/sans";
|
||||
import { type Metadata } from "next";
|
||||
|
||||
import { TRPCReactProvider } from "@/trpc/react";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create T3 App",
|
||||
@ -17,7 +18,10 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" className={`${GeistSans.variable}`}>
|
||||
<body>
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
<TRPCReactProvider>
|
||||
<Toaster />
|
||||
{children}
|
||||
</TRPCReactProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
44
src/components/copy-to-clip.tsx
Normal file
44
src/components/copy-to-clip.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { Button, ButtonProps } from "./ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Copy } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
function CopyToClip({
|
||||
text,
|
||||
children,
|
||||
buttonProps,
|
||||
targetName,
|
||||
}: {
|
||||
text: string;
|
||||
children?: React.ReactNode;
|
||||
buttonProps?: ButtonProps;
|
||||
targetName?: string;
|
||||
}) {
|
||||
const handleClick = () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast.success(`${targetName ?? "Text"} in die Zwischenablage kopiert`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
size={"icon"}
|
||||
onClick={handleClick}
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"size-max gap-1 bg-muted p-1 text-muted-foreground",
|
||||
buttonProps?.className,
|
||||
)}
|
||||
{...buttonProps}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
<Copy className="size-4" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyToClip;
|
||||
@ -1,80 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
src/components/data-table/index.tsx
Normal file
142
src/components/data-table/index.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
type PaginationState,
|
||||
type ColumnSizingState,
|
||||
type ColumnDef,
|
||||
type SortingState,
|
||||
type ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
getSortedRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
import { DataTablePagination } from "./pagination";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageSize: 25,
|
||||
pageIndex: 0,
|
||||
});
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
);
|
||||
const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({});
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onSortingChange: setSorting,
|
||||
onColumnSizingChange: setColumnSizing,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnSizing,
|
||||
pagination,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
placeholder="Email suchen..."
|
||||
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) =>
|
||||
table.getColumn("email")?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
style={{
|
||||
width: `${columnSizing[header.id] || "auto"}px`,
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: `${columnSizing[cell.column.id] || "auto"}px`,
|
||||
}}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<DataTablePagination table={table} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
99
src/components/data-table/pagination.tsx
Normal file
99
src/components/data-table/pagination.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import { Table } from "@tanstack/react-table";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
interface DataTablePaginationProps<TData> {
|
||||
table: Table<TData>;
|
||||
}
|
||||
|
||||
export function DataTablePagination<TData>({
|
||||
table,
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
const paginationOptions = [10, 25, 50, 100];
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
{/* {table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected. */}
|
||||
</div>
|
||||
<div className="flex items-center space-x-6 lg:space-x-8">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium">Reihen pro Seite</p>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[70px]">
|
||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
{paginationOptions.map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
<ChevronsLeft />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||
Seite {table.getState().pagination.pageIndex + 1} von{" "}
|
||||
{table.getPageCount()}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
<ChevronsRight />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
159
src/components/ui/select.tsx
Normal file
159
src/components/ui/select.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
31
src/components/ui/sonner.tsx
Normal file
31
src/components/ui/sonner.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@ -24,7 +24,7 @@ type RoleDefinition = {
|
||||
};
|
||||
};
|
||||
|
||||
const roles: { [key: number]: RoleDefinition } = {
|
||||
export const roleDefinitions: { [key: number]: RoleDefinition } = {
|
||||
[Role.USER]: {
|
||||
name: "User",
|
||||
color: {
|
||||
@ -50,9 +50,17 @@ const roles: { [key: number]: RoleDefinition } = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const roleDefinitionsArray = Object.entries(roleDefinitions).map(
|
||||
([key, role]) => ({
|
||||
key,
|
||||
role,
|
||||
}),
|
||||
);
|
||||
|
||||
export function getUserPermissions(userRole: number): RoleDefinition[] {
|
||||
return Object.keys(roles)
|
||||
return Object.keys(roleDefinitions)
|
||||
.map(Number) // Convert keys to numbers
|
||||
.filter((role) => (userRole & role) !== 0) // Check if role is set
|
||||
.map((role) => roles[role]!); // Convert back to role name
|
||||
.map((role) => roleDefinitions[role]!); // Convert back to role name
|
||||
}
|
||||
|
||||
11
src/server/actions/user.ts
Normal file
11
src/server/actions/user.ts
Normal file
@ -0,0 +1,11 @@
|
||||
"use server";
|
||||
|
||||
import { api } from "@/trpc/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function setUserPermissions(userId: string, permission: number) {
|
||||
const result = await api.users.setPermission({ userId, permission });
|
||||
if (!result[0]?.id) return { success: false };
|
||||
revalidatePath("/admin/benutzer");
|
||||
return { success: true };
|
||||
}
|
||||
@ -1,10 +1,31 @@
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { z } from "zod";
|
||||
import { permission } from "process";
|
||||
import { users } from "@/server/db/schema";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
export const usersRouter = createTRPCRouter({
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
const isAdmin = hasPermission(ctx.session.user.role, Role.ADMIN);
|
||||
if (!isAdmin) throw new Error("You are not allowed to get all users");
|
||||
return await ctx.db.query.users.findMany();
|
||||
return await ctx.db.query.users.findMany({
|
||||
orderBy: desc(users.role),
|
||||
});
|
||||
}),
|
||||
|
||||
setPermission: protectedProcedure
|
||||
.input(z.object({ userId: z.string(), permission: z.number() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const isAdmin = hasPermission(ctx.session.user.role, Role.ADMIN);
|
||||
if (!isAdmin)
|
||||
throw new Error("You are not allowed to set user permissions");
|
||||
return await ctx.db
|
||||
.update(users)
|
||||
.set({
|
||||
role: input.permission,
|
||||
})
|
||||
.where(eq(users.id, input.userId))
|
||||
.returning({ id: users.id });
|
||||
}),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user