refined infinite article grid and added article filtering
This commit is contained in:
parent
37233db0ec
commit
0f7bf09676
@ -29,6 +29,7 @@
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
@ -45,7 +46,6 @@
|
||||
"@trpc/client": "^11.0.0-rc.446",
|
||||
"@trpc/react-query": "^11.0.0-rc.446",
|
||||
"@trpc/server": "^11.0.0-rc.446",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
|
||||
"cheerio": "^1.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
148
pnpm-lock.yaml
generated
148
pnpm-lock.yaml
generated
@ -35,6 +35,9 @@ importers:
|
||||
'@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)
|
||||
'@radix-ui/react-dropdown-menu':
|
||||
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-label':
|
||||
specifier: ^2.1.2
|
||||
version: 2.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)
|
||||
@ -83,9 +86,6 @@ importers:
|
||||
'@trpc/server':
|
||||
specifier: ^11.0.0-rc.446
|
||||
version: 11.0.0-rc.824(typescript@5.8.2)
|
||||
babel-plugin-react-compiler:
|
||||
specifier: 19.0.0-beta-40c6c23-20250301
|
||||
version: 19.0.0-beta-40c6c23-20250301
|
||||
cheerio:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
@ -103,16 +103,16 @@ importers:
|
||||
version: 0.33.0(@types/react@18.3.18)(postgres@3.4.5)(react@18.3.1)
|
||||
geist:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.1(next@15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
version: 1.3.1(next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
lucide-react:
|
||||
specifier: ^0.477.0
|
||||
version: 0.477.0(react@18.3.1)
|
||||
next:
|
||||
specifier: ^15.0.1
|
||||
version: 15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
version: 15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-auth:
|
||||
specifier: 5.0.0-beta.25
|
||||
version: 5.0.0-beta.25(next@15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
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)
|
||||
@ -248,22 +248,10 @@ packages:
|
||||
'@auth/drizzle-adapter@1.8.0':
|
||||
resolution: {integrity: sha512-cxApE0h5WcyDsgGix9hzmWmCz0qxvmMJexAOQmI6R/YXYxrZ/mKBKu0BlfgQBR6z2XvNWl4wbEGchwSenSCksQ==}
|
||||
|
||||
'@babel/helper-string-parser@7.25.9':
|
||||
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.25.9':
|
||||
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.26.9':
|
||||
resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.26.9':
|
||||
resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@cfcs/core@0.0.6':
|
||||
resolution: {integrity: sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==}
|
||||
|
||||
@ -1185,6 +1173,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dropdown-menu@2.1.6':
|
||||
resolution: {integrity: sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-focus-guards@1.0.1':
|
||||
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
|
||||
peerDependencies:
|
||||
@ -1260,6 +1261,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-menu@2.1.6':
|
||||
resolution: {integrity: sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==}
|
||||
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-popover@1.1.6':
|
||||
resolution: {integrity: sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==}
|
||||
peerDependencies:
|
||||
@ -1364,6 +1378,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.2':
|
||||
resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==}
|
||||
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-select@2.1.6':
|
||||
resolution: {integrity: sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==}
|
||||
peerDependencies:
|
||||
@ -2070,9 +2097,6 @@ packages:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301:
|
||||
resolution: {integrity: sha512-himtjPafvMbA7PYnV2L+jprpB3h4rhx/n5s4L3gC654FOUsmsv5n4p8d6ufvK2zqUQs4kTOjgT2b4wnuDU32CA==}
|
||||
|
||||
bail@2.0.2:
|
||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||
|
||||
@ -4295,19 +4319,10 @@ snapshots:
|
||||
- '@simplewebauthn/server'
|
||||
- nodemailer
|
||||
|
||||
'@babel/helper-string-parser@7.25.9': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.25.9': {}
|
||||
|
||||
'@babel/runtime@7.26.9':
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/types@7.26.9':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.25.9
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
|
||||
'@cfcs/core@0.0.6':
|
||||
dependencies:
|
||||
'@egjs/component': 3.0.5
|
||||
@ -4975,6 +4990,21 @@ snapshots:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-dropdown-menu@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/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-id': 1.1.0(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-menu': 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-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)
|
||||
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-focus-guards@1.0.1(@types/react@18.3.18)(react@18.3.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.9
|
||||
@ -5035,6 +5065,32 @@ snapshots:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-menu@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/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-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-roving-focus': 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-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)
|
||||
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-popover@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)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
@ -5136,6 +5192,23 @@ snapshots:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-roving-focus@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/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-id': 1.1.0(@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-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)
|
||||
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-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
|
||||
@ -5871,10 +5944,6 @@ snapshots:
|
||||
|
||||
axobject-query@4.1.0: {}
|
||||
|
||||
babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301:
|
||||
dependencies:
|
||||
'@babel/types': 7.26.9
|
||||
|
||||
bail@2.0.2: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
@ -6672,9 +6741,9 @@ snapshots:
|
||||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
geist@1.3.1(next@15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
|
||||
geist@1.3.1(next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
|
||||
dependencies:
|
||||
next: 15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next: 15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
gesto@1.19.4:
|
||||
dependencies:
|
||||
@ -7355,10 +7424,10 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
next-auth@5.0.0-beta.25(next@15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
|
||||
next-auth@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):
|
||||
dependencies:
|
||||
'@auth/core': 0.37.2
|
||||
next: 15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
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):
|
||||
@ -7366,7 +7435,7 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
next@15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@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
|
||||
'@swc/counter': 0.1.3
|
||||
@ -7386,7 +7455,6 @@ snapshots:
|
||||
'@next/swc-linux-x64-musl': 15.2.1
|
||||
'@next/swc-win32-arm64-msvc': 15.2.1
|
||||
'@next/swc-win32-x64-msvc': 15.2.1
|
||||
babel-plugin-react-compiler: 19.0.0-beta-40c6c23-20250301
|
||||
sharp: 0.33.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
|
||||
@ -2,21 +2,39 @@ import "dotenv/config";
|
||||
import { db, DBType } from "../src/server/db";
|
||||
import { articles, categories, users } from "../src/server/db/schema";
|
||||
import fakeArticles from "./fake-articles.json";
|
||||
import fakeUsers from "./fake-users.json";
|
||||
import fakeCategories from "./fake-categories.json";
|
||||
import { generateSlug } from "@/lib/utils";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
|
||||
async function seed() {
|
||||
// await db.insert(users).values(fakeUsers);
|
||||
// await db
|
||||
// .insert(categories)
|
||||
// .values(
|
||||
// fakeCategories.map(({ name }) => ({ name, slug: generateSlug(name) })),
|
||||
// );
|
||||
const u = await db
|
||||
.insert(users)
|
||||
.values(fakeUsers)
|
||||
.returning({ id: users.id });
|
||||
console.log("Seeded " + u.length + " users");
|
||||
|
||||
await db
|
||||
const c = await db
|
||||
.insert(categories)
|
||||
.values(
|
||||
fakeCategories.map(({ name }) => ({ name, slug: generateSlug(name) })),
|
||||
)
|
||||
.returning({
|
||||
id: categories.id,
|
||||
});
|
||||
console.log("Seeded " + c.length + " categories");
|
||||
|
||||
const a = await db
|
||||
.insert(articles)
|
||||
.values(fakeArticles.map(({ title }) => ({ title, slug: createId() })));
|
||||
.values(
|
||||
fakeArticles.map(({ title }) => ({
|
||||
title,
|
||||
slug: createId(),
|
||||
published: true,
|
||||
})),
|
||||
)
|
||||
.returning({ id: articles.id });
|
||||
console.log("Seeded " + a.length + " articles");
|
||||
}
|
||||
|
||||
seed()
|
||||
|
||||
@ -9,7 +9,7 @@ import ArticleGrid, {
|
||||
import { Article, Category } from "@/server/db/schema";
|
||||
import ArrowLink from "@/components/arrow-link";
|
||||
import { appRoutes } from "@/config";
|
||||
import GlobalSearchWidget from "@/components/global-search-widget";
|
||||
import GlobalSearchWidget from "@/components/article/article-filter-bar";
|
||||
import { api } from "@/trpc/react";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { WikiSidebar } from "@/components/layout/wiki-sidebar";
|
||||
import { AppSidebar } from "@/components/layout/app-sidebar";
|
||||
import Navbar from "@/components/layout/navbar";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { auth } from "@/server/auth";
|
||||
import React from "react";
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
async function Layout({ children }: { children: React.ReactNode }) {
|
||||
const session = await auth();
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<WikiSidebar />
|
||||
<div className="w-full">
|
||||
<AppSidebar user={session?.user} />
|
||||
<div className="h-screen w-full bg-background">
|
||||
<Navbar />
|
||||
<main className="space-y-4 p-4">{children}</main>
|
||||
</div>
|
||||
|
||||
@ -5,6 +5,7 @@ import { type Metadata } from "next";
|
||||
|
||||
import { TRPCReactProvider } from "@/trpc/react";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create T3 App",
|
||||
@ -16,11 +17,22 @@ export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en" className={`${GeistSans.variable}`}>
|
||||
<html
|
||||
suppressHydrationWarning
|
||||
lang="en"
|
||||
className={`${GeistSans.variable}`}
|
||||
>
|
||||
<body>
|
||||
<TRPCReactProvider>
|
||||
<Toaster />
|
||||
{children}
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Toaster />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</TRPCReactProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
120
src/components/article/article-filter-bar.tsx
Normal file
120
src/components/article/article-filter-bar.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
import { cn, debounce } from "@/lib/utils";
|
||||
import React from "react";
|
||||
import { Input } from "../ui/input";
|
||||
import {
|
||||
ArrowDownAZ,
|
||||
ArrowUpAz,
|
||||
CalendarArrowDown,
|
||||
CalendarArrowUp,
|
||||
Eye,
|
||||
FilterIcon,
|
||||
LucideIcon,
|
||||
MessageSquare,
|
||||
SearchIcon,
|
||||
} from "lucide-react";
|
||||
import CategorySelect from "../category/category-select";
|
||||
import { Combobox } from "../combobox";
|
||||
|
||||
export type ArticleFilter = {
|
||||
query?: string;
|
||||
category?: string;
|
||||
sort?: string;
|
||||
};
|
||||
|
||||
function ArticleFilterBar({
|
||||
className,
|
||||
onFilterUpdate,
|
||||
}: {
|
||||
className?: string;
|
||||
onFilterUpdate: (filter: ArticleFilter) => void;
|
||||
}) {
|
||||
const [filter, setFilter] = React.useState<ArticleFilter>({
|
||||
query: "",
|
||||
});
|
||||
|
||||
const onFilterChange = React.useCallback(
|
||||
(newFilter: Partial<ArticleFilter>, notify: boolean = true) => {
|
||||
const f = { ...filter, ...newFilter };
|
||||
setFilter(f);
|
||||
if (notify) onFilterUpdate(f);
|
||||
},
|
||||
[filter, onFilterUpdate], // Ensure dependencies are listed
|
||||
);
|
||||
|
||||
const debouncedSearch = React.useMemo(
|
||||
() =>
|
||||
debounce((query: string) => {
|
||||
onFilterChange({ query });
|
||||
}, 300),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-center gap-2 rounded-md border bg-background p-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full">
|
||||
<SearchIcon className="absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
className="border-0 p-0 pl-8 shadow-none focus-visible:ring-0"
|
||||
value={filter.query}
|
||||
onChange={(e) => {
|
||||
onFilterChange({ query: e.currentTarget.value }, false);
|
||||
debouncedSearch(e.currentTarget.value);
|
||||
}}
|
||||
placeholder="Artikel Suche..."
|
||||
/>
|
||||
</div>
|
||||
<CategorySelect
|
||||
className="w-full max-w-xs"
|
||||
onSelect={(category) => {
|
||||
onFilterChange({
|
||||
category: category?.length ? category : undefined,
|
||||
});
|
||||
}}
|
||||
buttonProps={{
|
||||
size: "sm",
|
||||
}}
|
||||
/>
|
||||
<Combobox
|
||||
hideSearch
|
||||
data={sortItems}
|
||||
initialValue={filter.sort}
|
||||
onSelect={(currentValue) => {
|
||||
onFilterChange({
|
||||
sort: currentValue?.length ? currentValue : undefined,
|
||||
});
|
||||
}}
|
||||
className="w-full max-w-64"
|
||||
messageUi={{
|
||||
selectIcon: FilterIcon,
|
||||
select: "Sortieren",
|
||||
}}
|
||||
buttonProps={{
|
||||
size: "sm",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArticleFilterBar;
|
||||
|
||||
const sortItems: Array<{
|
||||
Icon: LucideIcon;
|
||||
value: string;
|
||||
label: string;
|
||||
}> = [
|
||||
{ Icon: CalendarArrowUp, value: "newest", label: "Neueste" },
|
||||
{ Icon: CalendarArrowDown, value: "oldest", label: "Älteste" },
|
||||
{ Icon: ArrowDownAZ, value: "abc", label: "Alphabetisch A-Z" },
|
||||
{ Icon: ArrowUpAz, value: "cba", label: "Alphabetisch Z-A" },
|
||||
// { Icon: Eye, value: "popular", label: "Beliebteste" },
|
||||
// { Icon: MessageSquare, value: "commented", label: "Meistkommentiert" },
|
||||
];
|
||||
@ -26,7 +26,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { createArticleSchema } from "@/lib/validation/zod/article";
|
||||
import { createArticle } from "@/server/actions/article";
|
||||
|
||||
function CreateArticleDialog() {
|
||||
function CreateArticleDialog({ cb }: { cb?: () => void }) {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const form = useForm<z.infer<typeof createArticleSchema>>({
|
||||
resolver: zodResolver(createArticleSchema),
|
||||
@ -38,6 +38,7 @@ function CreateArticleDialog() {
|
||||
// 2. Define a submit handler.
|
||||
async function onSubmit(values: z.infer<typeof createArticleSchema>) {
|
||||
setOpen(false);
|
||||
cb?.();
|
||||
await createArticle(values);
|
||||
}
|
||||
return (
|
||||
|
||||
@ -6,11 +6,17 @@ import { ARTICLE_GRID_CLASS } from "./article-grid";
|
||||
import ArticleCard from "../article-card";
|
||||
import { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import ArticleFilterBar, { ArticleFilter } from "../article-filter-bar";
|
||||
|
||||
function InfiniteArticlesGrid() {
|
||||
const [filter, setFilter] = React.useState<ArticleFilter | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
|
||||
api.article.getByPage.useInfiniteQuery(
|
||||
{},
|
||||
api.article.getByCursor.useInfiniteQuery(
|
||||
{
|
||||
filter,
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
},
|
||||
@ -31,7 +37,8 @@ function InfiniteArticlesGrid() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative space-y-4">
|
||||
<ArticleFilterBar onFilterUpdate={setFilter} />
|
||||
<menu className={`${ARTICLE_GRID_CLASS} overflow-auto`}>
|
||||
{data?.pages?.length
|
||||
? allItems.map((article, idx) => (
|
||||
|
||||
@ -16,7 +16,7 @@ function Avatar({
|
||||
}) {
|
||||
return (
|
||||
<AvatarComponent className={className}>
|
||||
<AvatarImage src={src!} />
|
||||
<AvatarImage src={src!} alt={fb!} />
|
||||
<AvatarFallback>{fb?.slice(0, 2)?.toUpperCase()}</AvatarFallback>
|
||||
</AvatarComponent>
|
||||
);
|
||||
|
||||
@ -1,33 +1,24 @@
|
||||
"use client";
|
||||
import { api } from "@/trpc/react";
|
||||
import React from "react";
|
||||
import { Combobox } from "../combobox";
|
||||
import { Combobox, ComboboxProps } from "../combobox";
|
||||
import { Icons } from "../icons";
|
||||
|
||||
function CategorySelect({
|
||||
initialValue,
|
||||
onSelect,
|
||||
}: {
|
||||
initialValue?: string;
|
||||
onSelect: (category?: string) => void;
|
||||
}) {
|
||||
function CategorySelect(props: Partial<ComboboxProps>) {
|
||||
const { data: categories } = api.category.getAll.useQuery();
|
||||
return (
|
||||
<div>
|
||||
<Combobox
|
||||
data={
|
||||
categories?.map(({ name, id }) => ({ label: name!, value: id! })) ??
|
||||
[]
|
||||
}
|
||||
initialValue={initialValue}
|
||||
onSelect={onSelect}
|
||||
className="w-full"
|
||||
messageUi={{
|
||||
select: "Kategorie auswählen...",
|
||||
placeholder: "Kategorie suchen...",
|
||||
empty: "Keine Kategorien gefunden",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Combobox
|
||||
{...(props as ComboboxProps)}
|
||||
messageUi={{
|
||||
select: "Kategorie auswählen...",
|
||||
selectIcon: Icons.category,
|
||||
placeholder: "Kategorie suchen...",
|
||||
empty: "Keine Kategorien gefunden",
|
||||
}}
|
||||
data={
|
||||
categories?.map(({ name, id }) => ({ label: name!, value: id! })) ?? []
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ const formSchema = categorySchema.pick({
|
||||
name: true,
|
||||
});
|
||||
|
||||
function CreateCategoryDialog() {
|
||||
function CreateCategoryDialog({ cb }: { cb?: () => void }) {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@ -42,6 +42,7 @@ function CreateCategoryDialog() {
|
||||
// 2. Define a submit handler.
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
setOpen(false);
|
||||
cb?.();
|
||||
await createCategory(values);
|
||||
}
|
||||
return (
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { Check, ChevronsUpDown, LucideIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button, ButtonProps } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@ -19,19 +19,23 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
type ComboboxProps = {
|
||||
export type ComboboxProps = {
|
||||
data: {
|
||||
label: string;
|
||||
value: string;
|
||||
Icon?: LucideIcon;
|
||||
}[];
|
||||
onSelect: (value: string) => void;
|
||||
onSelect: (value?: string) => void;
|
||||
messageUi?: {
|
||||
select?: string;
|
||||
selectIcon?: LucideIcon;
|
||||
empty?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
initialValue?: string;
|
||||
className?: string;
|
||||
hideSearch?: boolean;
|
||||
buttonProps?: ButtonProps;
|
||||
};
|
||||
|
||||
export function Combobox({
|
||||
@ -39,11 +43,13 @@ export function Combobox({
|
||||
initialValue,
|
||||
messageUi,
|
||||
className,
|
||||
hideSearch,
|
||||
buttonProps,
|
||||
onSelect,
|
||||
}: ComboboxProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [value, setValue] = React.useState(initialValue ?? "");
|
||||
|
||||
const selectedItem = data.find((item) => item.value === value)!;
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
@ -55,10 +61,27 @@ export function Combobox({
|
||||
"w-[200px] justify-between shadow-none hover:bg-background",
|
||||
className,
|
||||
)}
|
||||
{...buttonProps}
|
||||
>
|
||||
{value
|
||||
? data.find((item) => item.value === value)?.label
|
||||
: (messageUi?.select ?? "Select...")}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-muted-foreground",
|
||||
selectedItem && "text-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedItem?.Icon ? (
|
||||
<selectedItem.Icon className="size-4" />
|
||||
) : messageUi?.selectIcon ? (
|
||||
<messageUi.selectIcon className="size-4" />
|
||||
) : null}
|
||||
|
||||
<span>
|
||||
{selectedItem?.label
|
||||
? selectedItem.label
|
||||
: (messageUi?.select ?? "Select...")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@ -75,10 +98,12 @@ export function Combobox({
|
||||
: 0;
|
||||
}}
|
||||
>
|
||||
<CommandInput
|
||||
placeholder={messageUi?.placeholder ?? "Search..."}
|
||||
className="h-9"
|
||||
/>
|
||||
{!hideSearch && (
|
||||
<CommandInput
|
||||
placeholder={messageUi?.placeholder ?? "Search..."}
|
||||
className="h-9"
|
||||
/>
|
||||
)}
|
||||
<CommandList>
|
||||
<CommandEmpty>{messageUi?.empty ?? "Nothing found."}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
@ -87,16 +112,18 @@ export function Combobox({
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue);
|
||||
onSelect(currentValue);
|
||||
const newValue = currentValue === value ? "" : currentValue;
|
||||
setValue(newValue);
|
||||
onSelect(newValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{item?.Icon && <item.Icon className="ml-auto opacity-50" />}
|
||||
<span>{item.label}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{/* <span className="text-xs text-muted-foreground">
|
||||
{value === item.value && "ausgewählt"}
|
||||
</span>
|
||||
</span> */}
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
|
||||
36
src/components/editor-dropdown.tsx
Normal file
36
src/components/editor-dropdown.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import CreateArticleDialog from "@/components/article/create-article-dialog";
|
||||
import CreateCategoryDialog from "@/components/category/create-category-dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
function EditorDropdown() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant={"outline"}>
|
||||
<PlusIcon className="size-4" />
|
||||
<span>Erstellen</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-full max-w-48 space-y-2 bg-sidebar"
|
||||
align="end"
|
||||
>
|
||||
<CreateArticleDialog cb={() => setOpen(false)} />
|
||||
|
||||
<CreateCategoryDialog cb={() => setOpen(false)} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditorDropdown;
|
||||
@ -1,43 +0,0 @@
|
||||
"use client";
|
||||
import { cn, debounce } from "@/lib/utils";
|
||||
import React from "react";
|
||||
import { Input } from "./ui/input";
|
||||
|
||||
function GlobalSearchWidget({
|
||||
className,
|
||||
onDebouncedSearch,
|
||||
}: {
|
||||
className?: string;
|
||||
onDebouncedSearch: (query: string) => void;
|
||||
}) {
|
||||
const [query, setQuery] = React.useState("");
|
||||
|
||||
const debouncedSearch = React.useMemo(
|
||||
() =>
|
||||
debounce((q: string) => {
|
||||
onDebouncedSearch(q);
|
||||
}, 300),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-20 w-full items-center justify-center rounded-md bg-background p-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
className="shadow-none"
|
||||
value={query}
|
||||
onChange={(e) => {
|
||||
setQuery(e.currentTarget.value);
|
||||
debouncedSearch(e.currentTarget.value);
|
||||
}}
|
||||
placeholder="Suche Artikel oder Kategorien..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GlobalSearchWidget;
|
||||
@ -1,4 +1,4 @@
|
||||
import { LucideProps } from "lucide-react";
|
||||
import { Folder, LucideIcon, LucideProps, Newspaper } from "lucide-react";
|
||||
|
||||
export const Icons = {
|
||||
logo(props: LucideProps) {
|
||||
@ -22,4 +22,31 @@ export const Icons = {
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
|
||||
category: Folder,
|
||||
article: Newspaper,
|
||||
|
||||
// socials
|
||||
discord(props: LucideProps) {
|
||||
return (
|
||||
<svg
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 -28.5 256 256"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z"
|
||||
fill="#5865F2"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
105
src/components/layout/app-sidebar/index.tsx
Normal file
105
src/components/layout/app-sidebar/index.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import { Folder, LifeBuoy, Newspaper, Send } from "lucide-react";
|
||||
|
||||
import { NavMain } from "./nav-main";
|
||||
|
||||
import { NavSecondary } from "./nav-secondary";
|
||||
import { NavUser } from "./nav-user";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
import { User } from "next-auth";
|
||||
import NavTeamSection from "./nav-team-section";
|
||||
import NavBranding from "./nav-branding";
|
||||
import { Icons } from "@/components/icons";
|
||||
import { appConfig } from "@/config";
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "Artikel",
|
||||
url: "/artikel",
|
||||
isActive: true,
|
||||
icon: Newspaper,
|
||||
items: [
|
||||
{
|
||||
title: "Genesis",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Explorer",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Quantum",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Alle Artikel",
|
||||
url: "/artikel",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Kategorien",
|
||||
url: "/kategorien",
|
||||
icon: Folder,
|
||||
items: [
|
||||
{
|
||||
title: "History",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Starred",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "Alle Kategorien",
|
||||
url: "#",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
navSecondary: [
|
||||
{
|
||||
title: "Discord",
|
||||
url: appConfig.socials.discord,
|
||||
icon: Icons.discord,
|
||||
external: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function AppSidebar({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Sidebar> & { user?: User | null }) {
|
||||
return (
|
||||
<Sidebar variant="inset" className="border-r" {...props}>
|
||||
<SidebarHeader>
|
||||
<NavBranding subTitle="Wissen" />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
{props?.user && <NavTeamSection userRole={props.user.role} />}
|
||||
<NavUser user={props.user} />{" "}
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
31
src/components/layout/app-sidebar/nav-branding.tsx
Normal file
31
src/components/layout/app-sidebar/nav-branding.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Icons } from "@/components/icons";
|
||||
import { appConfig } from "@/config";
|
||||
import Link from "next/link";
|
||||
|
||||
function NavBranding({ subTitle }: { subTitle: string }) {
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<Link href="/">
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-foreground text-background">
|
||||
<Icons.logo className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{appConfig.name}</span>
|
||||
<span className="truncate text-xs">{subTitle}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavBranding;
|
||||
79
src/components/layout/app-sidebar/nav-main.tsx
Normal file
79
src/components/layout/app-sidebar/nav-main.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronRight, type LucideIcon } from "lucide-react";
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
icon: LucideIcon;
|
||||
isActive?: boolean;
|
||||
items?: {
|
||||
title: string;
|
||||
url: string;
|
||||
hideBorder?: boolean;
|
||||
}[];
|
||||
}[];
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<Collapsible key={item.title} asChild defaultOpen={item.isActive}>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip={item.title}>
|
||||
<a href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
{item.items?.length ? (
|
||||
<>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuAction className="data-[state=open]:rotate-90">
|
||||
<ChevronRight />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</SidebarMenuAction>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub className="">
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<a href={subItem.url}>
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</>
|
||||
) : null}
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
41
src/components/layout/app-sidebar/nav-secondary.tsx
Normal file
41
src/components/layout/app-sidebar/nav-secondary.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import * as React from "react";
|
||||
import { LucideProps, type LucideIcon } from "lucide-react";
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
export function NavSecondary({
|
||||
items,
|
||||
...props
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
icon: (props: LucideProps) => React.ReactNode;
|
||||
external?: boolean;
|
||||
}[];
|
||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||
return (
|
||||
<SidebarGroup {...props}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild size="sm">
|
||||
<a href={item.url} target={item.external ? "_blank" : "_self"}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
34
src/components/layout/app-sidebar/nav-team-section.tsx
Normal file
34
src/components/layout/app-sidebar/nav-team-section.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
function NavTeamSection({ userRole }: { userRole: UserRole }) {
|
||||
const isAdmin = hasPermission(userRole, Role.ADMIN);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAdmin && (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild>
|
||||
<Button
|
||||
asChild
|
||||
className="hover:bg-primary hover:text-primary-foreground"
|
||||
>
|
||||
<Link href={"/admin"}>Admin Dashboard</Link>
|
||||
</Button>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavTeamSection;
|
||||
114
src/components/layout/app-sidebar/nav-user.tsx
Normal file
114
src/components/layout/app-sidebar/nav-user.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { appRoutes } from "@/config";
|
||||
import Avatar from "@/components/avatar";
|
||||
|
||||
export function NavUser({ user }: { user?: any }) {
|
||||
const { isMobile } = useSidebar();
|
||||
if (!user)
|
||||
return (
|
||||
<Button asChild>
|
||||
<Link href={appRoutes.signin}>Anmelden</Link>
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar
|
||||
src={user.image}
|
||||
fb={user.name}
|
||||
className="h-8 w-8 rounded-lg"
|
||||
/>
|
||||
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar
|
||||
src={user.image}
|
||||
fb={user.name}
|
||||
className="h-8 w-8 rounded-lg"
|
||||
/>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
@ -1,65 +1,22 @@
|
||||
import React from "react";
|
||||
import { Input } from "../ui/input";
|
||||
import { auth } from "@/server/auth";
|
||||
import { Button } from "../ui/button";
|
||||
import Link from "next/link";
|
||||
import Avatar from "../avatar";
|
||||
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import CreateArticleDialog from "@/components/article/create-article-dialog";
|
||||
import CreateCategoryDialog from "@/components/category/create-category-dialog";
|
||||
import { appRoutes } from "@/config";
|
||||
|
||||
import { ModeToggle } from "../mode-switch";
|
||||
import EditorDropdown from "../editor-dropdown";
|
||||
|
||||
async function Navbar() {
|
||||
const session = await auth();
|
||||
const isEditor = session?.user
|
||||
? hasPermission(session.user.role, Role.EDITOR)
|
||||
: false;
|
||||
const isAdmin = session?.user
|
||||
? hasPermission(session.user.role, Role.ADMIN)
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="flex h-14 items-center justify-between border-b bg-sidebar px-4">
|
||||
<Input className="w-full max-w-xs" placeholder="Suche..." />
|
||||
<div className="flex h-14 items-center justify-end gap-4 border-b bg-background px-4">
|
||||
{isEditor && <EditorDropdown />}
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{isEditor && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button>Erstellen</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-full max-w-48 space-y-2 bg-sidebar"
|
||||
align="end"
|
||||
// side="left"
|
||||
>
|
||||
<CreateArticleDialog />
|
||||
<CreateCategoryDialog />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Button asChild>
|
||||
<Link href={appRoutes.admin.base}>Admin Dashboard</Link>
|
||||
</Button>
|
||||
)}
|
||||
{session ? (
|
||||
<Avatar
|
||||
className="size-8"
|
||||
src={session.user.image}
|
||||
fb={session.user.name}
|
||||
/>
|
||||
) : (
|
||||
<Button asChild>
|
||||
<Link href={"/api/auth/signin"}>Anmelden</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import Link from "next/link";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { Minus, Plus } from "lucide-react";
|
||||
import { api } from "@/trpc/server";
|
||||
import { appConfig } from "@/config/app.config";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "../ui/collapsible";
|
||||
import { appRoutes } from "@/config";
|
||||
import SidebarLink from "./sidebar-link";
|
||||
|
||||
export async function WikiSidebar({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Sidebar>) {
|
||||
const sidebarContent = await api.app.getSidebarContent();
|
||||
return (
|
||||
<Sidebar {...props}>
|
||||
<SidebarHeader className="flex h-14 items-center justify-center border-b">
|
||||
<Link href={"/"}>
|
||||
<span className="text-2xl font-bold">{appConfig.name}</span>
|
||||
</Link>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
{sidebarContent?.map((category, index) => (
|
||||
<Collapsible
|
||||
key={category.id}
|
||||
defaultOpen={index === 1}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton>
|
||||
<Link href={appRoutes.category(category.slug)}>
|
||||
<span>{category.name}</span>
|
||||
</Link>
|
||||
<Plus className="ml-auto group-data-[state=open]/collapsible:hidden" />
|
||||
<Minus className="ml-auto group-data-[state=closed]/collapsible:hidden" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
{category?.articles?.length ? (
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{category.articles.map((article) => (
|
||||
<SidebarMenuSubItem key={article.slug}>
|
||||
<SidebarLink
|
||||
title={article.title!}
|
||||
url={appRoutes.article(article.slug)}
|
||||
/>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
) : null}
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Link
|
||||
className="text-xs text-muted-foreground underline"
|
||||
href={"/impressum"}
|
||||
>
|
||||
Impressum
|
||||
</Link>
|
||||
<Separator orientation="vertical" />
|
||||
<Link
|
||||
href={"/datenschutz"}
|
||||
className="text-xs text-muted-foreground underline"
|
||||
>
|
||||
Datenschutz
|
||||
</Link>
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
40
src/components/mode-switch.tsx
Normal file
40
src/components/mode-switch.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Hell
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dunkel
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
11
src/components/theme-provider.tsx
Normal file
11
src/components/theme-provider.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
201
src/components/ui/dropdown-menu.tsx
Normal file
201
src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,201 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
@ -1,7 +1,14 @@
|
||||
export type AppConfig = {
|
||||
name: string;
|
||||
socials: {
|
||||
discord: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const appConfig: AppConfig = {
|
||||
name: "Logipedia",
|
||||
|
||||
socials: {
|
||||
discord: "https://discord.com",
|
||||
},
|
||||
};
|
||||
|
||||
@ -12,3 +12,9 @@ export const articleSchema = z.object({
|
||||
export const createArticleSchema = articleSchema.pick({
|
||||
title: true,
|
||||
});
|
||||
|
||||
export const articleFilterSchema = z.object({
|
||||
query: z.string().optional(),
|
||||
category: z.string().optional(),
|
||||
sort: z.string().optional(),
|
||||
});
|
||||
|
||||
@ -7,13 +7,29 @@ import {
|
||||
} from "@/server/api/trpc";
|
||||
import { Article, articles } from "@/server/db/schema";
|
||||
import {
|
||||
articleFilterSchema,
|
||||
articleSchema,
|
||||
createArticleSchema,
|
||||
} from "@/lib/validation/zod/article";
|
||||
import { and, count, eq, gt, like, sql } from "drizzle-orm";
|
||||
import { and, asc, count, desc, eq, gt, ilike, like, sql } from "drizzle-orm";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import { generateSlug } from "@/lib/utils";
|
||||
|
||||
const getArticleSorting = (sort: string) => {
|
||||
switch (sort) {
|
||||
case "newest":
|
||||
return desc(articles.createdAt);
|
||||
case "oldest":
|
||||
return asc(articles.createdAt);
|
||||
case "abc":
|
||||
return asc(articles.title);
|
||||
case "cba":
|
||||
return desc(articles.title);
|
||||
default:
|
||||
return desc(articles.createdAt); // Default to newest
|
||||
}
|
||||
};
|
||||
|
||||
export const articleRouter = createTRPCRouter({
|
||||
// queries
|
||||
search: publicProcedure
|
||||
@ -32,25 +48,35 @@ export const articleRouter = createTRPCRouter({
|
||||
with: { category: true },
|
||||
});
|
||||
}),
|
||||
getByPage: publicProcedure
|
||||
getByCursor: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
categoryId: z.string().optional(),
|
||||
limit: z.number().optional(),
|
||||
cursor: z.string().optional(),
|
||||
filter: articleFilterSchema.optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { categoryId, cursor } = input!;
|
||||
const { cursor } = input!;
|
||||
const limit = input?.limit ?? 50;
|
||||
const cursorArg = cursor ? gt(articles.slug, cursor) : undefined;
|
||||
const categoryArg = categoryId
|
||||
? eq(articles.categoryId, categoryId)
|
||||
|
||||
const queryFilterArg = input?.filter?.query?.length
|
||||
? ilike(articles.title, "%" + input.filter.query + "%")
|
||||
: undefined;
|
||||
const categoryArg = input?.filter?.category
|
||||
? eq(articles.categoryId, input.filter.category)
|
||||
: undefined;
|
||||
const orderBy = getArticleSorting(input?.filter?.sort ?? "newest");
|
||||
const items = await ctx.db.query.articles.findMany({
|
||||
where: and(cursorArg, categoryArg),
|
||||
where: and(
|
||||
cursorArg,
|
||||
categoryArg,
|
||||
queryFilterArg,
|
||||
eq(articles.published, true),
|
||||
),
|
||||
limit: limit + 1,
|
||||
orderBy: articles.slug,
|
||||
orderBy,
|
||||
columns: {
|
||||
title: true,
|
||||
slug: true,
|
||||
|
||||
@ -5,32 +5,32 @@
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
/* --sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%; */
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
@ -38,33 +38,34 @@
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
/* --sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%; */
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user