Added clerk for authentication
This commit is contained in:
parent
d66a19021a
commit
b5fdb9ccae
29 changed files with 848 additions and 134 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.ts",
|
"config": "tailwind.config.ts",
|
||||||
"css": "src/styles/globals.css",
|
"css": "src/styles/globals.css",
|
||||||
"baseColor": "zinc",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,14 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/nextjs": "^6.5.1",
|
"@clerk/nextjs": "^6.5.1",
|
||||||
"@clerk/themes": "^2.1.48",
|
"@clerk/themes": "^2.1.48",
|
||||||
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.1",
|
"@radix-ui/react-navigation-menu": "^1.2.1",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
"@sentry/nextjs": "^8",
|
"@sentry/nextjs": "^8",
|
||||||
"@t3-oss/env-nextjs": "^0.10.1",
|
"@t3-oss/env-nextjs": "^0.10.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
|
|
@ -37,6 +42,7 @@
|
||||||
"posthog-js": "^1.194.1",
|
"posthog-js": "^1.194.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.53.2",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|
|
||||||
132
pnpm-lock.yaml
generated
132
pnpm-lock.yaml
generated
|
|
@ -14,15 +14,30 @@ importers:
|
||||||
'@clerk/themes':
|
'@clerk/themes':
|
||||||
specifier: ^2.1.48
|
specifier: ^2.1.48
|
||||||
version: 2.1.48
|
version: 2.1.48
|
||||||
|
'@hookform/resolvers':
|
||||||
|
specifier: ^3.9.1
|
||||||
|
version: 3.9.1(react-hook-form@7.53.2(react@18.3.1))
|
||||||
'@radix-ui/react-avatar':
|
'@radix-ui/react-avatar':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-checkbox':
|
||||||
|
specifier: ^1.1.2
|
||||||
|
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-label':
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-navigation-menu':
|
'@radix-ui/react-navigation-menu':
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
version: 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-slot':
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
'@radix-ui/react-switch':
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@sentry/nextjs':
|
'@sentry/nextjs':
|
||||||
specifier: ^8
|
specifier: ^8
|
||||||
version: 8.41.0(@opentelemetry/core@1.28.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.28.0(@opentelemetry/api@1.9.0))(next@15.0.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.96.1(esbuild@0.19.12))
|
version: 8.41.0(@opentelemetry/core@1.28.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.28.0(@opentelemetry/api@1.9.0))(next@15.0.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.96.1(esbuild@0.19.12))
|
||||||
|
|
@ -59,6 +74,9 @@ importers:
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
|
react-hook-form:
|
||||||
|
specifier: ^7.53.2
|
||||||
|
version: 7.53.2(react@18.3.1)
|
||||||
server-only:
|
server-only:
|
||||||
specifier: ^0.0.1
|
specifier: ^0.0.1
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
|
|
@ -532,6 +550,11 @@ packages:
|
||||||
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
|
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@hookform/resolvers@3.9.1':
|
||||||
|
resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==}
|
||||||
|
peerDependencies:
|
||||||
|
react-hook-form: ^7.0.0
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
|
|
@ -994,6 +1017,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-checkbox@1.1.2':
|
||||||
|
resolution: {integrity: sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==}
|
||||||
|
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.0':
|
'@radix-ui/react-collection@1.1.0':
|
||||||
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
|
resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1100,6 +1136,19 @@ packages:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-label@2.1.0':
|
||||||
|
resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==}
|
||||||
|
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-navigation-menu@1.2.1':
|
'@radix-ui/react-navigation-menu@1.2.1':
|
||||||
resolution: {integrity: sha512-egDo0yJD2IK8L17gC82vptkvW1jLeni1VuqCyzY727dSJdk5cDjINomouLoNk8RVF7g2aNIfENKWL4UzeU9c8Q==}
|
resolution: {integrity: sha512-egDo0yJD2IK8L17gC82vptkvW1jLeni1VuqCyzY727dSJdk5cDjINomouLoNk8RVF7g2aNIfENKWL4UzeU9c8Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1161,6 +1210,19 @@ packages:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-switch@1.1.1':
|
||||||
|
resolution: {integrity: sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==}
|
||||||
|
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-use-callback-ref@1.1.0':
|
'@radix-ui/react-use-callback-ref@1.1.0':
|
||||||
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
|
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1206,6 +1268,15 @@ packages:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-use-size@1.1.0':
|
||||||
|
resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-visually-hidden@1.1.0':
|
'@radix-ui/react-visually-hidden@1.1.0':
|
||||||
resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
|
resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -2989,6 +3060,12 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.3.1
|
react: ^18.3.1
|
||||||
|
|
||||||
|
react-hook-form@7.53.2:
|
||||||
|
resolution: {integrity: sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
|
|
||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
|
@ -3834,6 +3911,10 @@ snapshots:
|
||||||
|
|
||||||
'@eslint/js@8.57.1': {}
|
'@eslint/js@8.57.1': {}
|
||||||
|
|
||||||
|
'@hookform/resolvers@3.9.1(react-hook-form@7.53.2(react@18.3.1))':
|
||||||
|
dependencies:
|
||||||
|
react-hook-form: 7.53.2(react@18.3.1)
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
|
|
@ -4315,6 +4396,22 @@ snapshots:
|
||||||
'@types/react': 18.3.12
|
'@types/react': 18.3.12
|
||||||
'@types/react-dom': 18.3.1
|
'@types/react-dom': 18.3.1
|
||||||
|
|
||||||
|
'@radix-ui/react-checkbox@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.0
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
'@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
'@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(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.12)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.12
|
||||||
|
'@types/react-dom': 18.3.1
|
||||||
|
|
||||||
'@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
|
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
|
@ -4410,6 +4507,15 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.12
|
'@types/react': 18.3.12
|
||||||
|
|
||||||
|
'@radix-ui/react-label@2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.12
|
||||||
|
'@types/react-dom': 18.3.1
|
||||||
|
|
||||||
'@radix-ui/react-navigation-menu@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-navigation-menu@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.0
|
'@radix-ui/primitive': 1.1.0
|
||||||
|
|
@ -4468,6 +4574,21 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.12
|
'@types/react': 18.3.12
|
||||||
|
|
||||||
|
'@radix-ui/react-switch@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.0
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
'@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(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.12)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.12
|
||||||
|
'@types/react-dom': 18.3.1
|
||||||
|
|
||||||
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)':
|
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|
@ -4500,6 +4621,13 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.12
|
'@types/react': 18.3.12
|
||||||
|
|
||||||
|
'@radix-ui/react-use-size@1.1.0(@types/react@18.3.12)(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.12
|
||||||
|
|
||||||
'@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|
@ -6431,6 +6559,10 @@ snapshots:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
scheduler: 0.23.2
|
scheduler: 0.23.2
|
||||||
|
|
||||||
|
react-hook-form@7.53.2(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1):
|
react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { Dialog, DialogContent, DialogTitle } from "~/components/ui/dialog";
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function Modal({ children }: { children: React.ReactNode }) {
|
export default function Modal({ children }: { children: React.ReactNode }) {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,26 @@
|
||||||
import FullPageTimer from "~/app/_components/full-page-timer";
|
import FullPageTimer from "@/app/_components/full-page-timer";
|
||||||
import { getTimer } from "~/server/queries";
|
import { getTimer } from "@/server/queries";
|
||||||
import Modal from "./modal";
|
import Modal from "./modal";
|
||||||
|
import { useAuth } from "@clerk/nextjs";
|
||||||
|
|
||||||
export default async function TimerModal({
|
export default async function TimerModal({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
}) {
|
}) {
|
||||||
|
const user = useAuth();
|
||||||
|
|
||||||
const timer = await getTimer((await params).id);
|
const timer = await getTimer((await params).id);
|
||||||
|
|
||||||
|
const owner = timer.userEmail === user.userId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal>
|
<Modal>
|
||||||
<FullPageTimer eventTime={timer.updatedAt} caption={timer.caption} />
|
<FullPageTimer
|
||||||
|
eventTime={timer.updatedAt}
|
||||||
|
caption={timer.caption}
|
||||||
|
owner={owner}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useAuth, useUser } from "@clerk/nextjs";
|
||||||
import posthog from "posthog-js";
|
import posthog from "posthog-js";
|
||||||
import { PostHogProvider } from "posthog-js/react";
|
import { PostHogProvider } from "posthog-js/react";
|
||||||
|
|
||||||
|
|
@ -9,6 +11,28 @@ if (typeof window !== "undefined") {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PostHogAuthWrapper({ children }: { children: React.ReactNode }) {
|
||||||
|
const auth = useAuth();
|
||||||
|
const userInfo = useUser();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userInfo.user) {
|
||||||
|
posthog.identify(userInfo.user.id, {
|
||||||
|
email: userInfo.user.emailAddresses[0]?.emailAddress,
|
||||||
|
name: userInfo.user.fullName,
|
||||||
|
});
|
||||||
|
} else if (!auth.isSignedIn) {
|
||||||
|
posthog.reset();
|
||||||
|
}
|
||||||
|
}, [auth, userInfo]);
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
export function CSPostHogProvider({ children }: { children: React.ReactNode }) {
|
export function CSPostHogProvider({ children }: { children: React.ReactNode }) {
|
||||||
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
|
return (
|
||||||
|
<PostHogProvider client={posthog}>
|
||||||
|
<PostHogAuthWrapper>{children}</PostHogAuthWrapper>
|
||||||
|
</PostHogProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ const calculateTimeSince = (start: Date | null) => {
|
||||||
export default function Timer(props: {
|
export default function Timer(props: {
|
||||||
eventTime: Date | null;
|
eventTime: Date | null;
|
||||||
caption: string;
|
caption: string;
|
||||||
|
owner: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [timeSince, setTimeSince] = useState(
|
const [timeSince, setTimeSince] = useState(
|
||||||
calculateTimeSince(props.eventTime),
|
calculateTimeSince(props.eventTime),
|
||||||
|
|
@ -53,7 +54,7 @@ export default function Timer(props: {
|
||||||
const formattedTime = formatTime(timeSince);
|
const formattedTime = formatTime(timeSince);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col justify-center">
|
<div className="flex h-full w-full flex-col justify-center text-center">
|
||||||
<div className="text-center text-2xl text-gray-300">
|
<div className="text-center text-2xl text-gray-300">
|
||||||
<p>It has been</p>
|
<p>It has been</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -91,9 +92,10 @@ export default function Timer(props: {
|
||||||
<div className="text-md px-1 text-gray-400">SECONDS</div>
|
<div className="text-md px-1 text-gray-400">SECONDS</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-2xl text-gray-300">
|
<div className="text-2xl text-gray-300">
|
||||||
<p>{props.caption}</p>
|
<p>{props.caption}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="my-8">I am the owner</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,47 @@
|
||||||
import { SignInButton, SignedIn, SignedOut, UserButton } from "@clerk/nextjs";
|
import { SignInButton, SignedIn, SignedOut, UserButton } from "@clerk/nextjs";
|
||||||
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
|
NavigationMenuItem,
|
||||||
NavigationMenuList,
|
NavigationMenuList,
|
||||||
} from "~/components/ui/navigation-menu";
|
NavigationMenuLink,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
} from "@/components/ui/navigation-menu";
|
||||||
|
|
||||||
export function TopNav() {
|
export function TopNav() {
|
||||||
return (
|
return (
|
||||||
<nav className="flex w-full items-center justify-between border-b px-8 py-4 text-xl font-semibold">
|
<nav className="flex w-full items-center justify-between border-b px-8 py-4 text-xl font-semibold">
|
||||||
|
<NavigationMenu className="gap-4">
|
||||||
|
<Link href="/">
|
||||||
<div>Days Since</div>
|
<div>Days Since</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<NavigationMenu>
|
<SignedIn>
|
||||||
<NavigationMenuList>
|
<NavigationMenuList>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
|
href="/timer"
|
||||||
|
>
|
||||||
|
My Timers
|
||||||
|
</NavigationMenuLink>
|
||||||
|
<NavigationMenuLink
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
|
href="/timer/create"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
</SignedIn>
|
||||||
|
</NavigationMenu>
|
||||||
|
|
||||||
<SignedOut>
|
<SignedOut>
|
||||||
<SignInButton mode="modal" />
|
<SignInButton mode="modal" />
|
||||||
</SignedOut>
|
</SignedOut>
|
||||||
<SignedIn>
|
<SignedIn>
|
||||||
<UserButton showName />
|
<UserButton />
|
||||||
</SignedIn>
|
</SignedIn>
|
||||||
</NavigationMenuList>
|
|
||||||
</NavigationMenu>
|
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import {
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "~/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Avatar, AvatarImage } from "~/components/ui/avatar";
|
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
|
||||||
const formatTime = (elapsed: number | null) => {
|
const formatTime = (elapsed: number | null) => {
|
||||||
if (elapsed === null) {
|
if (elapsed === null) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
// A faulty API route to test Sentry's error monitoring
|
|
||||||
export function GET() {
|
|
||||||
throw new Error("Sentry Example API Route Error");
|
|
||||||
return NextResponse.json({ data: "Testing Sentry Error..." });
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +1,14 @@
|
||||||
import { updateTimerTime } from "~/server/queries";
|
import { updateTimerTime } from "@/server/queries";
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
_: Request,
|
_: Request,
|
||||||
{ params }: { params: Promise<{ id: string }> },
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
) {
|
) {
|
||||||
const idAsNumber = parseInt((await params).id);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated = await updateTimerTime(idAsNumber);
|
await updateTimerTime((await params).id);
|
||||||
|
|
||||||
return Response.json({}, { status: 200 });
|
return Response.json({}, { status: 200 });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const typedError = error as Error;
|
|
||||||
|
|
||||||
if (error.message === "Timer not found") {
|
if (error.message === "Timer not found") {
|
||||||
return Response.json({}, { status: 404 });
|
return Response.json({}, { status: 404 });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import FullPageTimer from "~/app/_components/full-page-timer";
|
import FullPageTimer from "@/app/_components/full-page-timer";
|
||||||
import { getTimer } from "~/server/queries";
|
import { getTimer } from "@/server/queries";
|
||||||
|
|
||||||
export default async function TimerPage({
|
export default async function TimerPage({
|
||||||
params,
|
params,
|
||||||
|
|
|
||||||
7
src/app/timer/create/create-timer.tsx
Normal file
7
src/app/timer/create/create-timer.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { createTimer } from "@/server/queries";
|
||||||
|
|
||||||
|
export async function handleCreateTimer(caption: string, isPublic: boolean) {
|
||||||
|
return await createTimer(caption, isPublic);
|
||||||
|
}
|
||||||
83
src/app/timer/create/page.tsx
Normal file
83
src/app/timer/create/page.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { handleCreateTimer } from "./create-timer";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
caption: z.string().min(2, {
|
||||||
|
message: "Username must be at least 2 characters.",
|
||||||
|
}),
|
||||||
|
public: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function ProfileForm() {
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
caption: "",
|
||||||
|
public: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
await handleCreateTimer(values.caption, values.public);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center justify-center">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="flex w-3/12 flex-col gap-4 border p-12"
|
||||||
|
>
|
||||||
|
<div className="text-2xl">Create a new timer</div>
|
||||||
|
<hr className="mb-4" />
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="caption"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-lg">Caption</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Caption" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="public"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="text-lg">Public</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" className="my-4">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/app/timer/page.tsx
Normal file
48
src/app/timer/page.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Timer from "~/app/_components/timer";
|
||||||
|
import { getAllMyTimers } from "~/server/queries";
|
||||||
|
|
||||||
|
const hashEmail = (email: string) => {
|
||||||
|
const hash = createHash("sha256")
|
||||||
|
.update(email.trim().toLowerCase())
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function HomePage() {
|
||||||
|
const timers = await getAllMyTimers();
|
||||||
|
|
||||||
|
const modulo = timers.length > 4 ? Math.floor(Math.random() * 4) + 2 : 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"m-4 grid grid-cols-4 gap-4"}>
|
||||||
|
{timers.map((timer, idx) =>
|
||||||
|
idx % modulo == 0 ? (
|
||||||
|
<div key={timer.id} className="row-span-2">
|
||||||
|
<Link href={`/timer/${timer.id}`}>
|
||||||
|
<Timer
|
||||||
|
eventTime={timer.updatedAt}
|
||||||
|
caption={timer.caption}
|
||||||
|
email={timer.userEmail}
|
||||||
|
emailHash={hashEmail(timer.userEmail)}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div key={timer.id} className="row-span-1">
|
||||||
|
<Link href={`/timer/${timer.id}`}>
|
||||||
|
<Timer
|
||||||
|
eventTime={timer.updatedAt}
|
||||||
|
caption={timer.caption}
|
||||||
|
email={timer.userEmail}
|
||||||
|
emailHash={hashEmail(timer.userEmail)}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
const Avatar = React.forwardRef<
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
|
|
@ -13,12 +13,12 @@ const Avatar = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||||
|
|
||||||
const AvatarImage = React.forwardRef<
|
const AvatarImage = React.forwardRef<
|
||||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||||
|
|
@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef<
|
||||||
className={cn("aspect-square h-full w-full", className)}
|
className={cn("aspect-square h-full w-full", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||||
|
|
||||||
const AvatarFallback = React.forwardRef<
|
const AvatarFallback = React.forwardRef<
|
||||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||||
|
|
@ -40,11 +40,11 @@ const AvatarFallback = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
|
|
|
||||||
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { Slot } 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 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring 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 shadow hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
const Card = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
|
@ -10,12 +10,12 @@ const Card = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl border bg-card text-card-foreground shadow",
|
"rounded-xl border bg-card text-card-foreground shadow",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Card.displayName = "Card"
|
Card.displayName = "Card";
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
const CardHeader = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
|
@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardHeader.displayName = "CardHeader"
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
const CardTitle = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
|
@ -38,8 +38,8 @@ const CardTitle = React.forwardRef<
|
||||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardTitle.displayName = "CardTitle"
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
const CardDescription = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
|
@ -50,16 +50,16 @@ const CardDescription = React.forwardRef<
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardDescription.displayName = "CardDescription"
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
const CardContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
))
|
));
|
||||||
CardContent.displayName = "CardContent"
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
const CardFooter = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
|
@ -70,7 +70,14 @@ const CardFooter = React.forwardRef<
|
||||||
className={cn("flex items-center p-6 pt-0", className)}
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CardFooter.displayName = "CardFooter"
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
|
|
|
||||||
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 }
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
||||||
const DialogPortal = DialogPrimitive.Portal
|
const DialogPortal = DialogPrimitive.Portal;
|
||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
|
@ -22,12 +22,12 @@ const DialogOverlay = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
|
@ -39,7 +39,7 @@ const DialogContent = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
))
|
));
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
className,
|
className,
|
||||||
|
|
@ -60,12 +60,12 @@ const DialogHeader = ({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
DialogHeader.displayName = "DialogHeader"
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
const DialogFooter = ({
|
const DialogFooter = ({
|
||||||
className,
|
className,
|
||||||
|
|
@ -74,12 +74,12 @@ const DialogFooter = ({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
DialogFooter.displayName = "DialogFooter"
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
|
@ -89,12 +89,12 @@ const DialogTitle = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-lg font-semibold leading-none tracking-tight",
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
const DialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
|
@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef<
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -119,4 +119,4 @@ export {
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
}
|
};
|
||||||
|
|
|
||||||
179
src/components/ui/form.tsx
Normal file
179
src/components/ui/form.tsx
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
ControllerProps,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
const Form = FormProvider;
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
|
const itemContext = React.useContext(FormItemContext);
|
||||||
|
const { getFieldState, formState } = useFormContext();
|
||||||
|
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const FormItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const id = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormItem.displayName = "FormItem";
|
||||||
|
|
||||||
|
const FormLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(error && "text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormLabel.displayName = "FormLabel";
|
||||||
|
|
||||||
|
const FormControl = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Slot>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Slot>
|
||||||
|
>(({ ...props }, ref) => {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
|
useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
ref={ref}
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormControl.displayName = "FormControl";
|
||||||
|
|
||||||
|
const FormDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormDescription.displayName = "FormDescription";
|
||||||
|
|
||||||
|
const FormMessage = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, children, ...props }, ref) => {
|
||||||
|
const { error, formMessageId } = useFormField();
|
||||||
|
const body = error ? String(error?.message) : children;
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
FormMessage.displayName = "FormMessage";
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
};
|
||||||
22
src/components/ui/input.tsx
Normal file
22
src/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
export { Input };
|
||||||
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||||
|
);
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Label };
|
||||||
|
|
@ -3,7 +3,7 @@ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||||
import { cva } from "class-variance-authority";
|
import { cva } from "class-variance-authority";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const NavigationMenu = React.forwardRef<
|
const NavigationMenu = React.forwardRef<
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||||
|
|
|
||||||
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,77 @@
|
||||||
import "server-only";
|
import "server-only";
|
||||||
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, or } from "drizzle-orm";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { timers } from "./db/schema";
|
import { timers } from "./db/schema";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import { auth, currentUser } from "@clerk/nextjs/server";
|
||||||
|
|
||||||
|
type NewTimer = typeof timers.$inferInsert;
|
||||||
|
|
||||||
export async function getTimer(id: string) {
|
export async function getTimer(id: string) {
|
||||||
|
const user = await auth();
|
||||||
|
|
||||||
const timer = await db.query.timers.findFirst({
|
const timer = await db.query.timers.findFirst({
|
||||||
where: (model, { and, eq }) =>
|
where: (model, { and, eq, or }) =>
|
||||||
and(eq(model.id, id), eq(model.public, true)),
|
and(
|
||||||
|
eq(model.id, id),
|
||||||
|
or(eq(model.public, true), eq(model.userId, user.userId ?? "")),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!timer) notFound();
|
if (!timer) notFound();
|
||||||
|
|
||||||
if (!timer.public) notFound();
|
|
||||||
|
|
||||||
return timer;
|
return timer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllTimers() {
|
export async function getAllTimers() {
|
||||||
|
const user = await auth();
|
||||||
|
|
||||||
const timers = db.query.timers
|
const timers = db.query.timers
|
||||||
.findMany({
|
.findMany({
|
||||||
where: (model, { eq }) => eq(model.public, true),
|
where: (model) =>
|
||||||
|
or(eq(model.public, true), eq(model.userId, user.userId ?? "")),
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return timers;
|
return timers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAllMyTimers() {
|
||||||
|
const user = await auth();
|
||||||
|
if (!user.userId) throw new Error("Not authenticated");
|
||||||
|
|
||||||
|
const timers = db.query.timers
|
||||||
|
.findMany({
|
||||||
|
where: (model, { eq }) => eq(model.userId, user.userId),
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return timers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTimer(caption: string, isPublic: boolean) {
|
||||||
|
const user = await auth();
|
||||||
|
if (!user.userId) throw new Error("Not authenticated");
|
||||||
|
|
||||||
|
const userInfo = await currentUser();
|
||||||
|
|
||||||
|
const insertTimer = (timer: NewTimer) => {
|
||||||
|
return db.insert(timers).values(timer).returning({ id: timers.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
const [timer] = await insertTimer({
|
||||||
|
userId: user.userId,
|
||||||
|
userEmail: userInfo?.emailAddresses[0]?.emailAddress ?? "",
|
||||||
|
caption,
|
||||||
|
public: isPublic,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!timer) throw new Error("Timer not created");
|
||||||
|
|
||||||
|
redirect(`/timer/${timer.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateTimerTime(id: string) {
|
export async function updateTimerTime(id: string) {
|
||||||
const updated = await db
|
const updated = await db
|
||||||
.update(timers)
|
.update(timers)
|
||||||
|
|
|
||||||
|
|
@ -4,57 +4,55 @@
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 240 10% 3.9%;
|
--foreground: 0 0% 3.9%;
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 240 10% 3.9%;
|
--card-foreground: 0 0% 3.9%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 240 10% 3.9%;
|
--popover-foreground: 0 0% 3.9%;
|
||||||
--primary: 240 5.9% 10%;
|
--primary: 0 0% 9%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--primary-foreground: 0 0% 98%;
|
||||||
--secondary: 240 4.8% 95.9%;
|
--secondary: 0 0% 96.1%;
|
||||||
--secondary-foreground: 240 5.9% 10%;
|
--secondary-foreground: 0 0% 9%;
|
||||||
--muted: 240 4.8% 95.9%;
|
--muted: 0 0% 96.1%;
|
||||||
--muted-foreground: 240 3.8% 46.1%;
|
--muted-foreground: 0 0% 45.1%;
|
||||||
--accent: 240 4.8% 95.9%;
|
--accent: 0 0% 96.1%;
|
||||||
--accent-foreground: 240 5.9% 10%;
|
--accent-foreground: 0 0% 9%;
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 240 5.9% 90%;
|
--border: 0 0% 89.8%;
|
||||||
--input: 240 5.9% 90%;
|
--input: 0 0% 89.8%;
|
||||||
--ring: 240 10% 3.9%;
|
--ring: 0 0% 3.9%;
|
||||||
--chart-1: 12 76% 61%;
|
--chart-1: 12 76% 61%;
|
||||||
--chart-2: 173 58% 39%;
|
--chart-2: 173 58% 39%;
|
||||||
--chart-3: 197 37% 24%;
|
--chart-3: 197 37% 24%;
|
||||||
--chart-4: 43 74% 66%;
|
--chart-4: 43 74% 66%;
|
||||||
--chart-5: 27 87% 67%;
|
--chart-5: 27 87% 67%;
|
||||||
--radius: 0.5rem
|
--radius: 0.5rem}
|
||||||
}
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 240 10% 3.9%;
|
--background: 0 0% 3.9%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 98%;
|
||||||
--card: 240 10% 3.9%;
|
--card: 0 0% 3.9%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
--popover: 240 10% 3.9%;
|
--popover: 0 0% 3.9%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
--primary: 0 0% 98%;
|
--primary: 0 0% 98%;
|
||||||
--primary-foreground: 240 5.9% 10%;
|
--primary-foreground: 0 0% 9%;
|
||||||
--secondary: 240 3.7% 15.9%;
|
--secondary: 0 0% 14.9%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--muted: 240 3.7% 15.9%;
|
--muted: 0 0% 14.9%;
|
||||||
--muted-foreground: 240 5% 64.9%;
|
--muted-foreground: 0 0% 63.9%;
|
||||||
--accent: 240 3.7% 15.9%;
|
--accent: 0 0% 14.9%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 0 0% 98%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 240 3.7% 15.9%;
|
--border: 0 0% 14.9%;
|
||||||
--input: 240 3.7% 15.9%;
|
--input: 0 0% 14.9%;
|
||||||
--ring: 240 4.9% 83.9%;
|
--ring: 0 0% 83.1%;
|
||||||
--chart-1: 220 70% 50%;
|
--chart-1: 220 70% 50%;
|
||||||
--chart-2: 160 60% 45%;
|
--chart-2: 160 60% 45%;
|
||||||
--chart-3: 30 80% 55%;
|
--chart-3: 30 80% 55%;
|
||||||
--chart-4: 280 65% 60%;
|
--chart-4: 280 65% 60%;
|
||||||
--chart-5: 340 75% 55%
|
--chart-5: 340 75% 55%}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
/* Path Aliases */
|
/* Path Aliases */
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Reference in a new issue