Compare commits
	
		
			183 Commits
		
	
	
		
			243cbf4026
			...
			395144cd65
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
								 | 
						395144cd65 | |
| 
							
							
								
								 | 
						4c2c9c5680 | |
| 
							
							
								
								 | 
						058f406e2b | |
| 
							
							
								
								 | 
						3c4a999ef7 | |
| 
							
							
								
								 | 
						db613357a7 | |
| 
							
							
								
								 | 
						ca097dfb65 | |
| 
							
							
								
								 | 
						20d211bdc4 | |
| 
							
							
								
								 | 
						a20dbf3d9e | |
| 
							
							
								
								 | 
						1c8a689b4c | |
| 
							
							
								
								 | 
						91a1bf468e | |
| 
							
							
								
								 | 
						5b4e961859 | |
| 
							
							
								
								 | 
						9bc1750848 | |
| 
							
							
								
								 | 
						91e8a17525 | |
| 
							
							
								
								 | 
						a4f5467832 | |
| 
							
							
								
								 | 
						6e501aa75f | |
| 
							
							
								
								 | 
						b9d5cfc18d | |
| 
							
							
								
								 | 
						454695ca1e | |
| 
							
							
								
								 | 
						fa62080e0c | |
| 
							
							
								
								 | 
						d2936f2a6b | |
| 
							
							
								
								 | 
						712c8da465 | |
| 
							
							
								
								 | 
						62d8f05d8a | |
| 
							
							
								
								 | 
						bc675cd258 | |
| 
							
							
								
								 | 
						def21a236c | |
| 
							
							
								
								 | 
						d7a60b331c | |
| 
							
							
								
								 | 
						2a2a994f90 | |
| 
							
							
								
								 | 
						d2815c7356 | |
| 
							
							
								
								 | 
						164097e821 | |
| 
							
							
								
								 | 
						35ac70b62b | |
| 
							
							
								
								 | 
						f8bb05d8b9 | |
| 
							
							
								
								 | 
						f21159b849 | |
| 
							
							
								
								 | 
						158d73a6df | |
| 
							
							
								
								 | 
						3f22b2ce82 | |
| 
							
							
								
								 | 
						c0f55be1b5 | |
| 
							
							
								
								 | 
						0bb8eac362 | |
| 
							
							
								
								 | 
						c7dfbee0e0 | |
| 
							
							
								
								 | 
						9eb558dc2f | |
| 
							
							
								
								 | 
						9c2148076b | |
| 
							
							
								
								 | 
						fff780922d | |
| 
							
							
								
								 | 
						1d13d79682 | |
| 
							
							
								
								 | 
						8dc7d08210 | |
| 
							
							
								
								 | 
						f9f2a8352d | |
| 
							
							
								
								 | 
						5aa503a236 | |
| 
							
							
								
								 | 
						1861630bf3 | |
| 
							
							
								
								 | 
						a9e072ab5f | |
| 
							
							
								
								 | 
						a204eec776 | |
| 
							
							
								
								 | 
						e5d2aba207 | |
| 
							
							
								
								 | 
						e9ed5ae9ea | |
| 
							
							
								
								 | 
						fdeeb955c9 | |
| 
							
							
								
								 | 
						c54720cc67 | |
| 
							
							
								
								 | 
						0586b33bcd | |
| 
							
							
								
								 | 
						5018422353 | |
| 
							
							
								
								 | 
						1b49672be6 | |
| 
							
							
								
								 | 
						6073a1dce5 | |
| 
							
							
								
								 | 
						2df9738364 | |
| 
							
							
								
								 | 
						fb4911c067 | |
| 
							
							
								
								 | 
						6e42145e88 | |
| 
							
							
								
								 | 
						b69a172dd5 | |
| 
							
							
								
								 | 
						edfee2c35d | |
| 
							
							
								
								 | 
						83efc850d3 | |
| 
							
							
								
								 | 
						356c487ed7 | |
| 
							
							
								
								 | 
						349a191d12 | |
| 
							
							
								
								 | 
						0138e1aa2a | |
| 
							
							
								
								 | 
						380fc56d17 | |
| 
							
							
								
								 | 
						23058ed48b | |
| 
							
							
								
								 | 
						773633d103 | |
| 
							
							
								
								 | 
						6378b358ed | |
| 
							
							
								
								 | 
						0e0c2a71ea | |
| 
							
							
								
								 | 
						52578b7979 | |
| 
							
							
								
								 | 
						6b9a8335de | |
| 
							
							
								
								 | 
						e272894918 | |
| 
							
							
								
								 | 
						a2682f4f05 | |
| 
							
							
								
								 | 
						24ddda4a9e | |
| 
							
							
								
								 | 
						d32b689fbb | |
| 
							
							
								
								 | 
						221323ae83 | |
| 
							
							
								
								 | 
						e8c37f794d | |
| 
							
							
								
								 | 
						2006641bb4 | |
| 
							
							
								
								 | 
						7a2d536318 | |
| 
							
							
								
								 | 
						9bf60c2282 | |
| 
							
							
								
								 | 
						d07d54731d | |
| 
							
							
								
								 | 
						eb2ecf6618 | |
| 
							
							
								
								 | 
						4f7d12edf5 | |
| 
							
							
								
								 | 
						21c0a1db6b | |
| 
							
							
								
								 | 
						c8cd1069da | |
| 
							
							
								
								 | 
						ae759de164 | |
| 
							
							
								
								 | 
						61d956b9cd | |
| 
							
							
								
								 | 
						527b0d2aac | |
| 
							
							
								
								 | 
						d741901afd | |
| 
							
							
								
								 | 
						483b9d987a | |
| 
							
							
								
								 | 
						9c9b010dc1 | |
| 
							
							
								
								 | 
						b1de1477a6 | |
| 
							
							
								
								 | 
						5058f5192e | |
| 
							
							
								
								 | 
						d0b7dd5910 | |
| 
							
							
								
								 | 
						7912104e77 | |
| 
							
							
								
								 | 
						9ac1a7c288 | |
| 
							
							
								
								 | 
						9cebe4c2f6 | |
| 
							
							
								
								 | 
						2fef5ae1cb | |
| 
							
							
								
								 | 
						854487a2f2 | |
| 
							
							
								
								 | 
						15a1309275 | |
| 
							
							
								
								 | 
						ae25aca0e8 | |
| 
							
							
								
								 | 
						9c0423d4e8 | |
| 
							
							
								
								 | 
						d5cac57485 | |
| 
							
							
								
								 | 
						f2231ea24d | |
| 
							
							
								
								 | 
						8e8228de16 | |
| 
							
							
								
								 | 
						2605226e0e | |
| 
							
							
								
								 | 
						a89535c058 | |
| 
							
							
								
								 | 
						d3689ab4b2 | |
| 
							
							
								
								 | 
						06650dcb22 | |
| 
							
							
								
								 | 
						7bdfe64acd | |
| 
							
							
								
								 | 
						7c33b50a40 | |
| 
							
							
								
								 | 
						0b4396a25f | |
| 
							
							
								
								 | 
						78b07861d9 | |
| 
							
							
								
								 | 
						944864eae2 | |
| 
							
							
								
								 | 
						66f5b6ff35 | |
| 
							
							
								
								 | 
						7e7b25cd79 | |
| 
							
							
								
								 | 
						0adc8a5c25 | |
| 
							
							
								
								 | 
						ad613517f4 | |
| 
							
							
								
								 | 
						f6784cca29 | |
| 
							
							
								
								 | 
						4ffa702471 | |
| 
							
							
								
								 | 
						ff524ac058 | |
| 
							
							
								
								 | 
						37ae474de7 | |
| 
							
							
								
								 | 
						1a7d439e30 | |
| 
							
							
								
								 | 
						c339aa5002 | |
| 
							
							
								
								 | 
						ef1fba1187 | |
| 
							
							
								
								 | 
						771534c3d9 | |
| 
							
							
								
								 | 
						fd3b9ac2b4 | |
| 
							
							
								
								 | 
						a78a2fa260 | |
| 
							
							
								
								 | 
						0670fe87ea | |
| 
							
							
								
								 | 
						f3d221e517 | |
| 
							
							
								
								 | 
						054ba0b224 | |
| 
							
							
								
								 | 
						1950e31cfe | |
| 
							
							
								
								 | 
						cee081fe61 | |
| 
							
							
								
								 | 
						8975263c47 | |
| 
							
							
								
								 | 
						bf126255d8 | |
| 
							
							
								
								 | 
						28f85bd714 | |
| 
							
							
								
								 | 
						588c37e68b | |
| 
							
							
								
								 | 
						f662ae8719 | |
| 
							
							
								
								 | 
						408fbad5fd | |
| 
							
							
								
								 | 
						4a5e2e4445 | |
| 
							
							
								
								 | 
						ddaf2bf1c4 | |
| 
							
							
								
								 | 
						99d693df6f | |
| 
							
							
								
								 | 
						2dc4bb2279 | |
| 
							
							
								
								 | 
						7df1ffdfaf | |
| 
							
							
								
								 | 
						e0b47622ce | |
| 
							
							
								
								 | 
						37aad4605c | |
| 
							
							
								
								 | 
						53fbddd846 | |
| 
							
							
								
								 | 
						d34fb324bd | |
| 
							
							
								
								 | 
						e05142999f | |
| 
							
							
								
								 | 
						4b7549020e | |
| 
							
							
								
								 | 
						5de197dfe0 | |
| 
							
							
								
								 | 
						74c8e04fcb | |
| 
							
							
								
								 | 
						bd4a690d95 | |
| 
							
							
								
								 | 
						4353536986 | |
| 
							
							
								
								 | 
						f3472d2a55 | |
| 
							
							
								
								 | 
						a8f68ff737 | |
| 
							
							
								
								 | 
						a8a4538513 | |
| 
							
							
								
								 | 
						f1422b5b24 | |
| 
							
							
								
								 | 
						0d25299ba6 | |
| 
							
							
								
								 | 
						3d571a871e | |
| 
							
							
								
								 | 
						0a5095bd5f | |
| 
							
							
								
								 | 
						a5e5c8c246 | |
| 
							
							
								
								 | 
						b1da7a6856 | |
| 
							
							
								
								 | 
						9e01f1d4ad | |
| 
							
							
								
								 | 
						a650bd7183 | |
| 
							
							
								
								 | 
						7a7cd39f6e | |
| 
							
							
								
								 | 
						c0428e2312 | |
| 
							
							
								
								 | 
						6f97e243e2 | |
| 
							
							
								
								 | 
						0c4b3167ce | |
| 
							
							
								
								 | 
						4cb077e7b9 | |
| 
							
							
								
								 | 
						87ec3c99c4 | |
| 
							
							
								
								 | 
						4f53932f9c | |
| 
							
							
								
								 | 
						6e640b3181 | |
| 
							
							
								
								 | 
						114d59e9ed | |
| 
							
							
								
								 | 
						9585c4843f | |
| 
							
							
								
								 | 
						c0ec0b0940 | |
| 
							
							
								
								 | 
						bbea849de9 | |
| 
							
							
								
								 | 
						34a18cae54 | |
| 
							
							
								
								 | 
						877fc08bd6 | |
| 
							
							
								
								 | 
						43b75b53b8 | |
| 
							
							
								
								 | 
						f186f32846 | |
| 
							
							
								
								 | 
						495d6e2311 | |
| 
							
							
								
								 | 
						db26cb602c | |
| 
							
							
								
								 | 
						7d5540a5f9 | |
| 
							
							
								
								 | 
						f1d53b93b6 | 
| 
						 | 
					@ -34,3 +34,6 @@ yarn-error.log*
 | 
				
			||||||
# typescript
 | 
					# typescript
 | 
				
			||||||
*.tsbuildinfo
 | 
					*.tsbuildinfo
 | 
				
			||||||
next-env.d.ts
 | 
					next-env.d.ts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#secret
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					pipeline {
 | 
				
			||||||
 | 
					agent any
 | 
				
			||||||
 | 
						environment{
 | 
				
			||||||
 | 
						JWT_TOKEN=credentials('JWT_TOKEN')
 | 
				
			||||||
 | 
						DATABASE_URL=credentials('DATABASE_URL')
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						stages{
 | 
				
			||||||
 | 
							stage('build'){
 | 
				
			||||||
 | 
								steps{
 | 
				
			||||||
 | 
									sh 'npm install'
 | 
				
			||||||
 | 
									sh 'npm run build'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							stage('deploy'){
 | 
				
			||||||
 | 
								steps{
 | 
				
			||||||
 | 
									sshPublisher(publishers: [sshPublisherDesc(configName: 'Demos', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: 'ssh-uploads/subman', remoteDirectorySDF: false, removePrefix: '', sourceFiles: './**')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										38
									
								
								README.md
								
								
								
								
							
							
						
						
									
										38
									
								
								README.md
								
								
								
								
							| 
						 | 
					@ -1,36 +1,18 @@
 | 
				
			||||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
 | 
					# Subman
 | 
				
			||||||
 | 
					## A self-hosted literary submission manager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Getting Started
 | 
					I developed this project as a demonstration of my full-stack development abilities, utilizing:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
First, run the development server:
 | 
					- Nextjs
 | 
				
			||||||
 | 
					- Tailwind
 | 
				
			||||||
 | 
					- heavily customised Shadcn components
 | 
				
			||||||
 | 
					- an Sqlite database with Prisma ORM as intermediary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					My previous attempt at this project was a Nodejs server with a React frontend, but this version is much better!
 | 
				
			||||||
npm run dev
 | 
					 | 
				
			||||||
# or
 | 
					 | 
				
			||||||
yarn dev
 | 
					 | 
				
			||||||
# or
 | 
					 | 
				
			||||||
pnpm dev
 | 
					 | 
				
			||||||
# or
 | 
					 | 
				
			||||||
bun dev
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
 | 
					## What it does
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
 | 
					Subman was inspired by my experiences submitting short fiction to magazines for publication. It allows the user to track where submissions are pending, in addition to meta-data such as genres, word count and so on. What you see here is the Minimum Shippable Product.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Learn More
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
To learn more about Next.js, take a look at the following resources:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
 | 
					 | 
				
			||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Deploy on Vercel
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "https://ui.shadcn.com/schema.json",
 | 
				
			||||||
 | 
					  "style": "default",
 | 
				
			||||||
 | 
					  "rsc": true,
 | 
				
			||||||
 | 
					  "tsx": true,
 | 
				
			||||||
 | 
					  "tailwind": {
 | 
				
			||||||
 | 
					    "config": "tailwind.config.js",
 | 
				
			||||||
 | 
					    "css": "src/app/globals.css",
 | 
				
			||||||
 | 
					    "baseColor": "slate",
 | 
				
			||||||
 | 
					    "cssVariables": true,
 | 
				
			||||||
 | 
					    "prefix": ""
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "aliases": {
 | 
				
			||||||
 | 
					    "components": "@/components",
 | 
				
			||||||
 | 
					    "utils": "@/lib/utils"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,9 @@
 | 
				
			||||||
/** @type {import('next').NextConfig} */
 | 
					/** @type {import('next').NextConfig} */
 | 
				
			||||||
const nextConfig = {};
 | 
					const nextConfig = {
 | 
				
			||||||
 | 
					  webpack: (config) => {
 | 
				
			||||||
 | 
					    config.externals = [...config.externals, "bcrypt"];
 | 
				
			||||||
 | 
					    return config;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default nextConfig;
 | 
					export default nextConfig;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										46
									
								
								package.json
								
								
								
								
							
							
						
						
									
										46
									
								
								package.json
								
								
								
								
							| 
						 | 
					@ -1,27 +1,63 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  "name": "subman-nextjs",
 | 
					  "name": "subman-nextjs",
 | 
				
			||||||
  "type":"module",
 | 
					  "type": "module",
 | 
				
			||||||
  "version": "0.1.0",
 | 
					  "version": "0.1.0",
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "dev": "next dev",
 | 
					    "dev": "next dev",
 | 
				
			||||||
    "build": "next build",
 | 
					    "build": "next build",
 | 
				
			||||||
    "start": "next start",
 | 
					    "start": "next start",
 | 
				
			||||||
    "lint": "next lint"
 | 
					    "lint": "next lint",
 | 
				
			||||||
 | 
					    "tailwind": "npx tailwindcss -i ./src/app/globals.css -o ./src/app/tailwind.css --watch"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@hookform/resolvers": "^3.6.0",
 | 
				
			||||||
 | 
					    "@mapbox/node-pre-gyp": "^1.0.11",
 | 
				
			||||||
    "@prisma/client": "^5.15.0",
 | 
					    "@prisma/client": "^5.15.0",
 | 
				
			||||||
    "next": "14.2.3",
 | 
					    "@radix-ui/react-checkbox": "^1.0.4",
 | 
				
			||||||
 | 
					    "@radix-ui/react-context-menu": "^2.2.1",
 | 
				
			||||||
 | 
					    "@radix-ui/react-dialog": "^1.1.0",
 | 
				
			||||||
 | 
					    "@radix-ui/react-dropdown-menu": "^2.0.6",
 | 
				
			||||||
 | 
					    "@radix-ui/react-icons": "^1.3.0",
 | 
				
			||||||
 | 
					    "@radix-ui/react-label": "^2.0.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-popover": "^1.0.7",
 | 
				
			||||||
 | 
					    "@radix-ui/react-select": "^2.0.0",
 | 
				
			||||||
 | 
					    "@radix-ui/react-slot": "^1.0.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-toast": "^1.1.5",
 | 
				
			||||||
 | 
					    "@tanstack/react-table": "^8.17.3",
 | 
				
			||||||
 | 
					    "@types/bcrypt": "^5.0.2",
 | 
				
			||||||
 | 
					    "bcrypt": "^5.1.1",
 | 
				
			||||||
 | 
					    "class-variance-authority": "^0.7.0",
 | 
				
			||||||
 | 
					    "clsx": "^2.1.1",
 | 
				
			||||||
 | 
					    "date-fns": "^3.6.0",
 | 
				
			||||||
 | 
					    "jose": "^5.8.0",
 | 
				
			||||||
 | 
					    "lucide": "^0.445.0",
 | 
				
			||||||
 | 
					    "lucide-react": "^0.394.0",
 | 
				
			||||||
 | 
					    "next": "^14.2.13",
 | 
				
			||||||
 | 
					    "next-themes": "^0.3.0",
 | 
				
			||||||
    "react": "^18",
 | 
					    "react": "^18",
 | 
				
			||||||
    "react-dom": "^18"
 | 
					    "react-day-picker": "^8.10.1",
 | 
				
			||||||
 | 
					    "react-dom": "^18",
 | 
				
			||||||
 | 
					    "react-hook-form": "^7.51.5",
 | 
				
			||||||
 | 
					    "recharts": "^2.12.7",
 | 
				
			||||||
 | 
					    "tailwind-merge": "^2.3.0",
 | 
				
			||||||
 | 
					    "tailwindcss-animate": "^1.0.7",
 | 
				
			||||||
 | 
					    "text-encoding": "^0.7.0",
 | 
				
			||||||
 | 
					    "zod": "^3.23.8"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@types/node": "^20",
 | 
					    "@types/node": "^20",
 | 
				
			||||||
    "@types/react": "^18",
 | 
					    "@types/react": "^18",
 | 
				
			||||||
    "@types/react-dom": "^18",
 | 
					    "@types/react-dom": "^18",
 | 
				
			||||||
    "eslint": "^8",
 | 
					    "autoprefixer": "^10.4.19",
 | 
				
			||||||
    "eslint-config-next": "14.2.3",
 | 
					    "eslint-config-next": "14.2.3",
 | 
				
			||||||
 | 
					    "postcss": "^8.4.38",
 | 
				
			||||||
    "prisma": "^5.15.0",
 | 
					    "prisma": "^5.15.0",
 | 
				
			||||||
 | 
					    "tailwindcss": "^3.4.4",
 | 
				
			||||||
    "typescript": "^5"
 | 
					    "typescript": "^5"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "overrides": {
 | 
				
			||||||
 | 
					    "@radix-ui/react-dismissable-layer": "^1.0.5",
 | 
				
			||||||
 | 
					    "@radix-ui/react-focus-scope": "^1.0.4"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  plugins: {
 | 
				
			||||||
 | 
					    tailwindcss: {},
 | 
				
			||||||
 | 
					    autoprefixer: {},
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								prisma/dev.db
								
								
								
								
							
							
						
						
									
										
											BIN
										
									
								
								prisma/dev.db
								
								
								
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					-- RedefineTables
 | 
				
			||||||
 | 
					PRAGMA defer_foreign_keys=ON;
 | 
				
			||||||
 | 
					PRAGMA foreign_keys=OFF;
 | 
				
			||||||
 | 
					CREATE TABLE "new_Sub" (
 | 
				
			||||||
 | 
					    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
 | 
					    "storyId" INTEGER,
 | 
				
			||||||
 | 
					    "pubId" INTEGER,
 | 
				
			||||||
 | 
					    "submitted" TEXT NOT NULL,
 | 
				
			||||||
 | 
					    "responded" TEXT,
 | 
				
			||||||
 | 
					    "responseId" INTEGER,
 | 
				
			||||||
 | 
					    CONSTRAINT "Sub_storyId_fkey" FOREIGN KEY ("storyId") REFERENCES "Story" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
 | 
				
			||||||
 | 
					    CONSTRAINT "Sub_pubId_fkey" FOREIGN KEY ("pubId") REFERENCES "Pub" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
 | 
				
			||||||
 | 
					    CONSTRAINT "Sub_responseId_fkey" FOREIGN KEY ("responseId") REFERENCES "Response" ("id") ON DELETE SET NULL ON UPDATE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					INSERT INTO "new_Sub" ("id", "pubId", "responded", "responseId", "storyId", "submitted") SELECT "id", "pubId", "responded", "responseId", "storyId", "submitted" FROM "Sub";
 | 
				
			||||||
 | 
					DROP TABLE "Sub";
 | 
				
			||||||
 | 
					ALTER TABLE "new_Sub" RENAME TO "Sub";
 | 
				
			||||||
 | 
					PRAGMA foreign_keys=ON;
 | 
				
			||||||
 | 
					PRAGMA defer_foreign_keys=OFF;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					  Warnings:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - You are about to drop the column `deleted` on the `Story` table. All the data in the column will be lost.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					-- RedefineTables
 | 
				
			||||||
 | 
					PRAGMA defer_foreign_keys=ON;
 | 
				
			||||||
 | 
					PRAGMA foreign_keys=OFF;
 | 
				
			||||||
 | 
					CREATE TABLE "new_Story" (
 | 
				
			||||||
 | 
					    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
 | 
					    "word_count" INTEGER NOT NULL,
 | 
				
			||||||
 | 
					    "title" TEXT NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					INSERT INTO "new_Story" ("id", "title", "word_count") SELECT "id", "title", "word_count" FROM "Story";
 | 
				
			||||||
 | 
					DROP TABLE "Story";
 | 
				
			||||||
 | 
					ALTER TABLE "new_Story" RENAME TO "Story";
 | 
				
			||||||
 | 
					PRAGMA foreign_keys=ON;
 | 
				
			||||||
 | 
					PRAGMA defer_foreign_keys=OFF;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					  Warnings:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - You are about to drop the column `deleted` on the `Pub` table. All the data in the column will be lost.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					-- RedefineTables
 | 
				
			||||||
 | 
					PRAGMA defer_foreign_keys=ON;
 | 
				
			||||||
 | 
					PRAGMA foreign_keys=OFF;
 | 
				
			||||||
 | 
					CREATE TABLE "new_Pub" (
 | 
				
			||||||
 | 
					    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
 | 
					    "title" TEXT NOT NULL,
 | 
				
			||||||
 | 
					    "link" TEXT NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    "query_after_days" INTEGER NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					INSERT INTO "new_Pub" ("id", "link", "query_after_days", "title") SELECT "id", "link", "query_after_days", "title" FROM "Pub";
 | 
				
			||||||
 | 
					DROP TABLE "Pub";
 | 
				
			||||||
 | 
					ALTER TABLE "new_Pub" RENAME TO "Pub";
 | 
				
			||||||
 | 
					PRAGMA foreign_keys=ON;
 | 
				
			||||||
 | 
					PRAGMA defer_foreign_keys=OFF;
 | 
				
			||||||
| 
						 | 
					@ -10,11 +10,17 @@ datasource db {
 | 
				
			||||||
  url      = "file:./dev.db"
 | 
					  url      = "file:./dev.db"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model User {
 | 
				
			||||||
 | 
					id Int @id @default(autoincrement())
 | 
				
			||||||
 | 
					email String
 | 
				
			||||||
 | 
					password String
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Story {
 | 
					model Story {
 | 
				
			||||||
  id         Int     @id @default(autoincrement())
 | 
					  id         Int     @id @default(autoincrement())
 | 
				
			||||||
  word_count Int
 | 
					  word_count Int
 | 
				
			||||||
  title      String
 | 
					  title      String
 | 
				
			||||||
  deleted    Int     @default(0)
 | 
					 | 
				
			||||||
  subs       Sub[]
 | 
					  subs       Sub[]
 | 
				
			||||||
  genres     Genre[]
 | 
					  genres     Genre[]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -24,7 +30,6 @@ model Pub {
 | 
				
			||||||
  title            String
 | 
					  title            String
 | 
				
			||||||
  link             String  @default("")
 | 
					  link             String  @default("")
 | 
				
			||||||
  query_after_days Int
 | 
					  query_after_days Int
 | 
				
			||||||
  deleted          Int     @default(0)
 | 
					 | 
				
			||||||
  subs             Sub[]
 | 
					  subs             Sub[]
 | 
				
			||||||
  genres           Genre[]
 | 
					  genres           Genre[]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -44,12 +49,12 @@ model Response {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Sub {
 | 
					model Sub {
 | 
				
			||||||
  id         Int      @id @default(autoincrement())
 | 
					  id         Int      @id @default(autoincrement())
 | 
				
			||||||
  story      Story    @relation(fields: [storyId], references: [id])
 | 
					  story      Story?    @relation(fields: [storyId], references: [id])
 | 
				
			||||||
  storyId    Int
 | 
					  storyId    Int?
 | 
				
			||||||
  pub        Pub      @relation(fields: [pubId], references: [id])
 | 
					  pub        Pub?      @relation(fields: [pubId], references: [id])
 | 
				
			||||||
  pubId      Int
 | 
					  pubId      Int?
 | 
				
			||||||
  submitted  String
 | 
					  submitted  String
 | 
				
			||||||
  responded  String
 | 
					  responded  String?
 | 
				
			||||||
  response   Response @relation(fields: [responseId], references: [id])
 | 
					  response   Response? @relation(fields: [responseId], references: [id])
 | 
				
			||||||
  responseId Int
 | 
					  responseId Int?
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,27 +1,24 @@
 | 
				
			||||||
import { PrismaClient } from '@prisma/client'
 | 
					import { PrismaClient } from "@prisma/client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const prisma = new PrismaClient()
 | 
					const prisma = new PrismaClient();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function main() {
 | 
					async function main() {
 | 
				
			||||||
	// ... you will write your Prisma Client queries here
 | 
					  // ... you will write your Prisma Client queries here
 | 
				
			||||||
	const story = await prisma.story.update({
 | 
					  const story = await prisma.story.update({
 | 
				
			||||||
		where: { id: 1 },
 | 
					    where: { id: 1 },
 | 
				
			||||||
		data: {
 | 
					    data: {
 | 
				
			||||||
			title: "Ghost Aliens of Mars",
 | 
					      title: "Ghost Aliens of Mars",
 | 
				
			||||||
			genres: { set: [{ id: 1 }, { id: 2 }], create: { name: "alien-punk" } }
 | 
					      genres: { set: [{ id: 1 }, { id: 2 }], create: { name: "alien-punk" } },
 | 
				
			||||||
		}
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	console.log(story)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
main()
 | 
					main()
 | 
				
			||||||
	.then(async () => {
 | 
					  .then(async () => {
 | 
				
			||||||
		await prisma.$disconnect()
 | 
					    await prisma.$disconnect();
 | 
				
			||||||
	})
 | 
					  })
 | 
				
			||||||
	.catch(async (e) => {
 | 
					  .catch(async (e) => {
 | 
				
			||||||
		console.error(e)
 | 
					    console.error(e);
 | 
				
			||||||
		await prisma.$disconnect()
 | 
					    await prisma.$disconnect();
 | 
				
			||||||
		process.exit(1)
 | 
					    process.exit(1);
 | 
				
			||||||
	})
 | 
					  });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import { cva, type VariantProps } from "class-variance-authority"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const badgeVariants = cva(
 | 
				
			||||||
 | 
					  "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    variants: {
 | 
				
			||||||
 | 
					      variant: {
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          "border-transparent bg-accent text-accent-foreground",
 | 
				
			||||||
 | 
					        secondary:
 | 
				
			||||||
 | 
					          "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
 | 
				
			||||||
 | 
					        destructive:
 | 
				
			||||||
 | 
					          "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
 | 
				
			||||||
 | 
					        outline: "text-foreground",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    defaultVariants: {
 | 
				
			||||||
 | 
					      variant: "default",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface BadgeProps
 | 
				
			||||||
 | 
					  extends React.HTMLAttributes<HTMLDivElement>,
 | 
				
			||||||
 | 
					  VariantProps<typeof badgeVariants> { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function Badge({ className, variant, ...props }: BadgeProps) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={cn(badgeVariants({ variant }), className)} {...props} />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Badge, badgeVariants }
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,56 @@
 | 
				
			||||||
 | 
					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 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    variants: {
 | 
				
			||||||
 | 
					      variant: {
 | 
				
			||||||
 | 
					        default: "bg-primary text-primary-foreground hover:bg-primary/90",
 | 
				
			||||||
 | 
					        destructive:
 | 
				
			||||||
 | 
					          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
 | 
				
			||||||
 | 
					        outline:
 | 
				
			||||||
 | 
					          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
 | 
				
			||||||
 | 
					        secondary:
 | 
				
			||||||
 | 
					          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
 | 
				
			||||||
 | 
					        ghost: "hover:bg-accent hover:text-accent-foreground",
 | 
				
			||||||
 | 
					        link: "text-primary underline-offset-4 hover:underline",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      size: {
 | 
				
			||||||
 | 
					        default: "h-10 px-4 py-2",
 | 
				
			||||||
 | 
					        sm: "h-9 rounded-md px-3",
 | 
				
			||||||
 | 
					        lg: "h-11 rounded-md px-8",
 | 
				
			||||||
 | 
					        icon: "h-10 w-10",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    defaultVariants: {
 | 
				
			||||||
 | 
					      variant: "default",
 | 
				
			||||||
 | 
					      size: "default",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 }
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,66 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import { ChevronLeft, ChevronRight } from "lucide-react"
 | 
				
			||||||
 | 
					import { DayPicker } from "react-day-picker"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					import { buttonVariants } from "@/components/ui/button"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type CalendarProps = React.ComponentProps<typeof DayPicker>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function Calendar({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  classNames,
 | 
				
			||||||
 | 
					  showOutsideDays = true,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: CalendarProps) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <DayPicker
 | 
				
			||||||
 | 
					      showOutsideDays={showOutsideDays}
 | 
				
			||||||
 | 
					      className={cn("p-3", className)}
 | 
				
			||||||
 | 
					      classNames={{
 | 
				
			||||||
 | 
					        months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
 | 
				
			||||||
 | 
					        month: "space-y-4",
 | 
				
			||||||
 | 
					        caption: "flex justify-center pt-1 relative items-center",
 | 
				
			||||||
 | 
					        caption_label: "text-sm font-medium",
 | 
				
			||||||
 | 
					        nav: "space-x-1 flex items-center",
 | 
				
			||||||
 | 
					        nav_button: cn(
 | 
				
			||||||
 | 
					          buttonVariants({ variant: "outline" }),
 | 
				
			||||||
 | 
					          "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        nav_button_previous: "absolute left-1",
 | 
				
			||||||
 | 
					        nav_button_next: "absolute right-1",
 | 
				
			||||||
 | 
					        table: "w-full border-collapse space-y-1",
 | 
				
			||||||
 | 
					        head_row: "flex",
 | 
				
			||||||
 | 
					        head_cell:
 | 
				
			||||||
 | 
					          "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
 | 
				
			||||||
 | 
					        row: "flex w-full mt-2",
 | 
				
			||||||
 | 
					        cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
 | 
				
			||||||
 | 
					        day: cn(
 | 
				
			||||||
 | 
					          buttonVariants({ variant: "ghost" }),
 | 
				
			||||||
 | 
					          "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        day_range_end: "day-range-end",
 | 
				
			||||||
 | 
					        day_selected:
 | 
				
			||||||
 | 
					          "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
 | 
				
			||||||
 | 
					        day_today: "bg-accent text-accent-foreground",
 | 
				
			||||||
 | 
					        day_outside:
 | 
				
			||||||
 | 
					          "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
 | 
				
			||||||
 | 
					        day_disabled: "text-muted-foreground opacity-50",
 | 
				
			||||||
 | 
					        day_range_middle:
 | 
				
			||||||
 | 
					          "aria-selected:bg-accent aria-selected:text-accent-foreground",
 | 
				
			||||||
 | 
					        day_hidden: "invisible",
 | 
				
			||||||
 | 
					        ...classNames,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      components={{
 | 
				
			||||||
 | 
					        IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
 | 
				
			||||||
 | 
					        IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					Calendar.displayName = "Calendar"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Calendar }
 | 
				
			||||||
| 
						 | 
					@ -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-3 w-3 sm:h-6 sm:w-6 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 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 }
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,200 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
 | 
				
			||||||
 | 
					import { Check, ChevronRight, Circle } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenu = ContextMenuPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuTrigger = ContextMenuPrimitive.Trigger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuGroup = ContextMenuPrimitive.Group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuPortal = ContextMenuPrimitive.Portal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuSub = ContextMenuPrimitive.Sub
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuSubTrigger = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
 | 
				
			||||||
 | 
					    inset?: boolean
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					>(({ className, inset, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ContextMenuPrimitive.SubTrigger
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
 | 
				
			||||||
 | 
					      inset && "pl-8",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    {children}
 | 
				
			||||||
 | 
					    <ChevronRight className="ml-auto h-4 w-4" />
 | 
				
			||||||
 | 
					  </ContextMenuPrimitive.SubTrigger>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuSubContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ContextMenuPrimitive.SubContent
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "z-50 min-w-[8rem] overflow-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}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ContextMenuPrimitive.Content>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ContextMenuPrimitive.Portal>
 | 
				
			||||||
 | 
					    <ContextMenuPrimitive.Content
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 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}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </ContextMenuPrimitive.Portal>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuItem = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ContextMenuPrimitive.Item>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
 | 
				
			||||||
 | 
					    inset?: boolean
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					>(({ className, inset, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ContextMenuPrimitive.Item
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
				
			||||||
 | 
					      inset && "pl-8",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuCheckboxItem = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
 | 
				
			||||||
 | 
					>(({ className, children, checked, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ContextMenuPrimitive.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 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">
 | 
				
			||||||
 | 
					      <ContextMenuPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					        <Check className="h-4 w-4" />
 | 
				
			||||||
 | 
					      </ContextMenuPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					    {children}
 | 
				
			||||||
 | 
					  </ContextMenuPrimitive.CheckboxItem>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ContextMenuCheckboxItem.displayName =
 | 
				
			||||||
 | 
					  ContextMenuPrimitive.CheckboxItem.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuRadioItem = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
 | 
				
			||||||
 | 
					>(({ className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ContextMenuPrimitive.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 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">
 | 
				
			||||||
 | 
					      <ContextMenuPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					        <Circle className="h-2 w-2 fill-current" />
 | 
				
			||||||
 | 
					      </ContextMenuPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					    {children}
 | 
				
			||||||
 | 
					  </ContextMenuPrimitive.RadioItem>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuLabel = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ContextMenuPrimitive.Label>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
 | 
				
			||||||
 | 
					    inset?: boolean
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					>(({ className, inset, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ContextMenuPrimitive.Label
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "px-2 py-1.5 text-sm font-semibold text-foreground",
 | 
				
			||||||
 | 
					      inset && "pl-8",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuSeparator = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ContextMenuPrimitive.Separator>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ContextMenuPrimitive.Separator
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("-mx-1 my-1 h-px bg-border", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContextMenuShortcut = ({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLSpanElement>) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <span
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "ml-auto text-xs tracking-widest text-muted-foreground",
 | 
				
			||||||
 | 
					        className
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					ContextMenuShortcut.displayName = "ContextMenuShortcut"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  ContextMenu,
 | 
				
			||||||
 | 
					  ContextMenuTrigger,
 | 
				
			||||||
 | 
					  ContextMenuContent,
 | 
				
			||||||
 | 
					  ContextMenuItem,
 | 
				
			||||||
 | 
					  ContextMenuCheckboxItem,
 | 
				
			||||||
 | 
					  ContextMenuRadioItem,
 | 
				
			||||||
 | 
					  ContextMenuLabel,
 | 
				
			||||||
 | 
					  ContextMenuSeparator,
 | 
				
			||||||
 | 
					  ContextMenuShortcut,
 | 
				
			||||||
 | 
					  ContextMenuGroup,
 | 
				
			||||||
 | 
					  ContextMenuPortal,
 | 
				
			||||||
 | 
					  ContextMenuSub,
 | 
				
			||||||
 | 
					  ContextMenuSubContent,
 | 
				
			||||||
 | 
					  ContextMenuSubTrigger,
 | 
				
			||||||
 | 
					  ContextMenuRadioGroup,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,122 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as DialogPrimitive from "@radix-ui/react-dialog"
 | 
				
			||||||
 | 
					import { X } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Dialog = DialogPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogTrigger = DialogPrimitive.Trigger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogPortal = DialogPrimitive.Portal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogClose = DialogPrimitive.Close
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogOverlay = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Overlay>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPrimitive.Overlay
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    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",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Content>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
 | 
				
			||||||
 | 
					>(({ className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPortal>
 | 
				
			||||||
 | 
					    <DialogOverlay />
 | 
				
			||||||
 | 
					    <DialogPrimitive.Content
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      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",
 | 
				
			||||||
 | 
					        className
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
 | 
				
			||||||
 | 
					        <X className="h-4 w-4" />
 | 
				
			||||||
 | 
					        <span className="sr-only">Close</span>
 | 
				
			||||||
 | 
					      </DialogPrimitive.Close>
 | 
				
			||||||
 | 
					    </DialogPrimitive.Content>
 | 
				
			||||||
 | 
					  </DialogPortal>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogContent.displayName = DialogPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogHeader = ({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex flex-col space-y-1.5 text-center sm:text-left",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					DialogHeader.displayName = "DialogHeader"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogFooter = ({
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					DialogFooter.displayName = "DialogFooter"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogTitle = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Title>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPrimitive.Title
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "text-lg font-semibold leading-none tracking-tight",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogTitle.displayName = DialogPrimitive.Title.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DialogDescription = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof DialogPrimitive.Description>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <DialogPrimitive.Description
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-sm text-muted-foreground", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					DialogDescription.displayName = DialogPrimitive.Description.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  Dialog,
 | 
				
			||||||
 | 
					  DialogPortal,
 | 
				
			||||||
 | 
					  DialogOverlay,
 | 
				
			||||||
 | 
					  DialogClose,
 | 
				
			||||||
 | 
					  DialogTrigger,
 | 
				
			||||||
 | 
					  DialogContent,
 | 
				
			||||||
 | 
					  DialogHeader,
 | 
				
			||||||
 | 
					  DialogFooter,
 | 
				
			||||||
 | 
					  DialogTitle,
 | 
				
			||||||
 | 
					  DialogDescription,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,200 @@
 | 
				
			||||||
 | 
					"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 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
 | 
				
			||||||
 | 
					      inset && "pl-8",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    {children}
 | 
				
			||||||
 | 
					    <ChevronRight className="ml-auto h-4 w-4" />
 | 
				
			||||||
 | 
					  </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 min-w-[8rem] overflow-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 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",
 | 
				
			||||||
 | 
					      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,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,176 @@
 | 
				
			||||||
 | 
					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-sm 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-sm font-medium text-destructive", className)}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {body}
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					FormMessage.displayName = "FormMessage"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  useFormField,
 | 
				
			||||||
 | 
					  Form,
 | 
				
			||||||
 | 
					  FormItem,
 | 
				
			||||||
 | 
					  FormLabel,
 | 
				
			||||||
 | 
					  FormControl,
 | 
				
			||||||
 | 
					  FormDescription,
 | 
				
			||||||
 | 
					  FormMessage,
 | 
				
			||||||
 | 
					  FormField,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface InputProps
 | 
				
			||||||
 | 
					  extends React.InputHTMLAttributes<HTMLInputElement> { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Input = React.forwardRef<HTMLInputElement, InputProps>(
 | 
				
			||||||
 | 
					  ({ className, type, ...props }, ref) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        type={type}
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					          "flex h-10 w-full rounded-md border border-input bg-input px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
 | 
				
			||||||
 | 
					          className
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					Input.displayName = "Input"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Input }
 | 
				
			||||||
| 
						 | 
					@ -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-base font-bold 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 }
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as PopoverPrimitive from "@radix-ui/react-popover"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Popover = PopoverPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PopoverTrigger = PopoverPrimitive.Trigger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PopoverContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof PopoverPrimitive.Content>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
 | 
				
			||||||
 | 
					>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <PopoverPrimitive.Portal>
 | 
				
			||||||
 | 
					    <PopoverPrimitive.Content
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      align={align}
 | 
				
			||||||
 | 
					      sideOffset={sideOffset}
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </PopoverPrimitive.Portal>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					PopoverContent.displayName = PopoverPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Popover, PopoverTrigger, PopoverContent }
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,160 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as SelectPrimitive from "@radix-ui/react-select"
 | 
				
			||||||
 | 
					import { Check, ChevronDown, ChevronUp } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Select = SelectPrimitive.Root
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectGroup = SelectPrimitive.Group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectValue = SelectPrimitive.Value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectTrigger = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.Trigger>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
 | 
				
			||||||
 | 
					>(({ className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.Trigger
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    {children}
 | 
				
			||||||
 | 
					    <SelectPrimitive.Icon asChild>
 | 
				
			||||||
 | 
					      <ChevronDown className="h-4 w-4 opacity-50" />
 | 
				
			||||||
 | 
					    </SelectPrimitive.Icon>
 | 
				
			||||||
 | 
					  </SelectPrimitive.Trigger>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectScrollUpButton = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.ScrollUpButton
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex cursor-default items-center justify-center py-1",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <ChevronUp className="h-4 w-4" />
 | 
				
			||||||
 | 
					  </SelectPrimitive.ScrollUpButton>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectScrollDownButton = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.ScrollDownButton
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "flex cursor-default items-center justify-center py-1",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <ChevronDown className="h-4 w-4" />
 | 
				
			||||||
 | 
					  </SelectPrimitive.ScrollDownButton>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectScrollDownButton.displayName =
 | 
				
			||||||
 | 
					  SelectPrimitive.ScrollDownButton.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectContent = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.Content>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
 | 
				
			||||||
 | 
					>(({ className, children, position = "popper", ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.Portal>
 | 
				
			||||||
 | 
					    <SelectPrimitive.Content
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
 | 
				
			||||||
 | 
					        position === "popper" &&
 | 
				
			||||||
 | 
					          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
 | 
				
			||||||
 | 
					        className
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      position={position}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <SelectScrollUpButton />
 | 
				
			||||||
 | 
					      <SelectPrimitive.Viewport
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					          "p-1",
 | 
				
			||||||
 | 
					          position === "popper" &&
 | 
				
			||||||
 | 
					            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {children}
 | 
				
			||||||
 | 
					      </SelectPrimitive.Viewport>
 | 
				
			||||||
 | 
					      <SelectScrollDownButton />
 | 
				
			||||||
 | 
					    </SelectPrimitive.Content>
 | 
				
			||||||
 | 
					  </SelectPrimitive.Portal>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectContent.displayName = SelectPrimitive.Content.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectLabel = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.Label>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.Label
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectLabel.displayName = SelectPrimitive.Label.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectItem = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.Item>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
 | 
				
			||||||
 | 
					>(({ className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.Item
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
 | 
				
			||||||
 | 
					      <SelectPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					        <Check className="h-4 w-4" />
 | 
				
			||||||
 | 
					      </SelectPrimitive.ItemIndicator>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
 | 
				
			||||||
 | 
					  </SelectPrimitive.Item>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectItem.displayName = SelectPrimitive.Item.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SelectSeparator = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof SelectPrimitive.Separator>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <SelectPrimitive.Separator
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("-mx-1 my-1 h-px bg-muted", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					SelectSeparator.displayName = SelectPrimitive.Separator.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  Select,
 | 
				
			||||||
 | 
					  SelectGroup,
 | 
				
			||||||
 | 
					  SelectValue,
 | 
				
			||||||
 | 
					  SelectTrigger,
 | 
				
			||||||
 | 
					  SelectContent,
 | 
				
			||||||
 | 
					  SelectLabel,
 | 
				
			||||||
 | 
					  SelectItem,
 | 
				
			||||||
 | 
					  SelectSeparator,
 | 
				
			||||||
 | 
					  SelectScrollUpButton,
 | 
				
			||||||
 | 
					  SelectScrollDownButton,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,117 @@
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Table = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLTableElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLTableElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <div className="relative w-full overflow-auto">
 | 
				
			||||||
 | 
					    <table
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      className={cn("w-full caption-bottom text-sm", className)}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					Table.displayName = "Table"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TableHeader = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLTableSectionElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLTableSectionElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					TableHeader.displayName = "TableHeader"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TableBody = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLTableSectionElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLTableSectionElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <tbody
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("[&_tr:last-child]:border-0", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					TableBody.displayName = "TableBody"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TableFooter = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLTableSectionElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLTableSectionElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <tfoot
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					TableFooter.displayName = "TableFooter"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TableRow = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLTableRowElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLTableRowElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <tr
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					TableRow.displayName = "TableRow"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TableHead = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLTableCellElement,
 | 
				
			||||||
 | 
					  React.ThHTMLAttributes<HTMLTableCellElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <th
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "h-12 md:px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					TableHead.displayName = "TableHead"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TableCell = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLTableCellElement,
 | 
				
			||||||
 | 
					  React.TdHTMLAttributes<HTMLTableCellElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <td
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("md:p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					TableCell.displayName = "TableCell"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TableCaption = React.forwardRef<
 | 
				
			||||||
 | 
					  HTMLTableCaptionElement,
 | 
				
			||||||
 | 
					  React.HTMLAttributes<HTMLTableCaptionElement>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <caption
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("mt-4 text-sm text-muted-foreground", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					TableCaption.displayName = "TableCaption"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  Table,
 | 
				
			||||||
 | 
					  TableHeader,
 | 
				
			||||||
 | 
					  TableBody,
 | 
				
			||||||
 | 
					  TableFooter,
 | 
				
			||||||
 | 
					  TableHead,
 | 
				
			||||||
 | 
					  TableRow,
 | 
				
			||||||
 | 
					  TableCell,
 | 
				
			||||||
 | 
					  TableCaption,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,129 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import * as ToastPrimitives from "@radix-ui/react-toast"
 | 
				
			||||||
 | 
					import { cva, type VariantProps } from "class-variance-authority"
 | 
				
			||||||
 | 
					import { X } from "lucide-react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastProvider = ToastPrimitives.Provider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastViewport = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Viewport>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ToastPrimitives.Viewport
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ToastViewport.displayName = ToastPrimitives.Viewport.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const toastVariants = cva(
 | 
				
			||||||
 | 
					  "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    variants: {
 | 
				
			||||||
 | 
					      variant: {
 | 
				
			||||||
 | 
					        default: "border bg-background text-foreground",
 | 
				
			||||||
 | 
					        destructive:
 | 
				
			||||||
 | 
					          "destructive group border-destructive bg-destructive text-destructive-foreground",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    defaultVariants: {
 | 
				
			||||||
 | 
					      variant: "default",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Toast = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Root>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
 | 
				
			||||||
 | 
					    VariantProps<typeof toastVariants>
 | 
				
			||||||
 | 
					>(({ className, variant, ...props }, ref) => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ToastPrimitives.Root
 | 
				
			||||||
 | 
					      ref={ref}
 | 
				
			||||||
 | 
					      className={cn(toastVariants({ variant }), className)}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					Toast.displayName = ToastPrimitives.Root.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastAction = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Action>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ToastPrimitives.Action
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ToastAction.displayName = ToastPrimitives.Action.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastClose = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Close>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ToastPrimitives.Close
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn(
 | 
				
			||||||
 | 
					      "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
 | 
				
			||||||
 | 
					      className
 | 
				
			||||||
 | 
					    )}
 | 
				
			||||||
 | 
					    toast-close=""
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <X className="h-4 w-4" />
 | 
				
			||||||
 | 
					  </ToastPrimitives.Close>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ToastClose.displayName = ToastPrimitives.Close.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastTitle = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Title>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ToastPrimitives.Title
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-sm font-semibold", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ToastTitle.displayName = ToastPrimitives.Title.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ToastDescription = React.forwardRef<
 | 
				
			||||||
 | 
					  React.ElementRef<typeof ToastPrimitives.Description>,
 | 
				
			||||||
 | 
					  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					  <ToastPrimitives.Description
 | 
				
			||||||
 | 
					    ref={ref}
 | 
				
			||||||
 | 
					    className={cn("text-sm opacity-90", className)}
 | 
				
			||||||
 | 
					    {...props}
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
 | 
					ToastDescription.displayName = ToastPrimitives.Description.displayName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ToastActionElement = React.ReactElement<typeof ToastAction>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  type ToastProps,
 | 
				
			||||||
 | 
					  type ToastActionElement,
 | 
				
			||||||
 | 
					  ToastProvider,
 | 
				
			||||||
 | 
					  ToastViewport,
 | 
				
			||||||
 | 
					  Toast,
 | 
				
			||||||
 | 
					  ToastTitle,
 | 
				
			||||||
 | 
					  ToastDescription,
 | 
				
			||||||
 | 
					  ToastClose,
 | 
				
			||||||
 | 
					  ToastAction,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Toast,
 | 
				
			||||||
 | 
					  ToastClose,
 | 
				
			||||||
 | 
					  ToastDescription,
 | 
				
			||||||
 | 
					  ToastProvider,
 | 
				
			||||||
 | 
					  ToastTitle,
 | 
				
			||||||
 | 
					  ToastViewport,
 | 
				
			||||||
 | 
					} from "@/components/ui/toast"
 | 
				
			||||||
 | 
					import { useToast } from "@/components/ui/use-toast"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Toaster() {
 | 
				
			||||||
 | 
					  const { toasts } = useToast()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ToastProvider>
 | 
				
			||||||
 | 
					      {toasts.map(function ({ id, title, description, action, ...props }) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <Toast key={id} {...props}>
 | 
				
			||||||
 | 
					            <div className="grid gap-1">
 | 
				
			||||||
 | 
					              {title && <ToastTitle>{title}</ToastTitle>}
 | 
				
			||||||
 | 
					              {description && (
 | 
				
			||||||
 | 
					                <ToastDescription>{description}</ToastDescription>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            {action}
 | 
				
			||||||
 | 
					            <ToastClose />
 | 
				
			||||||
 | 
					          </Toast>
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
 | 
					      <ToastViewport />
 | 
				
			||||||
 | 
					    </ToastProvider>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,194 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Inspired by react-hot-toast library
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  ToastActionElement,
 | 
				
			||||||
 | 
					  ToastProps,
 | 
				
			||||||
 | 
					} from "@/components/ui/toast"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TOAST_LIMIT = 1
 | 
				
			||||||
 | 
					const TOAST_REMOVE_DELAY = 1000000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ToasterToast = ToastProps & {
 | 
				
			||||||
 | 
					  id: string
 | 
				
			||||||
 | 
					  title?: React.ReactNode
 | 
				
			||||||
 | 
					  description?: React.ReactNode
 | 
				
			||||||
 | 
					  action?: ToastActionElement
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const actionTypes = {
 | 
				
			||||||
 | 
					  ADD_TOAST: "ADD_TOAST",
 | 
				
			||||||
 | 
					  UPDATE_TOAST: "UPDATE_TOAST",
 | 
				
			||||||
 | 
					  DISMISS_TOAST: "DISMISS_TOAST",
 | 
				
			||||||
 | 
					  REMOVE_TOAST: "REMOVE_TOAST",
 | 
				
			||||||
 | 
					} as const
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let count = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function genId() {
 | 
				
			||||||
 | 
					  count = (count + 1) % Number.MAX_SAFE_INTEGER
 | 
				
			||||||
 | 
					  return count.toString()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ActionType = typeof actionTypes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Action =
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      type: ActionType["ADD_TOAST"]
 | 
				
			||||||
 | 
					      toast: ToasterToast
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      type: ActionType["UPDATE_TOAST"]
 | 
				
			||||||
 | 
					      toast: Partial<ToasterToast>
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      type: ActionType["DISMISS_TOAST"]
 | 
				
			||||||
 | 
					      toastId?: ToasterToast["id"]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      type: ActionType["REMOVE_TOAST"]
 | 
				
			||||||
 | 
					      toastId?: ToasterToast["id"]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface State {
 | 
				
			||||||
 | 
					  toasts: ToasterToast[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const addToRemoveQueue = (toastId: string) => {
 | 
				
			||||||
 | 
					  if (toastTimeouts.has(toastId)) {
 | 
				
			||||||
 | 
					    return
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeout = setTimeout(() => {
 | 
				
			||||||
 | 
					    toastTimeouts.delete(toastId)
 | 
				
			||||||
 | 
					    dispatch({
 | 
				
			||||||
 | 
					      type: "REMOVE_TOAST",
 | 
				
			||||||
 | 
					      toastId: toastId,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }, TOAST_REMOVE_DELAY)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  toastTimeouts.set(toastId, timeout)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const reducer = (state: State, action: Action): State => {
 | 
				
			||||||
 | 
					  switch (action.type) {
 | 
				
			||||||
 | 
					    case "ADD_TOAST":
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...state,
 | 
				
			||||||
 | 
					        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case "UPDATE_TOAST":
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...state,
 | 
				
			||||||
 | 
					        toasts: state.toasts.map((t) =>
 | 
				
			||||||
 | 
					          t.id === action.toast.id ? { ...t, ...action.toast } : t
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    case "DISMISS_TOAST": {
 | 
				
			||||||
 | 
					      const { toastId } = action
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // ! Side effects ! - This could be extracted into a dismissToast() action,
 | 
				
			||||||
 | 
					      // but I'll keep it here for simplicity
 | 
				
			||||||
 | 
					      if (toastId) {
 | 
				
			||||||
 | 
					        addToRemoveQueue(toastId)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        state.toasts.forEach((toast) => {
 | 
				
			||||||
 | 
					          addToRemoveQueue(toast.id)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...state,
 | 
				
			||||||
 | 
					        toasts: state.toasts.map((t) =>
 | 
				
			||||||
 | 
					          t.id === toastId || toastId === undefined
 | 
				
			||||||
 | 
					            ? {
 | 
				
			||||||
 | 
					                ...t,
 | 
				
			||||||
 | 
					                open: false,
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            : t
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    case "REMOVE_TOAST":
 | 
				
			||||||
 | 
					      if (action.toastId === undefined) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          ...state,
 | 
				
			||||||
 | 
					          toasts: [],
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        ...state,
 | 
				
			||||||
 | 
					        toasts: state.toasts.filter((t) => t.id !== action.toastId),
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const listeners: Array<(state: State) => void> = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let memoryState: State = { toasts: [] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function dispatch(action: Action) {
 | 
				
			||||||
 | 
					  memoryState = reducer(memoryState, action)
 | 
				
			||||||
 | 
					  listeners.forEach((listener) => {
 | 
				
			||||||
 | 
					    listener(memoryState)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Toast = Omit<ToasterToast, "id">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toast({ ...props }: Toast) {
 | 
				
			||||||
 | 
					  const id = genId()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const update = (props: ToasterToast) =>
 | 
				
			||||||
 | 
					    dispatch({
 | 
				
			||||||
 | 
					      type: "UPDATE_TOAST",
 | 
				
			||||||
 | 
					      toast: { ...props, id },
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dispatch({
 | 
				
			||||||
 | 
					    type: "ADD_TOAST",
 | 
				
			||||||
 | 
					    toast: {
 | 
				
			||||||
 | 
					      ...props,
 | 
				
			||||||
 | 
					      id,
 | 
				
			||||||
 | 
					      open: true,
 | 
				
			||||||
 | 
					      onOpenChange: (open) => {
 | 
				
			||||||
 | 
					        if (!open) dismiss()
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    id: id,
 | 
				
			||||||
 | 
					    dismiss,
 | 
				
			||||||
 | 
					    update,
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function useToast() {
 | 
				
			||||||
 | 
					  const [state, setState] = React.useState<State>(memoryState)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  React.useEffect(() => {
 | 
				
			||||||
 | 
					    listeners.push(setState)
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      const index = listeners.indexOf(setState)
 | 
				
			||||||
 | 
					      if (index > -1) {
 | 
				
			||||||
 | 
					        listeners.splice(index, 1)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [state])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    ...state,
 | 
				
			||||||
 | 
					    toast,
 | 
				
			||||||
 | 
					    dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { useToast, toast }
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					import { type ClassValue, clsx } from "clsx"
 | 
				
			||||||
 | 
					import { twMerge } from "tailwind-merge"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function cn(...inputs: ClassValue[]) {
 | 
				
			||||||
 | 
					  return twMerge(clsx(inputs))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,141 @@
 | 
				
			||||||
 | 
					"use server"
 | 
				
			||||||
 | 
					import prisma from 'app/lib/db';
 | 
				
			||||||
 | 
					import { jwtVerify, JWTPayload, decodeJwt, SignJWT } from 'jose';
 | 
				
			||||||
 | 
					import { cookies } from 'next/headers';
 | 
				
			||||||
 | 
					import { loginSchema, LoginSchema } from 'app/login/schema';
 | 
				
			||||||
 | 
					import { NextResponse } from 'next/server';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getJWTSecretKey<Uint8Array>() {
 | 
				
			||||||
 | 
						const secret = process.env.JWT_SECRET
 | 
				
			||||||
 | 
						if (!secret) throw new Error("There is no JWT secret key")
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							const enc = new TextEncoder().encode(secret)
 | 
				
			||||||
 | 
							return enc
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							throw new Error("failed to getJWTSecretKey", error.message)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function verifyJwt(token: string): Promise<JWTPayload | null> {
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							const key = await getJWTSecretKey()
 | 
				
			||||||
 | 
							const { payload } = await jwtVerify(token, key)
 | 
				
			||||||
 | 
							return payload
 | 
				
			||||||
 | 
						} catch {
 | 
				
			||||||
 | 
							return null
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getJwt() {
 | 
				
			||||||
 | 
						const cookieStore = cookies()
 | 
				
			||||||
 | 
						const token = cookieStore.get("token")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (token) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const payload = await verifyJwt(token.value)
 | 
				
			||||||
 | 
								if (payload) {
 | 
				
			||||||
 | 
									const authPayload = {
 | 
				
			||||||
 | 
										email: payload.email as string,
 | 
				
			||||||
 | 
										iat: payload.iat as number,
 | 
				
			||||||
 | 
										exp: payload.exp as number
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return authPayload
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return null
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return null
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function logout() {
 | 
				
			||||||
 | 
						const cookieStore = cookies()
 | 
				
			||||||
 | 
						const token = cookieStore.get('token')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (token) {
 | 
				
			||||||
 | 
							//empty catch swallows errors
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								cookieStore.delete('token')
 | 
				
			||||||
 | 
							} catch { }
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const userData = cookieStore.get("userData")
 | 
				
			||||||
 | 
						if (userData) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								cookieStore.delete('userData')
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							} catch (_) { }
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						//return false if there is no userdata
 | 
				
			||||||
 | 
						return null
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function setUserDataCookie(userData) {
 | 
				
			||||||
 | 
						const cookieStore = cookies();
 | 
				
			||||||
 | 
						cookieStore.set({
 | 
				
			||||||
 | 
							name: 'userData',
 | 
				
			||||||
 | 
							value: JSON.stringify(userData),
 | 
				
			||||||
 | 
							path: '/',
 | 
				
			||||||
 | 
							maxAge: 3600,
 | 
				
			||||||
 | 
							sameSite: 'strict'
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function login(userLogin: LoginSchema) {
 | 
				
			||||||
 | 
						const isSafe = loginSchema.safeParse(userLogin)
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!isSafe.success) throw new Error("parse failed")
 | 
				
			||||||
 | 
							const user = await prisma.user.findFirst({ where: { email: userLogin.email } })
 | 
				
			||||||
 | 
							if (!user) throw new Error("user does not exist")
 | 
				
			||||||
 | 
							const bcrypt = require("bcrypt");
 | 
				
			||||||
 | 
							const passwordIsValid = await bcrypt.compare(userLogin.password, user.password)
 | 
				
			||||||
 | 
							if (!passwordIsValid) throw new Error("password is not valid")
 | 
				
			||||||
 | 
							return { email: userLogin.email }
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error("WHOOPS", error)
 | 
				
			||||||
 | 
							throw new Error('login failed')
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function jwtExpires() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,65 @@
 | 
				
			||||||
 | 
					import { NextResponse, NextRequest } from "next/server";
 | 
				
			||||||
 | 
					import prisma from "app/lib/db";
 | 
				
			||||||
 | 
					import { SignJWT } from "jose";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getJWTSecretKey, login, setUserDataCookie } from "../actions";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface UserLoginRequest {
 | 
				
			||||||
 | 
						email: string
 | 
				
			||||||
 | 
						password: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//render route afresh every time
 | 
				
			||||||
 | 
					const dynamic = 'force-dynamic'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//POST endpoint
 | 
				
			||||||
 | 
					export async function POST(request: NextRequest) {
 | 
				
			||||||
 | 
						const body = await request.json()
 | 
				
			||||||
 | 
						const { email, password } = body
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!email || !password) {
 | 
				
			||||||
 | 
							const res = {
 | 
				
			||||||
 | 
								succes: false,
 | 
				
			||||||
 | 
								message: 'Email or password missing'
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return NextResponse.json(res, { status: 400 })
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//fetch user from db, throw if email or password are invalid
 | 
				
			||||||
 | 
							const user = await login({ email, password })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//create and sign JWT
 | 
				
			||||||
 | 
							const token = await new SignJWT({
 | 
				
			||||||
 | 
								...user
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
								.setProtectedHeader({ alg: 'HS256' })
 | 
				
			||||||
 | 
								.setIssuedAt()
 | 
				
			||||||
 | 
								.setExpirationTime('1h')
 | 
				
			||||||
 | 
								.sign(await getJWTSecretKey())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//make response
 | 
				
			||||||
 | 
							const res = { success: true }
 | 
				
			||||||
 | 
							const response = NextResponse.json(res)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//Store jwt as secure http-only cookie
 | 
				
			||||||
 | 
							response.cookies.set({
 | 
				
			||||||
 | 
								name: 'token',
 | 
				
			||||||
 | 
								value: token,
 | 
				
			||||||
 | 
								path: '/', //defines where the cookie can be accessed - in this case, site wide
 | 
				
			||||||
 | 
								maxAge: 3600, //1 hour
 | 
				
			||||||
 | 
								httpOnly: true,
 | 
				
			||||||
 | 
								sameSite: 'strict'
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							//Store public user data as cookie
 | 
				
			||||||
 | 
							setUserDataCookie(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
							const res = { success: false, message: error.message || 'something went wrong' }
 | 
				
			||||||
 | 
							return NextResponse.json(res, { status: 500 })
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					import { revalidatePath } from "next/cache";
 | 
				
			||||||
 | 
					import { NextRequest } from "next/server";
 | 
				
			||||||
 | 
					import { logout } from "../actions";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dynamic = 'force-dynamic'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function POST(request: NextRequest) {
 | 
				
			||||||
 | 
						await logout()
 | 
				
			||||||
 | 
						revalidatePath('/login')
 | 
				
			||||||
 | 
						const response = {
 | 
				
			||||||
 | 
							success: true,
 | 
				
			||||||
 | 
							message: 'Logged out successfully',
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return new Response(JSON.stringify(response), {
 | 
				
			||||||
 | 
							headers: {
 | 
				
			||||||
 | 
								'Content-Type': 'application/json',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,107 +1,160 @@
 | 
				
			||||||
:root {
 | 
					@tailwind base;
 | 
				
			||||||
  --max-width: 1100px;
 | 
					@tailwind components;
 | 
				
			||||||
  --border-radius: 12px;
 | 
					@tailwind utilities;
 | 
				
			||||||
  --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
 | 
					 | 
				
			||||||
    "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
 | 
					 | 
				
			||||||
    "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  --foreground-rgb: 0, 0, 0;
 | 
					/**/
 | 
				
			||||||
  --background-start-rgb: 214, 219, 220;
 | 
					/* @layer base { */
 | 
				
			||||||
  --background-end-rgb: 255, 255, 255;
 | 
					/*   :root { */
 | 
				
			||||||
 | 
					/*     --background: 43 62% 98%; */
 | 
				
			||||||
 | 
					/*     --foreground: 43 73% 2%; */
 | 
				
			||||||
 | 
					/*     --muted: 43 24% 85%; */
 | 
				
			||||||
 | 
					/*     --muted-foreground: 43 10% 37%; */
 | 
				
			||||||
 | 
					/*     --popover: 43 62% 98%; */
 | 
				
			||||||
 | 
					/*     --popover-foreground: 43 73% 2%; */
 | 
				
			||||||
 | 
					/*     --card: 43 62% 98%; */
 | 
				
			||||||
 | 
					/*     --card-foreground: 43 73% 2%; */
 | 
				
			||||||
 | 
					/*     --border: 43 15% 91%; */
 | 
				
			||||||
 | 
					/*     --input: 43 15% 91%; */
 | 
				
			||||||
 | 
					/*     --primary: 43 50% 69%; */
 | 
				
			||||||
 | 
					/*     --primary-foreground: 43 50% 9%; */
 | 
				
			||||||
 | 
					/*     --secondary: 43 6% 92%; */
 | 
				
			||||||
 | 
					/*     --secondary-foreground: 43 6% 32%; */
 | 
				
			||||||
 | 
					/*     --accent: 43 13% 83%; */
 | 
				
			||||||
 | 
					/*     --accent-foreground: 43 13% 23%; */
 | 
				
			||||||
 | 
					/*     --destructive: 8 84% 20%; */
 | 
				
			||||||
 | 
					/*     --destructive-foreground: 8 84% 80%; */
 | 
				
			||||||
 | 
					/*     --ring: 43 50% 69%; */
 | 
				
			||||||
 | 
					/*     --radius: 0.5rem; */
 | 
				
			||||||
 | 
					/*   } */
 | 
				
			||||||
 | 
					/**/
 | 
				
			||||||
 | 
					/*   .dark { */
 | 
				
			||||||
 | 
					/*     --background: 43 48% 4%; */
 | 
				
			||||||
 | 
					/*     --foreground: 43 26% 97%; */
 | 
				
			||||||
 | 
					/*     --muted: 43 24% 15%; */
 | 
				
			||||||
 | 
					/*     --muted-foreground: 43 10% 63%; */
 | 
				
			||||||
 | 
					/*     --popover: 43 48% 4%; */
 | 
				
			||||||
 | 
					/*     --popover-foreground: 43 26% 97%; */
 | 
				
			||||||
 | 
					/*     --card: 43 48% 4%; */
 | 
				
			||||||
 | 
					/*     --card-foreground: 43 26% 97%; */
 | 
				
			||||||
 | 
					/*     --border: 43 15% 13%; */
 | 
				
			||||||
 | 
					/*     --input: 43 15% 13%; */
 | 
				
			||||||
 | 
					/*     --primary: 43 50% 69%; */
 | 
				
			||||||
 | 
					/*     --primary-foreground: 43 50% 9%; */
 | 
				
			||||||
 | 
					/*     --secondary: 43 8% 18%; */
 | 
				
			||||||
 | 
					/*     --secondary-foreground: 43 8% 78%; */
 | 
				
			||||||
 | 
					/*     --accent: 43 14% 23%; */
 | 
				
			||||||
 | 
					/*     --accent-foreground: 43 14% 83%; */
 | 
				
			||||||
 | 
					/*     --destructive: 8 84% 52%; */
 | 
				
			||||||
 | 
					/*     --destructive-foreground: 0 0% 100%; */
 | 
				
			||||||
 | 
					/*     --ring: 43 50% 69%; */
 | 
				
			||||||
 | 
					/*   } */
 | 
				
			||||||
 | 
					/* } */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  --primary-glow: conic-gradient(
 | 
					 | 
				
			||||||
    from 180deg at 50% 50%,
 | 
					 | 
				
			||||||
    #16abff33 0deg,
 | 
					 | 
				
			||||||
    #0885ff33 55deg,
 | 
					 | 
				
			||||||
    #54d6ff33 120deg,
 | 
					 | 
				
			||||||
    #0071ff33 160deg,
 | 
					 | 
				
			||||||
    transparent 360deg
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  --secondary-glow: radial-gradient(
 | 
					 | 
				
			||||||
    rgba(255, 255, 255, 1),
 | 
					 | 
				
			||||||
    rgba(255, 255, 255, 0)
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  --tile-start-rgb: 239, 245, 249;
 | 
					@layer base {
 | 
				
			||||||
  --tile-end-rgb: 228, 232, 233;
 | 
					 | 
				
			||||||
  --tile-border: conic-gradient(
 | 
					 | 
				
			||||||
    #00000080,
 | 
					 | 
				
			||||||
    #00000040,
 | 
					 | 
				
			||||||
    #00000030,
 | 
					 | 
				
			||||||
    #00000020,
 | 
					 | 
				
			||||||
    #00000010,
 | 
					 | 
				
			||||||
    #00000010,
 | 
					 | 
				
			||||||
    #00000080
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  --callout-rgb: 238, 240, 241;
 | 
					 | 
				
			||||||
  --callout-border-rgb: 172, 175, 176;
 | 
					 | 
				
			||||||
  --card-rgb: 180, 185, 188;
 | 
					 | 
				
			||||||
  --card-border-rgb: 131, 134, 135;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@media (prefers-color-scheme: dark) {
 | 
					 | 
				
			||||||
  :root {
 | 
					  :root {
 | 
				
			||||||
    --foreground-rgb: 255, 255, 255;
 | 
					    --background: 220 0% 96%;
 | 
				
			||||||
    --background-start-rgb: 0, 0, 0;
 | 
					    --foreground: 222.2 84% 4.9%;
 | 
				
			||||||
    --background-end-rgb: 0, 0, 0;
 | 
					    --muted: 210 40% 96.1%;
 | 
				
			||||||
 | 
					    --muted-foreground: 215.4 16.3% 46.9%;
 | 
				
			||||||
 | 
					    --popover: 0 0% 100%;
 | 
				
			||||||
 | 
					    --popover-foreground: 222.2 84% 4.9%;
 | 
				
			||||||
 | 
					    --card: 0 0% 100%;
 | 
				
			||||||
 | 
					    --card-foreground: 222.2 84% 4.9%;
 | 
				
			||||||
 | 
					    --border: 214.3 31.8% 91.4%;
 | 
				
			||||||
 | 
					    --input: 214.3 31.8% 91.4%;
 | 
				
			||||||
 | 
					    --primary: 144.91 90% 32%;
 | 
				
			||||||
 | 
					    --primary-foreground: 75 10% 97.84%;
 | 
				
			||||||
 | 
					    --secondary: 240 0% 100%;
 | 
				
			||||||
 | 
					    --secondary-foreground: 150 95% 30%;
 | 
				
			||||||
 | 
					    --accent: 150 55% 95%;
 | 
				
			||||||
 | 
					    --accent-foreground: 155 100% 20%;
 | 
				
			||||||
 | 
					    --destructive: 0 84.2% 60.2%;
 | 
				
			||||||
 | 
					    --destructive-foreground: 210 0% 100%;
 | 
				
			||||||
 | 
					    --ring: 150 100% 40%;
 | 
				
			||||||
 | 
					    --radius: 0.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
 | 
					  .dark {
 | 
				
			||||||
    --secondary-glow: linear-gradient(
 | 
					    --background: 222.2 40% 4%;
 | 
				
			||||||
      to bottom right,
 | 
					    --foreground: 210 40% 98%;
 | 
				
			||||||
      rgba(1, 65, 255, 0),
 | 
					    --muted: 217.2 32.6% 17.5%;
 | 
				
			||||||
      rgba(1, 65, 255, 0),
 | 
					    --muted-foreground: 215 20.2% 65.1%;
 | 
				
			||||||
      rgba(1, 65, 255, 0.3)
 | 
					    --popover: 230 25% 10%;
 | 
				
			||||||
    );
 | 
					    --popover-foreground: 210 40% 98%;
 | 
				
			||||||
 | 
					    --card: 222.2 20% 6%;
 | 
				
			||||||
    --tile-start-rgb: 2, 13, 46;
 | 
					    --card-foreground: 210 40% 98%;
 | 
				
			||||||
    --tile-end-rgb: 2, 5, 19;
 | 
					    --border: 217.2 20% 10%;
 | 
				
			||||||
    --tile-border: conic-gradient(
 | 
					    --input: 217.2 32.6% 17.5%;
 | 
				
			||||||
      #ffffff80,
 | 
					    --primary: 155 70% 35%;
 | 
				
			||||||
      #ffffff40,
 | 
					    --primary-foreground: 80 10% 97.84%;
 | 
				
			||||||
      #ffffff30,
 | 
					    --secondary: 200 50% 98%;
 | 
				
			||||||
      #ffffff20,
 | 
					    --secondary-foreground: 155 85% 30%;
 | 
				
			||||||
      #ffffff10,
 | 
					    --accent: 170 60% 10%;
 | 
				
			||||||
      #ffffff10,
 | 
					    --accent-foreground: 155 60% 65%;
 | 
				
			||||||
      #ffffff80
 | 
					    --destructive: 5 90% 65%;
 | 
				
			||||||
    );
 | 
					    --destructive-foreground: 0 100% 10%;
 | 
				
			||||||
 | 
					    --ring: 160 90% 45%;
 | 
				
			||||||
    --callout-rgb: 20, 20, 20;
 | 
					 | 
				
			||||||
    --callout-border-rgb: 108, 108, 108;
 | 
					 | 
				
			||||||
    --card-rgb: 100, 100, 100;
 | 
					 | 
				
			||||||
    --card-border-rgb: 200, 200, 200;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* {
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  padding: 0;
 | 
					 | 
				
			||||||
  margin: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
html,
 | 
					 | 
				
			||||||
body {
 | 
					 | 
				
			||||||
  max-width: 100vw;
 | 
					 | 
				
			||||||
  overflow-x: hidden;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
body {
 | 
					/**/
 | 
				
			||||||
  color: rgb(var(--foreground-rgb));
 | 
					/* @layer base { */
 | 
				
			||||||
  background: linear-gradient(
 | 
					/*   :root { */
 | 
				
			||||||
      to bottom,
 | 
					/*     --background: 258 70% 100%; */
 | 
				
			||||||
      transparent,
 | 
					/*     --foreground: 258 77% 0%; */
 | 
				
			||||||
      rgb(var(--background-end-rgb))
 | 
					/*     --muted: 258 29% 85%; */
 | 
				
			||||||
    )
 | 
					/*     --muted-foreground: 258 10% 40%; */
 | 
				
			||||||
    rgb(var(--background-start-rgb));
 | 
					/*     --popover: 258 70% 100%; */
 | 
				
			||||||
}
 | 
					/*     --popover-foreground: 258 77% 0%; */
 | 
				
			||||||
 | 
					/*     --card: 258 70% 100%; */
 | 
				
			||||||
 | 
					/*     --card-foreground: 258 77% 0%; */
 | 
				
			||||||
 | 
					/*     --border: 220 13% 91%; */
 | 
				
			||||||
 | 
					/*     --input: 220 13% 91%; */
 | 
				
			||||||
 | 
					/*     --primary: 258 58% 37%; */
 | 
				
			||||||
 | 
					/*     --primary-foreground: 258 58% 97%; */
 | 
				
			||||||
 | 
					/*     --secondary: 258 19% 81%; */
 | 
				
			||||||
 | 
					/*     --secondary-foreground: 258 19% 21%; */
 | 
				
			||||||
 | 
					/*     --accent: 258 19% 81%; */
 | 
				
			||||||
 | 
					/*     --accent-foreground: 258 19% 21%; */
 | 
				
			||||||
 | 
					/*     --destructive: 19 98% 27%; */
 | 
				
			||||||
 | 
					/*     --destructive-foreground: 19 98% 87%; */
 | 
				
			||||||
 | 
					/*     --ring: 258 58% 37%; */
 | 
				
			||||||
 | 
					/*     --radius: 0.5rem; */
 | 
				
			||||||
 | 
					/*   } */
 | 
				
			||||||
 | 
					/**/
 | 
				
			||||||
 | 
					/*   .dark { */
 | 
				
			||||||
 | 
					/*     --background: 258 53% 3%; */
 | 
				
			||||||
 | 
					/*     --foreground: 258 40% 97%; */
 | 
				
			||||||
 | 
					/*     --muted: 258 29% 15%; */
 | 
				
			||||||
 | 
					/*     --muted-foreground: 258 10% 60%; */
 | 
				
			||||||
 | 
					/*     --popover: 258 53% 3%; */
 | 
				
			||||||
 | 
					/*     --popover-foreground: 258 40% 97%; */
 | 
				
			||||||
 | 
					/*     --card: 258 53% 3%; */
 | 
				
			||||||
 | 
					/*     --card-foreground: 258 40% 97%; */
 | 
				
			||||||
 | 
					/*     --border: 215 27.9% 16.9%; */
 | 
				
			||||||
 | 
					/*     --input: 215 27.9% 16.9%; */
 | 
				
			||||||
 | 
					/*     --primary: 258 58% 37%; */
 | 
				
			||||||
 | 
					/*     --primary-foreground: 258 58% 97%; */
 | 
				
			||||||
 | 
					/*     --secondary: 258 15% 10%; */
 | 
				
			||||||
 | 
					/*     --secondary-foreground: 258 15% 70%; */
 | 
				
			||||||
 | 
					/*     --accent: 258 15% 10%; */
 | 
				
			||||||
 | 
					/*     --accent-foreground: 258 15% 70%; */
 | 
				
			||||||
 | 
					/*     --destructive: 19 98% 46%; */
 | 
				
			||||||
 | 
					/*     --destructive-foreground: 0 0% 100%; */
 | 
				
			||||||
 | 
					/*     --ring: 258 58% 37%; */
 | 
				
			||||||
 | 
					/*   } */
 | 
				
			||||||
 | 
					/* } */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
a {
 | 
					 | 
				
			||||||
  color: inherit;
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media (prefers-color-scheme: dark) {
 | 
					@layer base {
 | 
				
			||||||
  html {
 | 
					  * {
 | 
				
			||||||
    color-scheme: dark;
 | 
					    @apply border-border;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  body {
 | 
				
			||||||
 | 
					    @apply bg-background text-foreground;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					@tailwind base;
 | 
				
			||||||
 | 
					@tailwind components;
 | 
				
			||||||
 | 
					@tailwind utilities;
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,22 @@
 | 
				
			||||||
import type { Metadata } from "next";
 | 
					import type { Metadata } from "next";
 | 
				
			||||||
import { Inter } from "next/font/google";
 | 
					 | 
				
			||||||
import "./globals.css";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const inter = Inter({ subsets: ["latin"] });
 | 
					import { ThemeProvider } from "./ui/theme";
 | 
				
			||||||
 | 
					import { Toaster } from "@/components/ui/toaster";
 | 
				
			||||||
 | 
					import "./globals.css";
 | 
				
			||||||
 | 
					import Navlinks from "./ui/navLinks";
 | 
				
			||||||
 | 
					import { ModeToggle } from "./ui/modeToggle";
 | 
				
			||||||
 | 
					import { inter } from "./ui/fonts";
 | 
				
			||||||
 | 
					import LogoutButton from "./ui/logoutButton";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const metadata: Metadata = {
 | 
					export const metadata: Metadata = {
 | 
				
			||||||
  title: "Create Next App",
 | 
					  title: "Subman",
 | 
				
			||||||
  description: "Generated by create next app",
 | 
					  description: "A self-hosted literary submission tracker."
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function RootLayout({
 | 
					export default function RootLayout({
 | 
				
			||||||
  children,
 | 
					  children,
 | 
				
			||||||
}: Readonly<{
 | 
					}: Readonly<{
 | 
				
			||||||
| 
						 | 
					@ -17,10 +25,31 @@ export default function RootLayout({
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <html lang="en">
 | 
					    <html lang="en">
 | 
				
			||||||
      <body className={inter.className}>
 | 
					      <body className={inter.className}>
 | 
				
			||||||
        <div id="sidebar">
 | 
					        <ThemeProvider
 | 
				
			||||||
          SIDEBAR
 | 
					          attribute="class"
 | 
				
			||||||
        </div>
 | 
					          defaultTheme="system"
 | 
				
			||||||
        {children}</body>
 | 
					          enableSystem
 | 
				
			||||||
    </html>
 | 
					          disableTransitionOnChange
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div id="layout-container" className="md:p-4 w-screen h-screen mt-2 md:mt-6 flex justify-center">
 | 
				
			||||||
 | 
					            <div className="w-full md:w-5/6 flex flex-col md:flex-row">
 | 
				
			||||||
 | 
					              <div id="sidebar" className=" flex flex-row md:flex-col  justify-between items-center"> <header className="">
 | 
				
			||||||
 | 
					                <h1 className="font-black text-primary-foreground bg-primary antialiased w-full p-2 rounded-tl-3xl pl-6 pr-4 text-sm sm:text-4xl
 | 
				
			||||||
 | 
					                ">SubMan</h1>
 | 
				
			||||||
 | 
					                <p className="mt-2 mx-1 text-sm antialiased w-40 hidden md:block">The self-hosted literary submission tracker.</p>
 | 
				
			||||||
 | 
					              </header>
 | 
				
			||||||
 | 
					                <Navlinks className="md:mt-6" />
 | 
				
			||||||
 | 
					                <footer className="my-auto md:mt-auto flex justify-center"><ModeToggle /><LogoutButton />
 | 
				
			||||||
 | 
					                </footer>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div className="flex justify-center w-full">
 | 
				
			||||||
 | 
					                {children}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <Toaster />
 | 
				
			||||||
 | 
					        </ThemeProvider>
 | 
				
			||||||
 | 
					      </body>
 | 
				
			||||||
 | 
					    </html >
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,80 @@
 | 
				
			||||||
 | 
					"use server"
 | 
				
			||||||
 | 
					import { Pub, Story, Sub } from "@prisma/client"
 | 
				
			||||||
 | 
					import prisma from "./db"
 | 
				
			||||||
 | 
					import { revalidatePath } from "next/cache"
 | 
				
			||||||
 | 
					import { z } from "zod"
 | 
				
			||||||
 | 
					import { pubSchema } from "app/ui/forms/schemas"
 | 
				
			||||||
 | 
					import { subSchema } from "app/ui/forms/schemas"
 | 
				
			||||||
 | 
					import { prepGenreData, prepStoryData } from "./validate"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function createStory({ story, genres }: { story: Story, genres: number[] }): Promise<{ success: string }> {
 | 
				
			||||||
 | 
						// will return undefined if middleware authorization fails
 | 
				
			||||||
 | 
						"use server"
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							const storyData = await prepStoryData(story)
 | 
				
			||||||
 | 
							const genresArray = await prepGenreData(genres)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//submit
 | 
				
			||||||
 | 
							const res = await prisma.story.create({ data: storyData })
 | 
				
			||||||
 | 
							await prisma.story.update({
 | 
				
			||||||
 | 
								where: { id: res.id },
 | 
				
			||||||
 | 
								data: {
 | 
				
			||||||
 | 
									genres: { set: genresArray }
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							revalidatePath("/story")
 | 
				
			||||||
 | 
							return { success: `Created the story '${story.title}'.` }
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function createPub({ pub, genres }: { pub: Pub, genres: number[] }): Promise<{ success: string }> {
 | 
				
			||||||
 | 
						"use server"
 | 
				
			||||||
 | 
						const genresArray = genres.map(e => { return { id: e } })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						//prepare schemas
 | 
				
			||||||
 | 
						const schema = pubSchema.omit({ genres: true })
 | 
				
			||||||
 | 
						const genreSchema = z.object({ id: z.number() })
 | 
				
			||||||
 | 
						const genresSchema = z.array(genreSchema)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//validate
 | 
				
			||||||
 | 
							schema.parse(pub)
 | 
				
			||||||
 | 
							genresSchema.safeParse(genresArray)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//submit
 | 
				
			||||||
 | 
							const res = await prisma.pub.create({
 | 
				
			||||||
 | 
								data: pub
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							const genresRes = await prisma.pub.update({
 | 
				
			||||||
 | 
								where: { id: res.id },
 | 
				
			||||||
 | 
								data:
 | 
				
			||||||
 | 
									{ genres: { set: genresArray } }
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							revalidatePath("/publication")
 | 
				
			||||||
 | 
							return { success: `Created the publication '${pub.title}'.` }
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function createSub(data: Sub): Promise<Sub | boolean> {
 | 
				
			||||||
 | 
						"use server"
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							subSchema.parse(data)
 | 
				
			||||||
 | 
							const res = await prisma.sub.create({ data })
 | 
				
			||||||
 | 
							revalidatePath("/submission")
 | 
				
			||||||
 | 
							return res
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					"use server"
 | 
				
			||||||
 | 
					import { revalidatePath } from "next/cache";
 | 
				
			||||||
 | 
					import prisma from "./db";
 | 
				
			||||||
 | 
					import { redirect } from "next/navigation";
 | 
				
			||||||
 | 
					import { Pathname } from "app/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tableMap = {
 | 
				
			||||||
 | 
						"/story": "story",
 | 
				
			||||||
 | 
						"/publication": "pub",
 | 
				
			||||||
 | 
						"/submission": "sub"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteRecord(id: number, pathname: string): Promise<undefined | boolean> {
 | 
				
			||||||
 | 
						const table = tableMap[pathname]
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							//@ts-ignore
 | 
				
			||||||
 | 
							const res = await prisma[table].delete({ where: { id } })
 | 
				
			||||||
 | 
							console.log(`deleted from ${table}: ${res.id}`)
 | 
				
			||||||
 | 
							revalidatePath(pathname)
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
							return undefined
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteRecords(ids: number[], pathname: "/story" | "/publication" | "/submission"): Promise<boolean | undefined> {
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							const table = tableMap[pathname]
 | 
				
			||||||
 | 
							ids.forEach(async (id) => {
 | 
				
			||||||
 | 
								const res = await prisma[table].delete({
 | 
				
			||||||
 | 
									where: { id }
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								console.log(`deleted from ${table}: ${res.id}`)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							revalidatePath(pathname)
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
							return undefined
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					import { Genre } from "@prisma/client";
 | 
				
			||||||
 | 
					import { FilterFn, Row } from "@tanstack/react-table";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const genrePickerFilterFn = (row: Row<any>, columnId: string, filterValue: any) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const genres: Genre[] = row.getValue(columnId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (let index = 0; index < genres.length; index++) {
 | 
				
			||||||
 | 
							if (genres[genres.length - 1].name.includes(filterValue)) {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					export function letterCase(str: String) {
 | 
				
			||||||
 | 
						return str.charAt(0).toUpperCase() + str.slice(1)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					"use server"
 | 
				
			||||||
 | 
					import prisma from "./db"
 | 
				
			||||||
 | 
					export async function getStories() {
 | 
				
			||||||
 | 
						return prisma.story.findMany()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export async function getStoriesWithGenres() {
 | 
				
			||||||
 | 
						return prisma.story.findMany(
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								include: {
 | 
				
			||||||
 | 
									genres: true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export async function getStoriesWithGenresAndSubs() {
 | 
				
			||||||
 | 
						return prisma.story.findMany({
 | 
				
			||||||
 | 
							include: {
 | 
				
			||||||
 | 
								genres: true,
 | 
				
			||||||
 | 
								subs: true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getPubs() {
 | 
				
			||||||
 | 
						return prisma.pub.findMany()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export async function getPubsWithGenres() {
 | 
				
			||||||
 | 
						return prisma.pub.findMany({
 | 
				
			||||||
 | 
							include: { genres: true }
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getGenres() {
 | 
				
			||||||
 | 
						return prisma.genre.findMany()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export async function getResponses() {
 | 
				
			||||||
 | 
						return prisma.response.findMany()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getSubs() {
 | 
				
			||||||
 | 
						return prisma.sub.findMany()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getSubsComplete() {
 | 
				
			||||||
 | 
						return prisma.sub.findMany({
 | 
				
			||||||
 | 
							include: {
 | 
				
			||||||
 | 
								story: true,
 | 
				
			||||||
 | 
								pub: true,
 | 
				
			||||||
 | 
								response: true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					export function tableNameToItemName(tableName: string) {
 | 
				
			||||||
 | 
						const map = {
 | 
				
			||||||
 | 
							subs: "submission",
 | 
				
			||||||
 | 
							pubs: "publication",
 | 
				
			||||||
 | 
							story: "story"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return map[tableName]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					export default function pluralize(word: string): string {
 | 
				
			||||||
 | 
						const map = {
 | 
				
			||||||
 | 
							story: "stories",
 | 
				
			||||||
 | 
							publication: "publications",
 | 
				
			||||||
 | 
							submission: "submissions"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return map[word]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					function randomArrayItem(array: Array<string>) {
 | 
				
			||||||
 | 
					  return array[Math.floor(Math.random() * (array.length - 1))]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function randomStoryTitle() {
 | 
				
			||||||
 | 
					  const titles = [
 | 
				
			||||||
 | 
					    "The Signalman",
 | 
				
			||||||
 | 
					    "The Time Machine",
 | 
				
			||||||
 | 
					    "The Lawnmover Man",
 | 
				
			||||||
 | 
					    "La Casa de Adela",
 | 
				
			||||||
 | 
					    "The Tell-Tale Heart",
 | 
				
			||||||
 | 
					    "The Lottery",
 | 
				
			||||||
 | 
					    "The Birds",
 | 
				
			||||||
 | 
					    "The Minority Report",
 | 
				
			||||||
 | 
					    "The Bear Came Over the Mountain"
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					  return randomArrayItem(titles)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function randomPublicationTitle() {
 | 
				
			||||||
 | 
					  const titles = [
 | 
				
			||||||
 | 
					    "Nightmare Magazine",
 | 
				
			||||||
 | 
					    "Apex",
 | 
				
			||||||
 | 
					    "The Dark",
 | 
				
			||||||
 | 
					    "Reader's Digest",
 | 
				
			||||||
 | 
					    "The New Yorker",
 | 
				
			||||||
 | 
					    "Short Story Magazine",
 | 
				
			||||||
 | 
					    "Weird Tales",
 | 
				
			||||||
 | 
					    "Detective Stories"
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					  return randomArrayItem(titles)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,113 @@
 | 
				
			||||||
 | 
					"use server"
 | 
				
			||||||
 | 
					import { prepGenreData, prepPubData, prepStoryData } from "./validate"
 | 
				
			||||||
 | 
					import { Genre, Pub, Story, Sub } from "@prisma/client"
 | 
				
			||||||
 | 
					import prisma from "./db"
 | 
				
			||||||
 | 
					import { revalidatePath } from "next/cache"
 | 
				
			||||||
 | 
					import { subSchema } from "app/ui/forms/schemas"
 | 
				
			||||||
 | 
					import { SubForm } from "app/ui/forms/sub"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function updateField({ datum, table, column, id, pathname }: { datum?: string | number | Genre[], table: string, column: string, id: number, pathname: string }) {
 | 
				
			||||||
 | 
						"use server"
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							const res = await prisma[table].update({
 | 
				
			||||||
 | 
								where: { id },
 | 
				
			||||||
 | 
								data: {
 | 
				
			||||||
 | 
									[column]: datum
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							console.log(`updated record in ${table}: ${JSON.stringify(res)}`)
 | 
				
			||||||
 | 
							revalidatePath(pathname)
 | 
				
			||||||
 | 
							return res
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
							return null
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function updateGenres({ genres, table, id, pathname }: { genres: { id: number }[], table: string, id: number, pathname: string }) {
 | 
				
			||||||
 | 
						"use server"
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							const res = await prisma[table].update({
 | 
				
			||||||
 | 
								where: { id },
 | 
				
			||||||
 | 
								data: {
 | 
				
			||||||
 | 
									genres: { set: genres }
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							console.log(`updated record in ${table}: ${JSON.stringify(res)}`)
 | 
				
			||||||
 | 
							revalidatePath(pathname)
 | 
				
			||||||
 | 
							return res
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
							return null
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function updateSub(data: SubForm): Promise<Sub> {
 | 
				
			||||||
 | 
						"use server"
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							subSchema.parse(data)
 | 
				
			||||||
 | 
							const res = await prisma.sub.update({ where: { id: data.id }, data })
 | 
				
			||||||
 | 
							revalidatePath("submission")
 | 
				
			||||||
 | 
							return res
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
							return null
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function updateStory(data: Story & { genres: number[] }): Promise<{ success: string }> {
 | 
				
			||||||
 | 
						"use server"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							//prep and validate
 | 
				
			||||||
 | 
							const storyData = await prepStoryData(data)
 | 
				
			||||||
 | 
							const genresArray = await prepGenreData(data.genres)
 | 
				
			||||||
 | 
							//submit
 | 
				
			||||||
 | 
							const res = await prisma.story.update({
 | 
				
			||||||
 | 
								where: { id: data.id },
 | 
				
			||||||
 | 
								data: storyData
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							const genreRes = await prisma.story.update({
 | 
				
			||||||
 | 
								where: { id: data.id },
 | 
				
			||||||
 | 
								data: {
 | 
				
			||||||
 | 
									genres: { set: genresArray }
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							return { success: "Updated the story '" + res.title + "'." }
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
							return null
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function updatePub(data: Pub & { genres: number[] }): Promise<{ success: string }> {
 | 
				
			||||||
 | 
						"use server"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							//prep and validate
 | 
				
			||||||
 | 
							const pubData = await prepPubData
 | 
				
			||||||
 | 
								(data)
 | 
				
			||||||
 | 
							const genresArray = await prepGenreData(data.genres)
 | 
				
			||||||
 | 
							//submit
 | 
				
			||||||
 | 
							const res = await prisma.pub.update({
 | 
				
			||||||
 | 
								where: { id: data.id },
 | 
				
			||||||
 | 
								data: pubData
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							await prisma.pub.update({
 | 
				
			||||||
 | 
								where: { id: data.id },
 | 
				
			||||||
 | 
								data: {
 | 
				
			||||||
 | 
									genres: { set: genresArray }
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							return { success: "Updated the publication '" + res.title + "'" }
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
							return null
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { storySchema } from "app/ui/forms/schemas";
 | 
				
			||||||
 | 
					import { Pub, Story } from "@prisma/client";
 | 
				
			||||||
 | 
					import { pubSchema } from "app/ui/forms/schemas";
 | 
				
			||||||
 | 
					import { StoryWithGenres } from "app/story/page";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//schemas
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const storySchemaTrimmed = storySchema.omit({ genres: true })
 | 
				
			||||||
 | 
					const pubSchemaTrimmed = pubSchema.omit({ genres: true })
 | 
				
			||||||
 | 
					const genreSchema = z.object({ id: z.number() })
 | 
				
			||||||
 | 
					const genresSchema = z.array(genreSchema)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function prepStoryData(data: Story): Promise<{ title: string, word_count: number }> {
 | 
				
			||||||
 | 
						const storyData = structuredClone(data)
 | 
				
			||||||
 | 
						//throw an error if validation fails
 | 
				
			||||||
 | 
						storySchemaTrimmed.safeParse(storyData)
 | 
				
			||||||
 | 
						return storyData
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function prepPubData(data: Pub): Promise<Pub> {
 | 
				
			||||||
 | 
						const pubData = structuredClone(data)
 | 
				
			||||||
 | 
						pubSchemaTrimmed.safeParse(pubData)
 | 
				
			||||||
 | 
						return pubData
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function prepGenreData(data: number[]): Promise<{ id: number }[]> {
 | 
				
			||||||
 | 
						"use server"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						//prepare data
 | 
				
			||||||
 | 
						const genresArray = data.map((e) => { return { id: e } })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						//prepare schemas
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						//throws error if validation fails
 | 
				
			||||||
 | 
						genresSchema.safeParse(genresArray)
 | 
				
			||||||
 | 
						return genresArray
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					import { ComponentProps } from "react"
 | 
				
			||||||
 | 
					export const LoadingSpinner = ({ className }: ComponentProps<"svg">) => {
 | 
				
			||||||
 | 
					  return <div className="size-full flex justify-center items-center z">
 | 
				
			||||||
 | 
					    <svg
 | 
				
			||||||
 | 
					      xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					      width="100"
 | 
				
			||||||
 | 
					      height="100"
 | 
				
			||||||
 | 
					      viewBox="0 0 24 24"
 | 
				
			||||||
 | 
					      fill="none"
 | 
				
			||||||
 | 
					      stroke="currentColor"
 | 
				
			||||||
 | 
					      strokeWidth="2"
 | 
				
			||||||
 | 
					      strokeLinecap="round"
 | 
				
			||||||
 | 
					      strokeLinejoin="round"
 | 
				
			||||||
 | 
					      className={cn("animate-spin", className)}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <path d="M21 12a9 9 0 1 1-6.219-8.56" />
 | 
				
			||||||
 | 
					    </svg>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default function Loading() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <LoadingSpinner />
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,86 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
 | 
					import { toast } from "@/components/ui/use-toast";
 | 
				
			||||||
 | 
					import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
 | 
				
			||||||
 | 
					import { Input } from "@/components/ui/input";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { redirect } from "next/navigation";
 | 
				
			||||||
 | 
					import { loginSchema } from "./schema";
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation";
 | 
				
			||||||
 | 
					import { useSearchParams } from "next/navigation";
 | 
				
			||||||
 | 
					import revalidate from "./revalidate";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import Link from "next/link";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function LoginForm() {
 | 
				
			||||||
 | 
						const router = useRouter()
 | 
				
			||||||
 | 
						const searchParams = useSearchParams()
 | 
				
			||||||
 | 
						const redirect = searchParams.get("from") ?? "/submission"
 | 
				
			||||||
 | 
						const form = useForm<z.infer<typeof loginSchema>>({
 | 
				
			||||||
 | 
							resolver: zodResolver(loginSchema),
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						const [submitted, setSubmitted] = useState(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onSubmit = form.handleSubmit(async (data, event) => {
 | 
				
			||||||
 | 
							event.preventDefault()
 | 
				
			||||||
 | 
							const res = await fetch('/api/auth/login', {
 | 
				
			||||||
 | 
								method: 'POST',
 | 
				
			||||||
 | 
								headers: {
 | 
				
			||||||
 | 
									'Content-type': 'application/json',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								body: JSON.stringify(data),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if (res.status === 200) {
 | 
				
			||||||
 | 
								toast({ title: "login successful!" })
 | 
				
			||||||
 | 
								setSubmitted(true)
 | 
				
			||||||
 | 
								await revalidate(redirect)
 | 
				
			||||||
 | 
								//BUG:the first time user logs in, page refreshes instead of redirecting
 | 
				
			||||||
 | 
								router.push(redirect)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								toast({ title: "login failed!" })
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<>
 | 
				
			||||||
 | 
								{submitted ? <p>Logging in...</p> :
 | 
				
			||||||
 | 
									<Form {...form}>
 | 
				
			||||||
 | 
										<form onSubmit={onSubmit} className="mt-20 flex flex-col items-center space-y-6">
 | 
				
			||||||
 | 
											<FormField
 | 
				
			||||||
 | 
												control={form.control}
 | 
				
			||||||
 | 
												name="email"
 | 
				
			||||||
 | 
												render={({ field }) => (
 | 
				
			||||||
 | 
													<FormItem>
 | 
				
			||||||
 | 
														<FormLabel>Email Address</FormLabel>
 | 
				
			||||||
 | 
														<FormControl>
 | 
				
			||||||
 | 
															<Input placeholder="email goes here" {...field} />
 | 
				
			||||||
 | 
														</FormControl>
 | 
				
			||||||
 | 
														<FormMessage />
 | 
				
			||||||
 | 
													</FormItem>
 | 
				
			||||||
 | 
												)}
 | 
				
			||||||
 | 
											></FormField>
 | 
				
			||||||
 | 
											<FormField
 | 
				
			||||||
 | 
												control={form.control}
 | 
				
			||||||
 | 
												name="password"
 | 
				
			||||||
 | 
												render={({ field }) => (
 | 
				
			||||||
 | 
													<FormItem>
 | 
				
			||||||
 | 
														<FormLabel>Password</FormLabel>
 | 
				
			||||||
 | 
														<FormControl>
 | 
				
			||||||
 | 
															<Input placeholder="password goes here" type="password"{...field} />
 | 
				
			||||||
 | 
														</FormControl>
 | 
				
			||||||
 | 
														<FormMessage />
 | 
				
			||||||
 | 
													</FormItem>
 | 
				
			||||||
 | 
												)}
 | 
				
			||||||
 | 
											></FormField>
 | 
				
			||||||
 | 
											<Button type="submit" className="mt-4">SUBMIT</Button>
 | 
				
			||||||
 | 
										</form>
 | 
				
			||||||
 | 
									</Form>
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							</>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					"use server"
 | 
				
			||||||
 | 
					import { revalidatePath } from "next/cache"
 | 
				
			||||||
 | 
					export default async function revalidate(path: string) {
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							revalidatePath(path)
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error(error)
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					import { z } from "zod"
 | 
				
			||||||
 | 
					export const loginSchema = z.object({
 | 
				
			||||||
 | 
						email: z.string().email(),
 | 
				
			||||||
 | 
						password: z.string().min(6)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type LoginSchema = z.infer<typeof loginSchema>
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
					@ -5,7 +5,9 @@ export default function Home() {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <main className={styles.main}>
 | 
					    <main className={styles.main}>
 | 
				
			||||||
      Hello
 | 
					      Hello
 | 
				
			||||||
 | 
					      <h1 className="text-3xl font-black underline">
 | 
				
			||||||
 | 
					        Hello world!
 | 
				
			||||||
 | 
					      </h1>
 | 
				
			||||||
    </main>
 | 
					    </main>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,37 @@
 | 
				
			||||||
 | 
					import prisma from "app/lib/db";
 | 
				
			||||||
 | 
					import { columns } from "app/submission/columns";
 | 
				
			||||||
 | 
					import GenreBadges from "app/ui/genreBadges";
 | 
				
			||||||
 | 
					import { PageHeader, PageSubHeader } from "app/ui/pageHeader";
 | 
				
			||||||
 | 
					import { DataTable } from "app/ui/tables/data-table";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getPubWithGenres(id: string) {
 | 
				
			||||||
 | 
					  return prisma.pub.findFirst({
 | 
				
			||||||
 | 
					    where: { id: Number(id) },
 | 
				
			||||||
 | 
					    include: { genres: true }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getPubSubmissions(id: string) {
 | 
				
			||||||
 | 
					  return prisma.sub.findMany({
 | 
				
			||||||
 | 
					    where: { storyId: Number(id) },
 | 
				
			||||||
 | 
					    include: {
 | 
				
			||||||
 | 
					      story: true,
 | 
				
			||||||
 | 
					      pub: true,
 | 
				
			||||||
 | 
					      response: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function Page({ params }: { params: { id: string } }) {
 | 
				
			||||||
 | 
					  const pub = await getPubWithGenres(params.id)
 | 
				
			||||||
 | 
					  const pubSubs = await getPubSubmissions(params.id)
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="container">
 | 
				
			||||||
 | 
					      <PageHeader>{pub.title}</PageHeader>
 | 
				
			||||||
 | 
					      <GenreBadges genres={pub.genres} className="my-6" />
 | 
				
			||||||
 | 
					      <PageSubHeader>Submissions:</PageSubHeader>
 | 
				
			||||||
 | 
					      <DataTable columns={columns} data={pubSubs} tableName="sub" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,97 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
 | 
				
			||||||
 | 
					import { ArrowUpDown, BookType, Clock, Drama, SquareArrowOutUpRight } from "lucide-react"
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button"
 | 
				
			||||||
 | 
					import { PubWithGenres } from "./page"
 | 
				
			||||||
 | 
					import { TextInputCell } from "app/ui/tables/inputs/textInput"
 | 
				
			||||||
 | 
					import { selectCol } from "app/ui/tables/selectColumn"
 | 
				
			||||||
 | 
					import NumberInputCell from "app/ui/tables/inputs/numberInput"
 | 
				
			||||||
 | 
					import { pubSchema } from "app/ui/forms/schemas"
 | 
				
			||||||
 | 
					import GenrePickerInputCell from "app/ui/tables/inputs/genrePickerInput"
 | 
				
			||||||
 | 
					import { genrePickerFilterFn } from "app/lib/filterFns"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const columnHelper = createColumnHelper<PubWithGenres>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const columns: ColumnDef<PubWithGenres>[] = [
 | 
				
			||||||
 | 
					  selectCol,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    accessorKey: "title",
 | 
				
			||||||
 | 
					    header: ({ column }) => {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="ghost"
 | 
				
			||||||
 | 
					          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <span className="hidden sm:block">
 | 
				
			||||||
 | 
					            Title
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					          <span className="block sm:hidden"><BookType /></span>
 | 
				
			||||||
 | 
					          <ArrowUpDown className="ml-2 h-4 w-4 hidden md:block" />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    cell: cell => (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        {/* @ts-ignore */}
 | 
				
			||||||
 | 
					        <p className="block text-xs max-w-24 break-words md:hidden">{cell.getValue()}</p>
 | 
				
			||||||
 | 
					        <TextInputCell cellContext={cell} className="hidden md:block" />
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    meta: { formSchema: pubSchema }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    accessorKey: "link",
 | 
				
			||||||
 | 
					    header: () => (
 | 
				
			||||||
 | 
					      <div className="mx-auto w-fit">
 | 
				
			||||||
 | 
					        <span className="hidden sm:block">Link</span>
 | 
				
			||||||
 | 
					        <span className="block sm:hidden"><SquareArrowOutUpRight /></span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    cell: cell => (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        {/* @ts-ignore */}
 | 
				
			||||||
 | 
					        <p className="block text-xs max-w-16 truncate md:hidden">{cell.getValue()}</p>
 | 
				
			||||||
 | 
					        <TextInputCell cellContext={cell} className="hidden md:block" />
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    meta: { formSchema: pubSchema }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  columnHelper.accessor("genres", {
 | 
				
			||||||
 | 
					    header: () => (
 | 
				
			||||||
 | 
					      <div className="w-fit mx-auto">
 | 
				
			||||||
 | 
					        <span className="hidden sm:block">Genres</span>
 | 
				
			||||||
 | 
					        <span className="sm:hidden"><Drama /></span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    cell: GenrePickerInputCell,
 | 
				
			||||||
 | 
					    filterFn: genrePickerFilterFn
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    accessorKey: "query_after_days",
 | 
				
			||||||
 | 
					    header: () => (
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        <span className="hidden sm:block">Query After (days)</span>
 | 
				
			||||||
 | 
					        <span className="sm:hidden"><Clock /></span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    enableColumnFilter: false,
 | 
				
			||||||
 | 
					    cell: cell => (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        {/* @ts-ignore */}
 | 
				
			||||||
 | 
					        <p className="block md:hidden text-center">{cell.getValue()}</p>
 | 
				
			||||||
 | 
					        <NumberInputCell cellContext={cell} className="hidden md:block" />
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      step: 10,
 | 
				
			||||||
 | 
					      formSchema: pubSchema
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { Dialog, DialogHeader, DialogTrigger, DialogContent, DialogClose, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { ComponentProps } from "react";
 | 
				
			||||||
 | 
					import { Genre } from "@prisma/client";
 | 
				
			||||||
 | 
					import { createPub } from "app/lib/create";
 | 
				
			||||||
 | 
					import PubForm from "app/ui/forms/pub";
 | 
				
			||||||
 | 
					import { Plus } from "lucide-react";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function CreatePubDialog({ genres }: ComponentProps<"div"> & { genres: Genre[] }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [isOpen, setIsOpen] = useState(false)
 | 
				
			||||||
 | 
					  function closeDialog() {
 | 
				
			||||||
 | 
					    setIsOpen(false)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <Dialog open={isOpen} onOpenChange={setIsOpen}>
 | 
				
			||||||
 | 
					      <DialogTrigger asChild>
 | 
				
			||||||
 | 
					        <Button>
 | 
				
			||||||
 | 
					          <span className="hidden md:block">Create new publication</span>
 | 
				
			||||||
 | 
					          <Plus className="block md:hidden" />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </DialogTrigger>
 | 
				
			||||||
 | 
					      <DialogContent>
 | 
				
			||||||
 | 
					        <DialogHeader>
 | 
				
			||||||
 | 
					          <DialogTitle>New publication</DialogTitle>
 | 
				
			||||||
 | 
					          <DialogDescription>Create an entry for a new publication i.e. a place you intend to submit stories to.</DialogDescription>
 | 
				
			||||||
 | 
					        </DialogHeader>
 | 
				
			||||||
 | 
					        <PubForm dbAction={createPub} genres={genres} closeDialog={closeDialog} />
 | 
				
			||||||
 | 
					        <DialogFooter>
 | 
				
			||||||
 | 
					          <Button form="pubform">Submit</Button>
 | 
				
			||||||
 | 
					        </DialogFooter>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      </DialogContent>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { Dialog, DialogHeader, DialogTrigger, DialogContent, DialogClose, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { ComponentProps } from "react";
 | 
				
			||||||
 | 
					import { Genre, Pub } from "@prisma/client";
 | 
				
			||||||
 | 
					import { createPub } from "app/lib/create";
 | 
				
			||||||
 | 
					import PubForm from "app/ui/forms/pub";
 | 
				
			||||||
 | 
					import { Plus } from "lucide-react";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { PubWithGenres } from "./page";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function EditPubDialog({ genres, closeDialog, defaults, dbAction }: ComponentProps<"div"> & { genres: Genre[], closeDialog: () => void, defaults: PubWithGenres, dbAction: (data: Pub & { genres: number[] }) => Promise<{ success: string }> }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <DialogHeader>
 | 
				
			||||||
 | 
					        <DialogTitle>Edit publication</DialogTitle>
 | 
				
			||||||
 | 
					        <DialogDescription>Modify an entry for an existing publication.</DialogDescription>
 | 
				
			||||||
 | 
					      </DialogHeader>
 | 
				
			||||||
 | 
					      <PubForm dbAction={dbAction} genres={genres} closeDialog={closeDialog} defaults={defaults} />
 | 
				
			||||||
 | 
					      <DialogFooter>
 | 
				
			||||||
 | 
					        <Button form="pubform">Submit</Button>
 | 
				
			||||||
 | 
					      </DialogFooter>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					import { LoadingSpinner } from "app/loading";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Loading() {
 | 
				
			||||||
 | 
					  return <LoadingSpinner />
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					import { Genre, Pub } from "@prisma/client";
 | 
				
			||||||
 | 
					import { getGenres, getPubsWithGenres } from "app/lib/get";
 | 
				
			||||||
 | 
					import { columns } from "./columns";
 | 
				
			||||||
 | 
					import { DataTable } from "app/ui/tables/data-table";
 | 
				
			||||||
 | 
					import CreatePubDialog from "./create";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type PubWithGenres = Pub & { genres: Array<Genre> }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function Page() {
 | 
				
			||||||
 | 
					  const genres = await getGenres()
 | 
				
			||||||
 | 
					  const pubs = await getPubsWithGenres()
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="container px-0 md:px-4 mx-auto">
 | 
				
			||||||
 | 
					      <DataTable data={pubs} columns={columns} tableName="pub" genres={genres}>
 | 
				
			||||||
 | 
					        <CreatePubDialog genres={genres} />
 | 
				
			||||||
 | 
					      </DataTable>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					.theme-rp {
 | 
				
			||||||
 | 
					  --rp-base: 249 22 12;
 | 
				
			||||||
 | 
					  --rp-surface: 247 23 15;
 | 
				
			||||||
 | 
					  --rp-overlay: 248 25 18;
 | 
				
			||||||
 | 
					  --rp-muted: 249 12 47;
 | 
				
			||||||
 | 
					  --rp-subtle: 248 15 61;
 | 
				
			||||||
 | 
					  --rp-text: 245 50 91;
 | 
				
			||||||
 | 
					  --rp-love: 343 76 68;
 | 
				
			||||||
 | 
					  --rp-gold: 35 88 72;
 | 
				
			||||||
 | 
					  --rp-rose: 2 55 83;
 | 
				
			||||||
 | 
					  --rp-pine: 197 49 38;
 | 
				
			||||||
 | 
					  --rp-foam: 189 43 73;
 | 
				
			||||||
 | 
					  --rp-iris: 267 57 78;
 | 
				
			||||||
 | 
					  --rp-highlight-low: 244 18 15;
 | 
				
			||||||
 | 
					  --rp-highlight-med: 249 15 28;
 | 
				
			||||||
 | 
					  --rp-highlight-high: 248 13 36;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.theme-rp-moon {
 | 
				
			||||||
 | 
					  --rp-base: 246 24 17;
 | 
				
			||||||
 | 
					  --rp-surface: 248 24 20;
 | 
				
			||||||
 | 
					  --rp-overlay: 248 21 26;
 | 
				
			||||||
 | 
					  --rp-muted: 249 12 47;
 | 
				
			||||||
 | 
					  --rp-subtle: 248 15 61;
 | 
				
			||||||
 | 
					  --rp-text: 245 50 91;
 | 
				
			||||||
 | 
					  --rp-love: 343 76 68;
 | 
				
			||||||
 | 
					  --rp-gold: 35 88 72;
 | 
				
			||||||
 | 
					  --rp-rose: 2 66 75;
 | 
				
			||||||
 | 
					  --rp-pine: 197 48 47;
 | 
				
			||||||
 | 
					  --rp-foam: 189 43 73;
 | 
				
			||||||
 | 
					  --rp-iris: 267 57 78;
 | 
				
			||||||
 | 
					  --rp-highlight-low: 245 22 20;
 | 
				
			||||||
 | 
					  --rp-highlight-med: 247 16 30;
 | 
				
			||||||
 | 
					  --rp-highlight-high: 249 15 38;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.theme-rp-dawn {
 | 
				
			||||||
 | 
					  --rp-base: 32 57 95;
 | 
				
			||||||
 | 
					  --rp-surface: 35 100 98;
 | 
				
			||||||
 | 
					  --rp-overlay: 33 43 91;
 | 
				
			||||||
 | 
					  --rp-muted: 257 9 61;
 | 
				
			||||||
 | 
					  --rp-subtle: 248 12 52;
 | 
				
			||||||
 | 
					  --rp-text: 248 19 40;
 | 
				
			||||||
 | 
					  --rp-love: 343 35 55;
 | 
				
			||||||
 | 
					  --rp-gold: 35 81 56;
 | 
				
			||||||
 | 
					  --rp-rose: 3 53 67;
 | 
				
			||||||
 | 
					  --rp-pine: 197 53 34;
 | 
				
			||||||
 | 
					  --rp-foam: 189 30 48;
 | 
				
			||||||
 | 
					  --rp-iris: 268 21 57;
 | 
				
			||||||
 | 
					  --rp-highlight-low: 25 35 93;
 | 
				
			||||||
 | 
					  --rp-highlight-med: 10 9 86;
 | 
				
			||||||
 | 
					  --rp-highlight-high: 315 4 80;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,41 @@
 | 
				
			||||||
 | 
					import prisma from "app/lib/db"
 | 
				
			||||||
 | 
					import { columns } from "app/submission/columns"
 | 
				
			||||||
 | 
					import { PageHeader, PageSubHeader } from "app/ui/pageHeader"
 | 
				
			||||||
 | 
					import { DataTable } from "app/ui/tables/data-table"
 | 
				
			||||||
 | 
					import GenreBadges from "app/ui/genreBadges"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//ids are string here because they're coming from url params
 | 
				
			||||||
 | 
					async function getStoryWithGenres(id: string) {
 | 
				
			||||||
 | 
					  return prisma.story.findFirst({
 | 
				
			||||||
 | 
					    where: { id: Number(id) }, include: {
 | 
				
			||||||
 | 
					      genres: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					async function getStorySubmissions(id: string) {
 | 
				
			||||||
 | 
					  return prisma.sub.findMany({
 | 
				
			||||||
 | 
					    where: { storyId: Number(id) }, include: {
 | 
				
			||||||
 | 
					      story: true,
 | 
				
			||||||
 | 
					      pub: true,
 | 
				
			||||||
 | 
					      response: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function Page({ params }: { params: { id: string } }) {
 | 
				
			||||||
 | 
					  const story = await getStoryWithGenres(params.id)
 | 
				
			||||||
 | 
					  const storySubs = await getStorySubmissions(params.id)
 | 
				
			||||||
 | 
					  return <>
 | 
				
			||||||
 | 
					    <div className="container">
 | 
				
			||||||
 | 
					      <PageHeader>{story?.title ?? ""}</PageHeader>
 | 
				
			||||||
 | 
					      <GenreBadges genres={story.genres} className="my-6" />
 | 
				
			||||||
 | 
					      <PageSubHeader>Submissions:</PageSubHeader>
 | 
				
			||||||
 | 
					      <DataTable columns={columns} data={storySubs} tableName="subs" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  </>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +0,0 @@
 | 
				
			||||||
export default function Page({ params }: { params: { slug: string } }) {
 | 
					 | 
				
			||||||
  return <div>My Post: {params.slug}</div>
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,87 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
 | 
				
			||||||
 | 
					import { StoryWithGenres } from "./page"
 | 
				
			||||||
 | 
					import { ArrowUpDown, BookType, Drama, Tally5 } from "lucide-react"
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button"
 | 
				
			||||||
 | 
					import { selectCol } from "app/ui/tables/selectColumn"
 | 
				
			||||||
 | 
					import NumberInputCell from "app/ui/tables/inputs/numberInput"
 | 
				
			||||||
 | 
					import { storySchema } from "app/ui/forms/schemas"
 | 
				
			||||||
 | 
					import { TextInputCell } from "app/ui/tables/inputs/textInput"
 | 
				
			||||||
 | 
					import GenrePickerInputCell from "app/ui/tables/inputs/genrePickerInput"
 | 
				
			||||||
 | 
					import { genrePickerFilterFn } from "app/lib/filterFns"
 | 
				
			||||||
 | 
					const columnHelper = createColumnHelper<StoryWithGenres>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const columns: ColumnDef<StoryWithGenres>[] = [
 | 
				
			||||||
 | 
					  selectCol,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    accessorKey: "title",
 | 
				
			||||||
 | 
					    header: ({ column }) => {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="ghost"
 | 
				
			||||||
 | 
					          className="px-1"
 | 
				
			||||||
 | 
					          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <span className="hidden sm:block">
 | 
				
			||||||
 | 
					            Title
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					          <span className="block sm:hidden"><BookType /></span>
 | 
				
			||||||
 | 
					          <ArrowUpDown className="ml-2 h-4 w-4 hidden md:block" />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    cell: cell => (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        {/* @ts-ignore */}
 | 
				
			||||||
 | 
					        <p className="block break-words max-w-28 md:hidden text-xs">{cell.getValue()}</p>
 | 
				
			||||||
 | 
					        <TextInputCell cellContext={cell} className="hidden md:block" />
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    meta: { formSchema: storySchema }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    accessorKey: "word_count",
 | 
				
			||||||
 | 
					    header: ({ column }) => {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="ghost"
 | 
				
			||||||
 | 
					          className="px-1"
 | 
				
			||||||
 | 
					          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <span className="hidden sm:block">
 | 
				
			||||||
 | 
					            Word Count
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					          <span className="sm:hidden">
 | 
				
			||||||
 | 
					            <Tally5 />
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					          <ArrowUpDown className="ml-2 h-4 w-4 hidden md:block" />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    enableColumnFilter: false,
 | 
				
			||||||
 | 
					    cell: cell => (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        {/* @ts-ignore */}
 | 
				
			||||||
 | 
					        <p className="block md:hidden text-center text-xs">{cell.getValue()}</p>
 | 
				
			||||||
 | 
					        <NumberInputCell cellContext={cell} className="hidden md:block" />
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      step: 50,
 | 
				
			||||||
 | 
					      formSchema: storySchema
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  columnHelper.accessor("genres", {
 | 
				
			||||||
 | 
					    header: () => (
 | 
				
			||||||
 | 
					      <div className="w-fit mx-auto">
 | 
				
			||||||
 | 
					        <span className="hidden sm:block">Genres</span>
 | 
				
			||||||
 | 
					        <span className="sm:hidden"><Drama /></span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    cell: GenrePickerInputCell,
 | 
				
			||||||
 | 
					    filterFn: genrePickerFilterFn,
 | 
				
			||||||
 | 
					    meta: {}
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { createStory } from "app/lib/create"
 | 
				
			||||||
 | 
					import { Dialog, DialogHeader, DialogTrigger, DialogContent, DialogClose, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { ComponentProps, useState } from "react";
 | 
				
			||||||
 | 
					import { Genre } from "@prisma/client";
 | 
				
			||||||
 | 
					import StoryForm from "app/ui/forms/story";
 | 
				
			||||||
 | 
					import { Plus } from "lucide-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function CreateStoryDialog({ genres }: ComponentProps<"div"> & { genres: Genre[] }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [isOpen, setIsOpen] = useState(false)
 | 
				
			||||||
 | 
					  function closeDialog() {
 | 
				
			||||||
 | 
					    setIsOpen(false)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog open={isOpen} onOpenChange={setIsOpen}>
 | 
				
			||||||
 | 
					      <DialogTrigger asChild>
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <Button className="hidden md:block">Create new story</Button>
 | 
				
			||||||
 | 
					          <Button className="block md:hidden"><Plus /> </Button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </DialogTrigger>
 | 
				
			||||||
 | 
					      <DialogContent>
 | 
				
			||||||
 | 
					        <DialogHeader>
 | 
				
			||||||
 | 
					          <DialogTitle>New story</DialogTitle>
 | 
				
			||||||
 | 
					          <DialogDescription>Create an entry for a new story i.e. a thing you intend to submit for publication.</DialogDescription>
 | 
				
			||||||
 | 
					        </DialogHeader>
 | 
				
			||||||
 | 
					        <StoryForm dbAction={createStory} genres={genres} className="" closeDialog={closeDialog} />
 | 
				
			||||||
 | 
					        <DialogFooter>
 | 
				
			||||||
 | 
					          <Button form="storyform">Submit</Button>
 | 
				
			||||||
 | 
					        </DialogFooter>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      </DialogContent>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,41 @@
 | 
				
			||||||
 | 
					import { getGenres } from "app/lib/get";
 | 
				
			||||||
 | 
					import StoryForm from "app/ui/forms/story";
 | 
				
			||||||
 | 
					import prisma from "app/lib/db";
 | 
				
			||||||
 | 
					import { revalidatePath } from "next/cache";
 | 
				
			||||||
 | 
					import { redirect } from "next/navigation";
 | 
				
			||||||
 | 
					import { CreateContainerContent, CreateContainerHeader, CreateContainer, CreateContainerDescription } from "app/ui/createContainer";
 | 
				
			||||||
 | 
					import { Story } from "@prisma/client";
 | 
				
			||||||
 | 
					export default async function Page() {
 | 
				
			||||||
 | 
						const genres = await getGenres()
 | 
				
			||||||
 | 
						async function createStory(data: Story & { genres: number[] }): Promise<{ success: string }> {
 | 
				
			||||||
 | 
							"use server"
 | 
				
			||||||
 | 
							const genresArray = data.genres.map(e => { return { id: e } })
 | 
				
			||||||
 | 
							const res = await prisma.story.create({
 | 
				
			||||||
 | 
								data: {
 | 
				
			||||||
 | 
									title: data.title,
 | 
				
			||||||
 | 
									word_count: data.word_count,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							console.log(res)
 | 
				
			||||||
 | 
							const genresRes = await prisma.story.update({
 | 
				
			||||||
 | 
								where: { id: res.id },
 | 
				
			||||||
 | 
								data: {
 | 
				
			||||||
 | 
									genres: { set: genresArray }
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							console.log(genresRes)
 | 
				
			||||||
 | 
							revalidatePath("/story")
 | 
				
			||||||
 | 
							redirect("/story")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<CreateContainer>
 | 
				
			||||||
 | 
								<CreateContainerHeader>New story</CreateContainerHeader>
 | 
				
			||||||
 | 
								<CreateContainerContent>
 | 
				
			||||||
 | 
									<CreateContainerDescription>Make an entry for a new work of fiction i.e. a thing you intend to submit for publication.</CreateContainerDescription>
 | 
				
			||||||
 | 
									<StoryForm genres={genres} dbAction={createStory} className="mt-6" />
 | 
				
			||||||
 | 
								</CreateContainerContent>
 | 
				
			||||||
 | 
							</CreateContainer>
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,27 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { ComponentProps, useState } from "react";
 | 
				
			||||||
 | 
					import { Genre, Story } from "@prisma/client";
 | 
				
			||||||
 | 
					import StoryForm from "app/ui/forms/story";
 | 
				
			||||||
 | 
					import { StoryWithGenres } from "./page";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function EditStoryDialog({ genres, closeDialog, defaults, dbAction }: ComponentProps<"div"> & { genres: Genre[], closeDialog: () => void, defaults: StoryWithGenres, dbAction: (data: Story & { genres: number[] }) => Promise<{ success: string }> }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <DialogHeader>
 | 
				
			||||||
 | 
					        <DialogTitle>Edit story</DialogTitle>
 | 
				
			||||||
 | 
					        <DialogDescription>Create an entry for a new story i.e. a thing you intend to submit for publication.</DialogDescription>
 | 
				
			||||||
 | 
					      </DialogHeader>
 | 
				
			||||||
 | 
					      <StoryForm dbAction={dbAction} genres={genres} className="" closeDialog={closeDialog} defaults={defaults} />
 | 
				
			||||||
 | 
					      <DialogFooter>
 | 
				
			||||||
 | 
					        <Button form="storyform">Submit</Button>
 | 
				
			||||||
 | 
					      </DialogFooter>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					import { LoadingSpinner } from "app/loading";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Loading() {
 | 
				
			||||||
 | 
					  return <LoadingSpinner />
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					import { Story } from "@prisma/client";
 | 
				
			||||||
 | 
					import { DataTable } from "app/ui/tables/data-table";
 | 
				
			||||||
 | 
					import { columns } from "./columns";
 | 
				
			||||||
 | 
					import { getGenres, getStoriesWithGenres, getPubsWithGenres } from "app/lib/get";
 | 
				
			||||||
 | 
					import { Genre } from "@prisma/client";
 | 
				
			||||||
 | 
					import CreateStoryDialog from "./create";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type StoryWithGenres = Story & { genres: Array<Genre> }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function Page() {
 | 
				
			||||||
 | 
					  const genres = await getGenres()
 | 
				
			||||||
 | 
					  const storiesWithGenres: Array<StoryWithGenres> = await getStoriesWithGenres()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="container px-1 md:px-4 mx-auto">
 | 
				
			||||||
 | 
					      <DataTable columns={columns} data={storiesWithGenres} tableName="story"
 | 
				
			||||||
 | 
					        genres={genres}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <CreateStoryDialog genres={genres} />
 | 
				
			||||||
 | 
					      </DataTable>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,121 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { CellContext, ColumnDef, createColumnHelper } from "@tanstack/react-table"
 | 
				
			||||||
 | 
					import { ArrowUpDown, BookText, CalendarMinus, CalendarPlus, MessageCircleReply, NotepadText } from "lucide-react"
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button"
 | 
				
			||||||
 | 
					import { SubComplete } from "./page"
 | 
				
			||||||
 | 
					import { selectCol } from "app/ui/tables/selectColumn"
 | 
				
			||||||
 | 
					import TitleContainer from "app/ui/titleContainer"
 | 
				
			||||||
 | 
					import { CalendarArrowUp } from "lucide"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const columns: ColumnDef<SubComplete>[] = [
 | 
				
			||||||
 | 
					  selectCol,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    accessorFn: row => {
 | 
				
			||||||
 | 
					      if (row.story) {
 | 
				
			||||||
 | 
					        return row.story.title
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return "RECORD DELETED"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    id: "story",
 | 
				
			||||||
 | 
					    header: () => (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        <span className="hidden md:block">Story</span>
 | 
				
			||||||
 | 
					        <NotepadText className="block md:hidden" />
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    cell: (props: CellContext<any, any>) => (<TitleContainer>{props.getValue()}</TitleContainer>)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    accessorFn: row => {
 | 
				
			||||||
 | 
					      if (row.pub) {
 | 
				
			||||||
 | 
					        return row.pub.title
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return "RECORD DELETED"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    id: "pub",
 | 
				
			||||||
 | 
					    header: () => (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        <span className="hidden md:block">Publication</span>
 | 
				
			||||||
 | 
					        <BookText className="block md:hidden" />
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    cell: (props: CellContext<any, any>) => (<TitleContainer>{props.getValue()}</TitleContainer>)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    accessorFn: row => new Date(row.submitted),
 | 
				
			||||||
 | 
					    id: "submitted",
 | 
				
			||||||
 | 
					    header: ({ column }) => {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="ghost"
 | 
				
			||||||
 | 
					          className="p-0 flex justify-center w-full"
 | 
				
			||||||
 | 
					          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <span className="hidden md:block"> Date Submitted </span>
 | 
				
			||||||
 | 
					          <CalendarPlus className="block md:hidden" />
 | 
				
			||||||
 | 
					          <ArrowUpDown className="ml-2 h-4 w-4 hidden md:block" />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    enableColumnFilter: false,
 | 
				
			||||||
 | 
					    sortingFn: "datetime",
 | 
				
			||||||
 | 
					    cell: (props: CellContext<any, any>) => (
 | 
				
			||||||
 | 
					      <p className="w-full text-center text-xs md:text-sm">{props.getValue().toLocaleDateString('ES', {
 | 
				
			||||||
 | 
					        day: 'numeric',
 | 
				
			||||||
 | 
					        month: 'numeric',
 | 
				
			||||||
 | 
					        year: '2-digit'
 | 
				
			||||||
 | 
					      })}</p>
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    accessorFn: row => row.responded ? new Date(row.responded) : null,
 | 
				
			||||||
 | 
					    id: "responded",
 | 
				
			||||||
 | 
					    header: ({ column }) => {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="ghost"
 | 
				
			||||||
 | 
					          className="p-0 flex justify-center w-full"
 | 
				
			||||||
 | 
					          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <span className="hidden md:block"> Date Responded </span>
 | 
				
			||||||
 | 
					          <CalendarMinus className="block md:hidden" />
 | 
				
			||||||
 | 
					          <ArrowUpDown className="ml-2 h-4 w-4 hidden md:block" />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    enableColumnFilter: false,
 | 
				
			||||||
 | 
					    sortingFn: "datetime",
 | 
				
			||||||
 | 
					    cell: (props: CellContext<any, any>) => (<p className="w-full text-center text-xs md:text-sm">{props.getValue()?.toLocaleDateString('ES', {
 | 
				
			||||||
 | 
					      day: 'numeric',
 | 
				
			||||||
 | 
					      month: 'numeric',
 | 
				
			||||||
 | 
					      year: '2-digit'
 | 
				
			||||||
 | 
					    })}</p>)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    accessorFn: row => {
 | 
				
			||||||
 | 
					      if (row.response) {
 | 
				
			||||||
 | 
					        return row.response.response
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return "RECORD DELETED"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    id: "response",
 | 
				
			||||||
 | 
					    header: ({ column }) => {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="ghost"
 | 
				
			||||||
 | 
					          className="p-0 flex justify-center w-full"
 | 
				
			||||||
 | 
					          onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <span className="hidden md:block"> Response </span>
 | 
				
			||||||
 | 
					          <MessageCircleReply className="block md:hidden" />
 | 
				
			||||||
 | 
					          <ArrowUpDown className="ml-2 h-4 w-4 hidden md:block" />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    cell: (props: CellContext<any, any>) => (<p className="w-full text-center text-xs md:text-sm">{props.getValue()}</p>)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,41 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { createSub } from "app/lib/create"
 | 
				
			||||||
 | 
					import { Dialog, DialogHeader, DialogTrigger, DialogContent, DialogClose, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { ComponentProps } from "react";
 | 
				
			||||||
 | 
					import { Pub, Response, Story } from "@prisma/client";
 | 
				
			||||||
 | 
					import SubmissionForm from "app/ui/forms/sub";
 | 
				
			||||||
 | 
					import { Plus } from "lucide-react";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function CreateSubmissionDialog({ stories, pubs, responses }: ComponentProps<"div"> & { stories: Story[], pubs: Pub[], responses: Response[] }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [isOpen, setIsOpen] = useState(false)
 | 
				
			||||||
 | 
					  function closeDialog() {
 | 
				
			||||||
 | 
					    setIsOpen(false)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog open={isOpen} onOpenChange={setIsOpen}>
 | 
				
			||||||
 | 
					      <DialogTrigger asChild>
 | 
				
			||||||
 | 
					        <Button>
 | 
				
			||||||
 | 
					          <span className="hidden md:block">Create new submission</span>
 | 
				
			||||||
 | 
					          <Plus className="block md:hidden" />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </DialogTrigger>
 | 
				
			||||||
 | 
					      <DialogContent className="text-xs md:text-sm">
 | 
				
			||||||
 | 
					        <DialogHeader>
 | 
				
			||||||
 | 
					          <DialogTitle>New submission</DialogTitle>
 | 
				
			||||||
 | 
					          <DialogDescription>Create an entry for a new story i.e. a thing you intend to submit for publication.</DialogDescription>
 | 
				
			||||||
 | 
					        </DialogHeader>
 | 
				
			||||||
 | 
					        <SubmissionForm pubs={pubs} responses={responses} stories={stories} closeDialog={closeDialog} />
 | 
				
			||||||
 | 
					        <DialogFooter>
 | 
				
			||||||
 | 
					          <Button form="subform">Submit</Button>
 | 
				
			||||||
 | 
					        </DialogFooter>
 | 
				
			||||||
 | 
					      </DialogContent>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					"use server"
 | 
				
			||||||
 | 
					import { getPubs, getResponses, getStories } from "app/lib/get";
 | 
				
			||||||
 | 
					import SubmissionForm from "app/ui/forms/sub";
 | 
				
			||||||
 | 
					import prisma from "app/lib/db";
 | 
				
			||||||
 | 
					import { CreateContainer, CreateContainerContent, CreateContainerHeader } from "app/ui/createContainer";
 | 
				
			||||||
 | 
					import { revalidatePath } from "next/cache";
 | 
				
			||||||
 | 
					import { redirect } from "next/navigation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function Page() {
 | 
				
			||||||
 | 
						const stories = await getStories()
 | 
				
			||||||
 | 
						const pubs = await getPubs()
 | 
				
			||||||
 | 
						const responses = await getResponses()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<CreateContainer>
 | 
				
			||||||
 | 
								<CreateContainerHeader>New submission</CreateContainerHeader>
 | 
				
			||||||
 | 
								<CreateContainerContent>
 | 
				
			||||||
 | 
									<SubmissionForm stories={stories} pubs={pubs} responses={responses} />
 | 
				
			||||||
 | 
								</CreateContainerContent>
 | 
				
			||||||
 | 
							</CreateContainer>
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { ComponentProps } from "react";
 | 
				
			||||||
 | 
					import { Pub, Response, Story } from "@prisma/client";
 | 
				
			||||||
 | 
					import { SubForm } from "app/ui/forms/sub";
 | 
				
			||||||
 | 
					import EditSubmissionForm from "app/ui/forms/editSub";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function EditSubmissionDialog({ stories, pubs, responses, defaults, children, closeDialog }: ComponentProps<"div"> & { stories: Story[], pubs: Pub[], responses: Response[], defaults: SubForm, closeDialog: () => void }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <DialogHeader>
 | 
				
			||||||
 | 
					        <DialogTitle>Edit Submission</DialogTitle>
 | 
				
			||||||
 | 
					        <DialogDescription>Change response status, edit dates etc</DialogDescription>
 | 
				
			||||||
 | 
					      </DialogHeader>
 | 
				
			||||||
 | 
					      <EditSubmissionForm pubs={pubs} responses={responses} stories={stories} defaults={defaults} closeDialog={closeDialog} />
 | 
				
			||||||
 | 
					      <DialogFooter>
 | 
				
			||||||
 | 
					        <Button form="subform">Submit</Button>
 | 
				
			||||||
 | 
					      </DialogFooter>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					import { LoadingSpinner } from "app/loading";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Loading() {
 | 
				
			||||||
 | 
					  return <LoadingSpinner />
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					import { getGenres, getPubs, getResponses, getStories, getSubsComplete } from "app/lib/get"
 | 
				
			||||||
 | 
					import { DataTable } from "app/ui/tables/data-table"
 | 
				
			||||||
 | 
					import { columns } from "./columns"
 | 
				
			||||||
 | 
					import { Pub, Response, Story, Sub } from "@prisma/client"
 | 
				
			||||||
 | 
					import CreateSubmissionDialog from "./create"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SubComplete = Sub & {
 | 
				
			||||||
 | 
					  pub: Pub,
 | 
				
			||||||
 | 
					  story: Story,
 | 
				
			||||||
 | 
					  response: Response
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default async function Page() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const subs: Array<SubComplete> = await getSubsComplete()
 | 
				
			||||||
 | 
					  const stories = await getStories()
 | 
				
			||||||
 | 
					  const pubs = await getPubs()
 | 
				
			||||||
 | 
					  const responses = await getResponses()
 | 
				
			||||||
 | 
					  const genres = await getGenres()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="container px-1 md:px-4 mx-auto">
 | 
				
			||||||
 | 
					      <DataTable data={subs} columns={columns} tableName="sub"
 | 
				
			||||||
 | 
					        stories={stories}
 | 
				
			||||||
 | 
					        pubs={pubs}
 | 
				
			||||||
 | 
					        responses={responses}
 | 
				
			||||||
 | 
					        genres={genres}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <CreateSubmissionDialog
 | 
				
			||||||
 | 
					          stories={stories}
 | 
				
			||||||
 | 
					          pubs={pubs}
 | 
				
			||||||
 | 
					          responses={responses}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </DataTable>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export type Pathname = "/story" | "/publication" | "/submission"
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					import { ComponentProps } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function CreateContainer({ children }: ComponentProps<"div">) {
 | 
				
			||||||
 | 
					  return <div className="w-2/5 m-auto bg-card rounded-t-3xl border-border">{children}</div>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function CreateContainerHeader({ children }: ComponentProps<"h1">) {
 | 
				
			||||||
 | 
					  return <h1 className="text-primary-foreground bg-primary w-full font-black text-3xl p-5 rounded-t-3xl">{children}</h1>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function CreateContainerDescription({ children }: ComponentProps<"p">) {
 | 
				
			||||||
 | 
					  return <p className="">{children}</p>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function CreateContainerContent({ children, className }: ComponentProps<"div">) {
 | 
				
			||||||
 | 
					  return <div className="p-6">
 | 
				
			||||||
 | 
					    {children}
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					import { Inter } from 'next/font/google';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const inter = Inter({ subsets: ['latin'] });
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,265 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { z } from "zod"
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod"
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form"
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button"
 | 
				
			||||||
 | 
					import { toast } from "@/components/ui/use-toast"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						Popover,
 | 
				
			||||||
 | 
						PopoverContent,
 | 
				
			||||||
 | 
						PopoverTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/popover"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Calendar } from "@/components/ui/calendar"
 | 
				
			||||||
 | 
					import { CalendarIcon } from "@radix-ui/react-icons"
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					import { format } from "date-fns"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						Form,
 | 
				
			||||||
 | 
						FormItem,
 | 
				
			||||||
 | 
						FormLabel,
 | 
				
			||||||
 | 
						FormField,
 | 
				
			||||||
 | 
						FormControl,
 | 
				
			||||||
 | 
						FormDescription,
 | 
				
			||||||
 | 
						FormMessage
 | 
				
			||||||
 | 
					} from "@/components/ui/form"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						Select,
 | 
				
			||||||
 | 
						SelectContent,
 | 
				
			||||||
 | 
						SelectItem,
 | 
				
			||||||
 | 
						SelectTrigger,
 | 
				
			||||||
 | 
						SelectValue,
 | 
				
			||||||
 | 
					} from "@/components/ui/select"
 | 
				
			||||||
 | 
					import { useState } from "react"
 | 
				
			||||||
 | 
					import { createSub } from "app/lib/create"
 | 
				
			||||||
 | 
					import { subSchema } from "./schemas"
 | 
				
			||||||
 | 
					import { updateSub } from "app/lib/update"
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SubForm = z.infer<typeof subSchema>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function EditSubmissionForm({ stories, pubs, responses, defaults, closeDialog }) {
 | 
				
			||||||
 | 
						const form = useForm<z.infer<typeof subSchema>>({
 | 
				
			||||||
 | 
							resolver: zodResolver(subSchema),
 | 
				
			||||||
 | 
							defaultValues: {
 | 
				
			||||||
 | 
								responseId: responses[0].id,
 | 
				
			||||||
 | 
								...defaults
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						const [isSubCalendarOpen, setIsSubCalendarOpen] = useState(false);
 | 
				
			||||||
 | 
						const [isRespCalendarOpen, setIsRespCalendarOpen] = useState(false);
 | 
				
			||||||
 | 
						const storiesSelectItems = stories.map(e => (
 | 
				
			||||||
 | 
							<SelectItem value={e.id.toString()} key={e.title}>
 | 
				
			||||||
 | 
								{e.title}
 | 
				
			||||||
 | 
							</SelectItem>
 | 
				
			||||||
 | 
						))
 | 
				
			||||||
 | 
						const pubsSelectItems = pubs.map(e => (
 | 
				
			||||||
 | 
							<SelectItem value={e.id} key={e.title}>
 | 
				
			||||||
 | 
								{e.title}
 | 
				
			||||||
 | 
							</SelectItem>
 | 
				
			||||||
 | 
						))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const reponsesSelectItems = responses.map(e => (
 | 
				
			||||||
 | 
							<SelectItem value={e.id} key={e.title}>
 | 
				
			||||||
 | 
								{e.response}
 | 
				
			||||||
 | 
							</SelectItem>
 | 
				
			||||||
 | 
						))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function onSubmit(values: z.infer<typeof subSchema>) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const res = await updateSub(values)
 | 
				
			||||||
 | 
								if (res === undefined) throw new Error("something went wrong")
 | 
				
			||||||
 | 
								toast({ title: "Successfully created new submission!" })
 | 
				
			||||||
 | 
								router.refresh()
 | 
				
			||||||
 | 
								closeDialog()
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								toast({
 | 
				
			||||||
 | 
									title: "UH-OH",
 | 
				
			||||||
 | 
									description: error.message
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						function onErrors(errors) {
 | 
				
			||||||
 | 
							toast({
 | 
				
			||||||
 | 
								title: "You have errors",
 | 
				
			||||||
 | 
								description: (
 | 
				
			||||||
 | 
									<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
 | 
				
			||||||
 | 
										<code className="text-white">{JSON.stringify(errors, null, 2)}</code>
 | 
				
			||||||
 | 
									</pre>
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							console.log(JSON.stringify(errors))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<Form {...form}>
 | 
				
			||||||
 | 
								<form id="subform" onSubmit={form.handleSubmit(onSubmit, onErrors)} className="space-y-2 md:space-y-8">
 | 
				
			||||||
 | 
									<FormField
 | 
				
			||||||
 | 
										control={form.control}
 | 
				
			||||||
 | 
										name="storyId"
 | 
				
			||||||
 | 
										render={({ field }) => (
 | 
				
			||||||
 | 
											<FormItem>
 | 
				
			||||||
 | 
												<FormLabel className="text-sm md:text-base">Story</FormLabel>
 | 
				
			||||||
 | 
												<Select onValueChange={field.onChange} defaultValue={field.value.toString()}>
 | 
				
			||||||
 | 
													<FormControl>
 | 
				
			||||||
 | 
														<SelectTrigger>
 | 
				
			||||||
 | 
															<SelectValue placeholder="Select something">
 | 
				
			||||||
 | 
																<p>{stories?.find(e => e.id === Number(field.value))?.title ?? null}</p>
 | 
				
			||||||
 | 
															</SelectValue>
 | 
				
			||||||
 | 
														</SelectTrigger>
 | 
				
			||||||
 | 
													</FormControl>
 | 
				
			||||||
 | 
													<SelectContent>
 | 
				
			||||||
 | 
														{storiesSelectItems}
 | 
				
			||||||
 | 
													</SelectContent>
 | 
				
			||||||
 | 
												</Select>
 | 
				
			||||||
 | 
												<FormDescription className="text-xs md:text-base">The piece you submitted</FormDescription>
 | 
				
			||||||
 | 
												<FormMessage />
 | 
				
			||||||
 | 
											</FormItem>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
									<FormField
 | 
				
			||||||
 | 
										control={form.control}
 | 
				
			||||||
 | 
										name="pubId"
 | 
				
			||||||
 | 
										render={({ field }) => (
 | 
				
			||||||
 | 
											<FormItem>
 | 
				
			||||||
 | 
												<FormLabel className="text-sm md:text-base">Publication</FormLabel>
 | 
				
			||||||
 | 
												<Select onValueChange={field.onChange} defaultValue={field.value.toString()}>
 | 
				
			||||||
 | 
													<FormControl>
 | 
				
			||||||
 | 
														<SelectTrigger>
 | 
				
			||||||
 | 
															<SelectValue placeholder="Select something">
 | 
				
			||||||
 | 
																<p>{pubs?.find(e => e.id === Number(field.value))?.title ?? null}</p>
 | 
				
			||||||
 | 
															</SelectValue>
 | 
				
			||||||
 | 
														</SelectTrigger>
 | 
				
			||||||
 | 
													</FormControl>
 | 
				
			||||||
 | 
													<SelectContent>
 | 
				
			||||||
 | 
														{pubsSelectItems}
 | 
				
			||||||
 | 
													</SelectContent>
 | 
				
			||||||
 | 
												</Select>
 | 
				
			||||||
 | 
												<FormDescription className="text-xs md:text-base">The market you sent it to</FormDescription>
 | 
				
			||||||
 | 
												<FormMessage />
 | 
				
			||||||
 | 
											</FormItem>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<FormField
 | 
				
			||||||
 | 
										control={form.control}
 | 
				
			||||||
 | 
										name="submitted"
 | 
				
			||||||
 | 
										render={({ field }) => (
 | 
				
			||||||
 | 
											<FormItem className="flex flex-col">
 | 
				
			||||||
 | 
												<FormLabel className="text-sm md:text-base">Date of submission</FormLabel>
 | 
				
			||||||
 | 
												<Popover modal={true} open={isSubCalendarOpen} onOpenChange={setIsSubCalendarOpen}>
 | 
				
			||||||
 | 
													<PopoverTrigger asChild>
 | 
				
			||||||
 | 
														<FormControl>
 | 
				
			||||||
 | 
															<Button
 | 
				
			||||||
 | 
																variant={"outline"}
 | 
				
			||||||
 | 
																className={cn(
 | 
				
			||||||
 | 
																	"w-[240px] pl-3 text-left font-normal",
 | 
				
			||||||
 | 
																	!field.value && "text-muted-foreground"
 | 
				
			||||||
 | 
																)}
 | 
				
			||||||
 | 
															>
 | 
				
			||||||
 | 
																{field.value ? (
 | 
				
			||||||
 | 
																	format(field.value, "PPP")
 | 
				
			||||||
 | 
																) : (
 | 
				
			||||||
 | 
																	<span>Pick a date</span>
 | 
				
			||||||
 | 
																)}
 | 
				
			||||||
 | 
																<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
 | 
				
			||||||
 | 
															</Button>
 | 
				
			||||||
 | 
														</FormControl>
 | 
				
			||||||
 | 
													</PopoverTrigger>
 | 
				
			||||||
 | 
													<PopoverContent className="w-auto p-0" align="start">
 | 
				
			||||||
 | 
														{/* @ts-ignore */}
 | 
				
			||||||
 | 
														<Calendar mode="single" selected={field.value}
 | 
				
			||||||
 | 
															onSelect={(e) => { field.onChange(e); setIsSubCalendarOpen(false); }}
 | 
				
			||||||
 | 
															disabled={(date) =>
 | 
				
			||||||
 | 
																date > new Date() || date < new Date("1900-01-01")
 | 
				
			||||||
 | 
															}
 | 
				
			||||||
 | 
															initialFocus
 | 
				
			||||||
 | 
														/>
 | 
				
			||||||
 | 
													</PopoverContent>
 | 
				
			||||||
 | 
												</Popover>
 | 
				
			||||||
 | 
												<FormDescription className="text-xs md:text-base">
 | 
				
			||||||
 | 
													The date you sent it
 | 
				
			||||||
 | 
												</FormDescription>
 | 
				
			||||||
 | 
												<FormMessage />
 | 
				
			||||||
 | 
											</FormItem>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<FormField
 | 
				
			||||||
 | 
										control={form.control}
 | 
				
			||||||
 | 
										name="responded"
 | 
				
			||||||
 | 
										render={({ field }) => (
 | 
				
			||||||
 | 
											<FormItem className="flex flex-col">
 | 
				
			||||||
 | 
												<FormLabel className="text-sm md:text-base">Date of response</FormLabel>
 | 
				
			||||||
 | 
												<Popover modal={true} open={isRespCalendarOpen} onOpenChange={setIsRespCalendarOpen}>
 | 
				
			||||||
 | 
													<PopoverTrigger asChild>
 | 
				
			||||||
 | 
														<FormControl>
 | 
				
			||||||
 | 
															<Button
 | 
				
			||||||
 | 
																variant={"outline"}
 | 
				
			||||||
 | 
																className={cn(
 | 
				
			||||||
 | 
																	"w-[240px] pl-3 text-left font-normal",
 | 
				
			||||||
 | 
																	!field.value && "text-muted-foreground"
 | 
				
			||||||
 | 
																)}
 | 
				
			||||||
 | 
															>
 | 
				
			||||||
 | 
																{field.value ? (
 | 
				
			||||||
 | 
																	format(field.value, "PPP")
 | 
				
			||||||
 | 
																) : (
 | 
				
			||||||
 | 
																	<span>Pick a date</span>
 | 
				
			||||||
 | 
																)}
 | 
				
			||||||
 | 
																<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
 | 
				
			||||||
 | 
															</Button>
 | 
				
			||||||
 | 
														</FormControl>
 | 
				
			||||||
 | 
													</PopoverTrigger>
 | 
				
			||||||
 | 
													<PopoverContent className="w-auto p-0" align="start">
 | 
				
			||||||
 | 
														{/* @ts-ignore */}
 | 
				
			||||||
 | 
														<Calendar selected={field.value} onSelect={(e) => { field.onChange(e); setIsRespCalendarOpen(false); }}
 | 
				
			||||||
 | 
															disabled={(date) =>
 | 
				
			||||||
 | 
																date > new Date() || date < new Date("1900-01-01")
 | 
				
			||||||
 | 
															}
 | 
				
			||||||
 | 
															initialFocus
 | 
				
			||||||
 | 
														/>
 | 
				
			||||||
 | 
													</PopoverContent>
 | 
				
			||||||
 | 
												</Popover>
 | 
				
			||||||
 | 
												<FormDescription className="text-xs md:text-base">
 | 
				
			||||||
 | 
													The date they wrote back
 | 
				
			||||||
 | 
												</FormDescription>
 | 
				
			||||||
 | 
												<FormMessage />
 | 
				
			||||||
 | 
											</FormItem>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<FormField
 | 
				
			||||||
 | 
										control={form.control}
 | 
				
			||||||
 | 
										name="responseId"
 | 
				
			||||||
 | 
										render={({ field }) => (
 | 
				
			||||||
 | 
											<FormItem>
 | 
				
			||||||
 | 
												<FormLabel className="text-sm md:text-base">Response</FormLabel>
 | 
				
			||||||
 | 
												<Select onValueChange={field.onChange} defaultValue={field.value.toString()}>
 | 
				
			||||||
 | 
													<FormControl>
 | 
				
			||||||
 | 
														<SelectTrigger>
 | 
				
			||||||
 | 
															<SelectValue>
 | 
				
			||||||
 | 
																<p>{responses?.find(e => e.id === Number(field.value))?.response ?? null}</p>
 | 
				
			||||||
 | 
															</SelectValue>
 | 
				
			||||||
 | 
														</SelectTrigger>
 | 
				
			||||||
 | 
													</FormControl>
 | 
				
			||||||
 | 
													<SelectContent>
 | 
				
			||||||
 | 
														{reponsesSelectItems}
 | 
				
			||||||
 | 
													</SelectContent>
 | 
				
			||||||
 | 
												</Select>
 | 
				
			||||||
 | 
												<FormDescription className="text-xs md:text-base">The market you sent it to</FormDescription>
 | 
				
			||||||
 | 
												<FormMessage />
 | 
				
			||||||
 | 
											</FormItem>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								</form>
 | 
				
			||||||
 | 
							</Form>
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,85 @@
 | 
				
			||||||
 | 
					import { FormField, FormItem, FormLabel, FormMessage, FormControl } from "@/components/ui/form"
 | 
				
			||||||
 | 
					import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button"
 | 
				
			||||||
 | 
					import { Badge } from "@/components/ui/badge"
 | 
				
			||||||
 | 
					import { Checkbox } from "@/components/ui/checkbox"
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					import { ComponentProps } from "react"
 | 
				
			||||||
 | 
					import { Genre } from "@prisma/client"
 | 
				
			||||||
 | 
					import { UseFormReturn } from "react-hook-form"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function GenrePicker({ genres, form }: ComponentProps<"div"> & { genres: Genre[], form: UseFormReturn }) {
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<Popover modal={true}>
 | 
				
			||||||
 | 
								<FormField
 | 
				
			||||||
 | 
									control={form.control}
 | 
				
			||||||
 | 
									name="genres"
 | 
				
			||||||
 | 
									render={({ field }) => (
 | 
				
			||||||
 | 
										<FormItem className="flex flex-col">
 | 
				
			||||||
 | 
											<FormLabel className="h-5">Genres</FormLabel>
 | 
				
			||||||
 | 
											<PopoverTrigger asChild>
 | 
				
			||||||
 | 
												<Button
 | 
				
			||||||
 | 
													variant={"outline"}
 | 
				
			||||||
 | 
													className={cn(
 | 
				
			||||||
 | 
														"min-w-fit max-w-60 pl-3 text-left font-normal flex-wrap gap-y-1 h-fit min-h-10",
 | 
				
			||||||
 | 
														!field.value && "text-muted-foreground"
 | 
				
			||||||
 | 
													)}
 | 
				
			||||||
 | 
												>
 | 
				
			||||||
 | 
													{field.value.length !== 0 ? (
 | 
				
			||||||
 | 
														field.value.map((e, i) => (<Badge key={i}>{genres.find(f => e === f.id).name}</Badge>))
 | 
				
			||||||
 | 
													) : (
 | 
				
			||||||
 | 
														<p>Select</p>
 | 
				
			||||||
 | 
													)}
 | 
				
			||||||
 | 
												</Button>
 | 
				
			||||||
 | 
											</PopoverTrigger>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											<PopoverContent align="start">
 | 
				
			||||||
 | 
												{genres.map((item) => (
 | 
				
			||||||
 | 
													< FormField
 | 
				
			||||||
 | 
														key={item.id}
 | 
				
			||||||
 | 
														control={form.control}
 | 
				
			||||||
 | 
														name="genres"
 | 
				
			||||||
 | 
														render={({ field }) => {
 | 
				
			||||||
 | 
															return (
 | 
				
			||||||
 | 
																<FormItem
 | 
				
			||||||
 | 
																	key={item.id}
 | 
				
			||||||
 | 
																	className="flex flex-row items-start space-x-3 space-y-0"
 | 
				
			||||||
 | 
																>
 | 
				
			||||||
 | 
																	<FormControl>
 | 
				
			||||||
 | 
																		<Checkbox
 | 
				
			||||||
 | 
																			checked={field.value?.includes(item.id)}
 | 
				
			||||||
 | 
																			onCheckedChange={(checked) => {
 | 
				
			||||||
 | 
																				return checked
 | 
				
			||||||
 | 
																					? field.onChange([...field.value, item.id])
 | 
				
			||||||
 | 
																					: field.onChange(
 | 
				
			||||||
 | 
																						field.value?.filter(
 | 
				
			||||||
 | 
																							(value) => value !== item.id
 | 
				
			||||||
 | 
																						)
 | 
				
			||||||
 | 
																					)
 | 
				
			||||||
 | 
																			}}
 | 
				
			||||||
 | 
																		/>
 | 
				
			||||||
 | 
																	</FormControl>
 | 
				
			||||||
 | 
																	<FormLabel className="text-sm font-normal">
 | 
				
			||||||
 | 
																		{item.name}
 | 
				
			||||||
 | 
																	</FormLabel>
 | 
				
			||||||
 | 
																</FormItem>
 | 
				
			||||||
 | 
															)
 | 
				
			||||||
 | 
														}}
 | 
				
			||||||
 | 
													/>
 | 
				
			||||||
 | 
												))}
 | 
				
			||||||
 | 
												<Button variant="link" className="p-0" onClick={() => form.setValue("genres", [])}>Clear</Button>
 | 
				
			||||||
 | 
											</PopoverContent>
 | 
				
			||||||
 | 
											<FormMessage />
 | 
				
			||||||
 | 
										</FormItem>
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							</Popover>
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,132 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { z } from "zod"
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod"
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						Form,
 | 
				
			||||||
 | 
						FormControl,
 | 
				
			||||||
 | 
						FormDescription,
 | 
				
			||||||
 | 
						FormField,
 | 
				
			||||||
 | 
						FormItem,
 | 
				
			||||||
 | 
						FormLabel,
 | 
				
			||||||
 | 
						FormMessage,
 | 
				
			||||||
 | 
					} from "@/components/ui/form"
 | 
				
			||||||
 | 
					import { Input } from "@/components/ui/input"
 | 
				
			||||||
 | 
					import { toast } from "@/components/ui/use-toast"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { randomPublicationTitle } from "app/lib/shortStoryTitleGenerator"
 | 
				
			||||||
 | 
					import { ComponentProps } from "react"
 | 
				
			||||||
 | 
					import { Genre, Pub } from "@prisma/client"
 | 
				
			||||||
 | 
					import GenrePicker from "./genrePicker"
 | 
				
			||||||
 | 
					import { pubSchema } from "./schemas"
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation"
 | 
				
			||||||
 | 
					import { Ban } from "lucide-react"
 | 
				
			||||||
 | 
					import { PubWithGenres } from "app/publication/page"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function PubForm({ genres, dbAction, className, closeDialog, defaults }: ComponentProps<"div"> & { genres: Array<Genre>, dbAction: (data: any) => Promise<{ success: string }>, closeDialog: () => void, defaults?: PubWithGenres }) {
 | 
				
			||||||
 | 
						const form = useForm<z.infer<typeof pubSchema>>({
 | 
				
			||||||
 | 
							resolver: zodResolver(pubSchema),
 | 
				
			||||||
 | 
							defaultValues: {
 | 
				
			||||||
 | 
								id: defaults?.id,
 | 
				
			||||||
 | 
								title: defaults?.title ?? "",
 | 
				
			||||||
 | 
								link: defaults?.link ?? "",
 | 
				
			||||||
 | 
								query_after_days: defaults?.query_after_days ?? 30,
 | 
				
			||||||
 | 
								genres: defaults?.genres.map(e => e.id) ?? []
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function onSubmit(values: z.infer<typeof pubSchema>) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const res = await dbAction({
 | 
				
			||||||
 | 
									pub: {
 | 
				
			||||||
 | 
										id: values?.id,
 | 
				
			||||||
 | 
										title: values.title,
 | 
				
			||||||
 | 
										link: values.link,
 | 
				
			||||||
 | 
										query_after_days: values.query_after_days
 | 
				
			||||||
 | 
									}, genres: values.genres
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								if (!res?.success) throw new Error("something went wrong")
 | 
				
			||||||
 | 
								toast({ title: "Success!", description: res.success })
 | 
				
			||||||
 | 
								router.refresh()
 | 
				
			||||||
 | 
								closeDialog()
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								toast({
 | 
				
			||||||
 | 
									title: "Oh dear... ",
 | 
				
			||||||
 | 
									description: error.message
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						function onErrors(errors) {
 | 
				
			||||||
 | 
							toast({
 | 
				
			||||||
 | 
								description: (
 | 
				
			||||||
 | 
									<Ban />
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							console.log(JSON.stringify(errors))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const exampleTitle = randomPublicationTitle()
 | 
				
			||||||
 | 
						const exampleUrl = "www." +
 | 
				
			||||||
 | 
							exampleTitle.replace(/ /g, '')
 | 
				
			||||||
 | 
								.toLowerCase() +
 | 
				
			||||||
 | 
							".com"
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<div className={className}>
 | 
				
			||||||
 | 
								<Form {...form}>
 | 
				
			||||||
 | 
									<form id="pubform" onSubmit={form.handleSubmit(onSubmit, onErrors)} className="space-y-8">
 | 
				
			||||||
 | 
										<FormField
 | 
				
			||||||
 | 
											control={form.control}
 | 
				
			||||||
 | 
											name="title"
 | 
				
			||||||
 | 
											render={({ field }) => (
 | 
				
			||||||
 | 
												<FormItem>
 | 
				
			||||||
 | 
													<FormLabel>Title</FormLabel>
 | 
				
			||||||
 | 
													<FormControl>
 | 
				
			||||||
 | 
														<Input placeholder={exampleTitle} {...field} />
 | 
				
			||||||
 | 
													</FormControl>
 | 
				
			||||||
 | 
													<FormMessage />
 | 
				
			||||||
 | 
												</FormItem>
 | 
				
			||||||
 | 
											)}
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										<FormField
 | 
				
			||||||
 | 
											control={form.control}
 | 
				
			||||||
 | 
											name="link"
 | 
				
			||||||
 | 
											render={({ field }) => (
 | 
				
			||||||
 | 
												<FormItem>
 | 
				
			||||||
 | 
													<FormLabel>Website</FormLabel>
 | 
				
			||||||
 | 
													<FormControl>
 | 
				
			||||||
 | 
														<Input placeholder={exampleUrl} {...field} />
 | 
				
			||||||
 | 
													</FormControl>
 | 
				
			||||||
 | 
													<FormMessage />
 | 
				
			||||||
 | 
												</FormItem>
 | 
				
			||||||
 | 
											)}
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										<div className="inline-flex flex-wrap w-full gap-x-16 gap-y-8 max-w-full h-fit">
 | 
				
			||||||
 | 
											<GenrePicker genres={genres} form={form} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											<FormField
 | 
				
			||||||
 | 
												control={form.control}
 | 
				
			||||||
 | 
												name="query_after_days"
 | 
				
			||||||
 | 
												render={({ field }) => (
 | 
				
			||||||
 | 
													<FormItem className="flex flex-col">
 | 
				
			||||||
 | 
														<FormLabel className="h-5">Query after (days)</FormLabel>
 | 
				
			||||||
 | 
														<FormControl>
 | 
				
			||||||
 | 
															<Input className=" w-24" type="number" step={5} min={30} {...field}></Input>
 | 
				
			||||||
 | 
														</FormControl>
 | 
				
			||||||
 | 
														<FormMessage />
 | 
				
			||||||
 | 
													</FormItem>
 | 
				
			||||||
 | 
												)}
 | 
				
			||||||
 | 
											/>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									</form>
 | 
				
			||||||
 | 
								</Form>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					import { getPubs } from "app/lib/get"
 | 
				
			||||||
 | 
					export default async function PubsDropdown() {
 | 
				
			||||||
 | 
						const pubs = await getPubs()
 | 
				
			||||||
 | 
						const pubsDropdown = pubs.map(e => {
 | 
				
			||||||
 | 
							return <option value={e.id} key={e.title}>{e.title}</option>
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return (<>
 | 
				
			||||||
 | 
							<label htmlFor="pubdDropdown">Publication:</label>
 | 
				
			||||||
 | 
							<select key="pubsDropdown" id="pubsDropdown">
 | 
				
			||||||
 | 
								{pubsDropdown}
 | 
				
			||||||
 | 
							</select>
 | 
				
			||||||
 | 
						</>
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					import prisma from "app/lib/db"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function ResponseDropdown() {
 | 
				
			||||||
 | 
						async function getResponses() {
 | 
				
			||||||
 | 
							"use server"
 | 
				
			||||||
 | 
							return prisma.response.findMany()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const responses = await getResponses()
 | 
				
			||||||
 | 
						const responsesDropdown = responses.map(e => {
 | 
				
			||||||
 | 
							return <option value={e.id} key={e.response}>{e.response}</option>
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<>
 | 
				
			||||||
 | 
								<label htmlFor="responsesDropdown">
 | 
				
			||||||
 | 
									Status:
 | 
				
			||||||
 | 
								</label>
 | 
				
			||||||
 | 
								<select key="responsesDropdown" id="responsesDropdown">
 | 
				
			||||||
 | 
									{responsesDropdown}
 | 
				
			||||||
 | 
								</select>
 | 
				
			||||||
 | 
							</>
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,54 @@
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const storySchema = z.object({
 | 
				
			||||||
 | 
						title: z.string().min(2).max(50),
 | 
				
			||||||
 | 
						word_count: z.coerce.number(),
 | 
				
			||||||
 | 
						genres: z.object({ id: z.number(), name: z.string() }).array()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const pubSchema = z.object({
 | 
				
			||||||
 | 
						id: z.coerce.number().optional(),
 | 
				
			||||||
 | 
						title: z.string().min(2).max(50),
 | 
				
			||||||
 | 
						link: z.string(),
 | 
				
			||||||
 | 
						query_after_days: z.coerce.number().min(30),
 | 
				
			||||||
 | 
						genres: z.array(z.number()),
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const subSchema = z.object({
 | 
				
			||||||
 | 
						id: z.number().optional(),
 | 
				
			||||||
 | 
						storyId: z.coerce.number(),
 | 
				
			||||||
 | 
						pubId: z.coerce.number(),
 | 
				
			||||||
 | 
						submitted: z.coerce.date().transform((date) => date.toString()),
 | 
				
			||||||
 | 
						responded: z.coerce.date().transform((date) => {
 | 
				
			||||||
 | 
							if (date.toString() !== new Date(null).toString()) {
 | 
				
			||||||
 | 
								return date.toString()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return null
 | 
				
			||||||
 | 
						}).optional(),
 | 
				
			||||||
 | 
						responseId: z.coerce.number()
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
						.refine(object => {
 | 
				
			||||||
 | 
							const submitted = new Date(object.submitted)
 | 
				
			||||||
 | 
							const responded = object.responded ? new Date(object.responded) : null
 | 
				
			||||||
 | 
							return responded >= submitted || responded === null
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: ["responded"],
 | 
				
			||||||
 | 
								message: "'Responded' must be a later date than 'submitted'"
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						.refine(object => {
 | 
				
			||||||
 | 
							if (object.responded) {
 | 
				
			||||||
 | 
								//there is a 'responded' date and the response is not 'pending'
 | 
				
			||||||
 | 
								return object.responseId !== 7
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (!object.responded) {
 | 
				
			||||||
 | 
								//there is not a 'responded' date and the response is pending
 | 
				
			||||||
 | 
								return object.responseId === 7
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								path: ["responseId"],
 | 
				
			||||||
 | 
								message: "A pending response cannot have a date, and a non-pending response must have a date"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,125 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { z } from "zod"
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod"
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						Form,
 | 
				
			||||||
 | 
						FormControl,
 | 
				
			||||||
 | 
						FormDescription,
 | 
				
			||||||
 | 
						FormField,
 | 
				
			||||||
 | 
						FormItem,
 | 
				
			||||||
 | 
						FormLabel,
 | 
				
			||||||
 | 
						FormMessage,
 | 
				
			||||||
 | 
					} from "@/components/ui/form"
 | 
				
			||||||
 | 
					import { Input } from "@/components/ui/input"
 | 
				
			||||||
 | 
					import { toast } from "@/components/ui/use-toast"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ComponentProps, SetStateAction } from "react"
 | 
				
			||||||
 | 
					import { Genre, Story } from "@prisma/client"
 | 
				
			||||||
 | 
					import { randomStoryTitle } from "app/lib/shortStoryTitleGenerator"
 | 
				
			||||||
 | 
					import GenrePicker from "./genrePicker"
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation"
 | 
				
			||||||
 | 
					import { Ban, Cross } from "lucide-react"
 | 
				
			||||||
 | 
					import { StoryWithGenres } from "app/story/page"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const formSchema = z.object({
 | 
				
			||||||
 | 
						id: z.number().optional(),
 | 
				
			||||||
 | 
						title: z.string().min(2).max(50),
 | 
				
			||||||
 | 
						word_count: z.coerce.number().min(100),
 | 
				
			||||||
 | 
						genres: z.array(z.number())
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function StoryForm({ genres, dbAction, className, closeDialog, defaults }: ComponentProps<"div"> & { genres: Array<Genre>, dbAction: (data: any) => Promise<{ success: string }>, className: string, closeDialog?: () => void, defaults?: StoryWithGenres }) {
 | 
				
			||||||
 | 
						const form = useForm<z.infer<typeof formSchema>>({
 | 
				
			||||||
 | 
							resolver: zodResolver(formSchema),
 | 
				
			||||||
 | 
							defaultValues: {
 | 
				
			||||||
 | 
								id: defaults?.id,
 | 
				
			||||||
 | 
								title: defaults?.title ?? "",
 | 
				
			||||||
 | 
								word_count: defaults?.word_count ?? 500,
 | 
				
			||||||
 | 
								genres: defaults?.genres.map(e => e.id) ?? []
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function onSubmit(values: z.infer<typeof formSchema>) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const res = await dbAction({
 | 
				
			||||||
 | 
									story: {
 | 
				
			||||||
 | 
										id: values?.id,
 | 
				
			||||||
 | 
										title: values.title,
 | 
				
			||||||
 | 
										word_count: values.word_count,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									genres: values.genres
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								//server actions return undefined if middleware authentication fails
 | 
				
			||||||
 | 
								if (!res?.success) throw new Error("something went wrong")
 | 
				
			||||||
 | 
								toast({ title: "Success!", description: res.success })
 | 
				
			||||||
 | 
								router.refresh()
 | 
				
			||||||
 | 
								closeDialog()
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								toast({
 | 
				
			||||||
 | 
									title: "Oh dear... ",
 | 
				
			||||||
 | 
									description: error.message
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						function onErrors(errors) {
 | 
				
			||||||
 | 
							toast({
 | 
				
			||||||
 | 
								description: (<Ban />)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							console.log(JSON.stringify(errors))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<div className={className}>
 | 
				
			||||||
 | 
								<Form {...form}>
 | 
				
			||||||
 | 
									<form onSubmit={form.handleSubmit(onSubmit, onErrors)} className="space-y-8" id="storyform">
 | 
				
			||||||
 | 
										<FormField
 | 
				
			||||||
 | 
											control={form.control}
 | 
				
			||||||
 | 
											name="title"
 | 
				
			||||||
 | 
											render={({ field }) => (
 | 
				
			||||||
 | 
												<FormItem>
 | 
				
			||||||
 | 
													<FormLabel>Title</FormLabel>
 | 
				
			||||||
 | 
													<FormControl>
 | 
				
			||||||
 | 
														<Input placeholder={randomStoryTitle()} {...field} />
 | 
				
			||||||
 | 
													</FormControl>
 | 
				
			||||||
 | 
													<FormMessage />
 | 
				
			||||||
 | 
												</FormItem>
 | 
				
			||||||
 | 
											)}
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										<div className="inline-flex flex-wrap justify-around items-start w-full gap-x-16 gap-y-8 items-baseline max-w-full">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											<GenrePicker
 | 
				
			||||||
 | 
												genres={genres}
 | 
				
			||||||
 | 
												form={form}
 | 
				
			||||||
 | 
											/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											<FormField
 | 
				
			||||||
 | 
												control={form.control}
 | 
				
			||||||
 | 
												name="word_count"
 | 
				
			||||||
 | 
												render={({ field }) => (
 | 
				
			||||||
 | 
													<FormItem className="flex flex-col ">
 | 
				
			||||||
 | 
														<FormLabel className="h-5">Word count</FormLabel>
 | 
				
			||||||
 | 
														<FormControl>
 | 
				
			||||||
 | 
															<Input className=" w-24" type="number" step={500} {...field}></Input>
 | 
				
			||||||
 | 
														</FormControl>
 | 
				
			||||||
 | 
														<FormMessage />
 | 
				
			||||||
 | 
													</FormItem>
 | 
				
			||||||
 | 
												)}
 | 
				
			||||||
 | 
											/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									</form>
 | 
				
			||||||
 | 
								</Form>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					import { getStories } from "app/lib/get"
 | 
				
			||||||
 | 
					export default async function StoryDropdown() {
 | 
				
			||||||
 | 
						const stories = await getStories()
 | 
				
			||||||
 | 
						const storiesDrowpdown = stories.map(e => {
 | 
				
			||||||
 | 
							return <option value={e.id} key={`${e.title}`}>{e.title}</option>
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<>
 | 
				
			||||||
 | 
								<label htmlFor="storyDropdown">Story:</label>
 | 
				
			||||||
 | 
								<select key="storyDropdown" id="storyDropdown">
 | 
				
			||||||
 | 
									{storiesDrowpdown}
 | 
				
			||||||
 | 
								</select>
 | 
				
			||||||
 | 
							</>
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,264 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { z } from "zod"
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod"
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form"
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button"
 | 
				
			||||||
 | 
					import { toast } from "@/components/ui/use-toast"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						Popover,
 | 
				
			||||||
 | 
						PopoverContent,
 | 
				
			||||||
 | 
						PopoverTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/popover"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Calendar } from "@/components/ui/calendar"
 | 
				
			||||||
 | 
					import { CalendarIcon } from "@radix-ui/react-icons"
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils"
 | 
				
			||||||
 | 
					import { format } from "date-fns"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						Form,
 | 
				
			||||||
 | 
						FormItem,
 | 
				
			||||||
 | 
						FormLabel,
 | 
				
			||||||
 | 
						FormField,
 | 
				
			||||||
 | 
						FormControl,
 | 
				
			||||||
 | 
						FormDescription,
 | 
				
			||||||
 | 
						FormMessage
 | 
				
			||||||
 | 
					} from "@/components/ui/form"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						Select,
 | 
				
			||||||
 | 
						SelectContent,
 | 
				
			||||||
 | 
						SelectItem,
 | 
				
			||||||
 | 
						SelectTrigger,
 | 
				
			||||||
 | 
						SelectValue,
 | 
				
			||||||
 | 
					} from "@/components/ui/select"
 | 
				
			||||||
 | 
					import { useState } from "react"
 | 
				
			||||||
 | 
					import { createSub } from "app/lib/create"
 | 
				
			||||||
 | 
					import { subSchema } from "./schemas"
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation"
 | 
				
			||||||
 | 
					import { Ban } from "lucide-react"
 | 
				
			||||||
 | 
					import { Story } from "@prisma/client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SubForm = z.infer<typeof subSchema>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function SubmissionForm({ stories, pubs, responses, defaults, closeDialog }: { stories: any, pubs: any, responses: any, defaults?: any, closeDialog?: () => void }) {
 | 
				
			||||||
 | 
						const form = useForm<z.infer<typeof subSchema>>({
 | 
				
			||||||
 | 
							resolver: zodResolver(subSchema),
 | 
				
			||||||
 | 
							defaultValues: {
 | 
				
			||||||
 | 
								responseId: responses[0].id,
 | 
				
			||||||
 | 
								...defaults
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						const [isSubCalendarOpen, setIsSubCalendarOpen] = useState(false);
 | 
				
			||||||
 | 
						const [isRespCalendarOpen, setIsRespCalendarOpen] = useState(false);
 | 
				
			||||||
 | 
						const storiesSelectItems = stories.map((e: Story) => (
 | 
				
			||||||
 | 
							<SelectItem value={e.id?.toString()} key={e.title}>
 | 
				
			||||||
 | 
								{e.title}
 | 
				
			||||||
 | 
							</SelectItem>
 | 
				
			||||||
 | 
						))
 | 
				
			||||||
 | 
						const pubsSelectItems = pubs.map(e => (
 | 
				
			||||||
 | 
							<SelectItem value={e.id} key={e.title}>
 | 
				
			||||||
 | 
								{e.title}
 | 
				
			||||||
 | 
							</SelectItem>
 | 
				
			||||||
 | 
						))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const reponsesSelectItems = responses.map(e => (
 | 
				
			||||||
 | 
							<SelectItem value={e.id} key={e.title}>
 | 
				
			||||||
 | 
								{e.response}
 | 
				
			||||||
 | 
							</SelectItem>
 | 
				
			||||||
 | 
						))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function onSubmit(values: z.infer<typeof subSchema>) {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								//@ts-ignore
 | 
				
			||||||
 | 
								const res = await createSub(values)
 | 
				
			||||||
 | 
								if (!res) throw new Error("something went wrong")
 | 
				
			||||||
 | 
								toast({ title: "Successfully created new submission!" })
 | 
				
			||||||
 | 
								router.refresh()
 | 
				
			||||||
 | 
								closeDialog()
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
 | 
								toast({
 | 
				
			||||||
 | 
									title: "UH-OH",
 | 
				
			||||||
 | 
									description: error.message
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						function onErrors(errors) {
 | 
				
			||||||
 | 
							toast({
 | 
				
			||||||
 | 
								description: (
 | 
				
			||||||
 | 
									<Ban />
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							console.log(JSON.stringify(errors))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							<Form {...form}>
 | 
				
			||||||
 | 
								<form id="subform" onSubmit={form.handleSubmit(onSubmit, onErrors)} className="space-y-2 md:space-y-8 text-xs">
 | 
				
			||||||
 | 
									<FormField
 | 
				
			||||||
 | 
										control={form.control}
 | 
				
			||||||
 | 
										name="storyId"
 | 
				
			||||||
 | 
										render={({ field }) => (
 | 
				
			||||||
 | 
											<FormItem>
 | 
				
			||||||
 | 
												<FormLabel>Story</FormLabel>
 | 
				
			||||||
 | 
												<Select onValueChange={field.onChange} defaultValue={field.value?.toString()}>
 | 
				
			||||||
 | 
													<FormControl>
 | 
				
			||||||
 | 
														<SelectTrigger>
 | 
				
			||||||
 | 
															<SelectValue placeholder="Select something">
 | 
				
			||||||
 | 
																<p>{stories?.find(e => e.id === Number(field.value))?.title ?? null}</p>
 | 
				
			||||||
 | 
															</SelectValue>
 | 
				
			||||||
 | 
														</SelectTrigger>
 | 
				
			||||||
 | 
													</FormControl>
 | 
				
			||||||
 | 
													<SelectContent>
 | 
				
			||||||
 | 
														{storiesSelectItems}
 | 
				
			||||||
 | 
													</SelectContent>
 | 
				
			||||||
 | 
												</Select>
 | 
				
			||||||
 | 
												<FormDescription className="text-xs md:text-base">The piece you submitted</FormDescription>
 | 
				
			||||||
 | 
												<FormMessage />
 | 
				
			||||||
 | 
											</FormItem>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
									<FormField
 | 
				
			||||||
 | 
										control={form.control}
 | 
				
			||||||
 | 
										name="pubId"
 | 
				
			||||||
 | 
										render={({ field }) => (
 | 
				
			||||||
 | 
											<FormItem>
 | 
				
			||||||
 | 
												<FormLabel className="text-sm md:text-base">Publication</FormLabel>
 | 
				
			||||||
 | 
												<Select onValueChange={field.onChange} defaultValue={field.value?.toString()}>
 | 
				
			||||||
 | 
													<FormControl>
 | 
				
			||||||
 | 
														<SelectTrigger>
 | 
				
			||||||
 | 
															<SelectValue placeholder="Select something">
 | 
				
			||||||
 | 
																<p>{pubs?.find(e => e.id === Number(field.value))?.title ?? null}</p>
 | 
				
			||||||
 | 
															</SelectValue>
 | 
				
			||||||
 | 
														</SelectTrigger>
 | 
				
			||||||
 | 
													</FormControl>
 | 
				
			||||||
 | 
													<SelectContent>
 | 
				
			||||||
 | 
														{pubsSelectItems}
 | 
				
			||||||
 | 
													</SelectContent>
 | 
				
			||||||
 | 
												</Select>
 | 
				
			||||||
 | 
												<FormDescription className="text-xs md:text-base">The market you sent it to</FormDescription>
 | 
				
			||||||
 | 
												<FormMessage />
 | 
				
			||||||
 | 
											</FormItem>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<FormField
 | 
				
			||||||
 | 
										control={form.control}
 | 
				
			||||||
 | 
										name="submitted"
 | 
				
			||||||
 | 
										render={({ field }) => (
 | 
				
			||||||
 | 
											<FormItem className="flex flex-col">
 | 
				
			||||||
 | 
												<FormLabel className="text-sm md:text-base">Date of submission</FormLabel>
 | 
				
			||||||
 | 
												<Popover modal={true} open={isSubCalendarOpen} onOpenChange={setIsSubCalendarOpen}>
 | 
				
			||||||
 | 
													<PopoverTrigger asChild>
 | 
				
			||||||
 | 
														<FormControl>
 | 
				
			||||||
 | 
															<Button
 | 
				
			||||||
 | 
																variant={"outline"}
 | 
				
			||||||
 | 
																className={cn(
 | 
				
			||||||
 | 
																	"w-[240px] pl-3 text-left font-normal",
 | 
				
			||||||
 | 
																	!field.value && "text-muted-foreground"
 | 
				
			||||||
 | 
																)}
 | 
				
			||||||
 | 
															>
 | 
				
			||||||
 | 
																{field.value ? (
 | 
				
			||||||
 | 
																	format(field.value, "PPP")
 | 
				
			||||||
 | 
																) : (
 | 
				
			||||||
 | 
																	<span>Pick a date</span>
 | 
				
			||||||
 | 
																)}
 | 
				
			||||||
 | 
																<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
 | 
				
			||||||
 | 
															</Button>
 | 
				
			||||||
 | 
														</FormControl>
 | 
				
			||||||
 | 
													</PopoverTrigger>
 | 
				
			||||||
 | 
													<PopoverContent className="w-auto p-0" align="start">
 | 
				
			||||||
 | 
														{/* @ts-ignore */}
 | 
				
			||||||
 | 
														<Calendar mode="single" selected={field.value}
 | 
				
			||||||
 | 
															onSelect={(e) => { field.onChange(e); setIsSubCalendarOpen(false); }}
 | 
				
			||||||
 | 
															disabled={(date) =>
 | 
				
			||||||
 | 
																date > new Date() || date < new Date("1900-01-01")
 | 
				
			||||||
 | 
															}
 | 
				
			||||||
 | 
															initialFocus
 | 
				
			||||||
 | 
														/>
 | 
				
			||||||
 | 
													</PopoverContent>
 | 
				
			||||||
 | 
												</Popover>
 | 
				
			||||||
 | 
												<FormDescription className="text-xs md:text-base">
 | 
				
			||||||
 | 
													The date you sent it
 | 
				
			||||||
 | 
												</FormDescription>
 | 
				
			||||||
 | 
												<FormMessage />
 | 
				
			||||||
 | 
											</FormItem>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<FormField
 | 
				
			||||||
 | 
										control={form.control}
 | 
				
			||||||
 | 
										name="responded"
 | 
				
			||||||
 | 
										render={({ field }) => (
 | 
				
			||||||
 | 
											<FormItem className="flex flex-col">
 | 
				
			||||||
 | 
												<FormLabel className="text-sm md:text-base">Date of response</FormLabel>
 | 
				
			||||||
 | 
												<Popover modal={true} open={isRespCalendarOpen} onOpenChange={setIsRespCalendarOpen}>
 | 
				
			||||||
 | 
													<PopoverTrigger asChild>
 | 
				
			||||||
 | 
														<FormControl>
 | 
				
			||||||
 | 
															<Button
 | 
				
			||||||
 | 
																variant={"outline"}
 | 
				
			||||||
 | 
																className={cn(
 | 
				
			||||||
 | 
																	"w-[240px] pl-3 text-left font-normal",
 | 
				
			||||||
 | 
																	!field.value && "text-muted-foreground"
 | 
				
			||||||
 | 
																)}
 | 
				
			||||||
 | 
															>
 | 
				
			||||||
 | 
																{field.value ? (
 | 
				
			||||||
 | 
																	format(field.value, "PPP")
 | 
				
			||||||
 | 
																) : (
 | 
				
			||||||
 | 
																	<span>Pick a date</span>
 | 
				
			||||||
 | 
																)}
 | 
				
			||||||
 | 
																<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
 | 
				
			||||||
 | 
															</Button>
 | 
				
			||||||
 | 
														</FormControl>
 | 
				
			||||||
 | 
													</PopoverTrigger>
 | 
				
			||||||
 | 
													<PopoverContent className="w-auto p-0" align="start">
 | 
				
			||||||
 | 
														{/* @ts-ignore */}
 | 
				
			||||||
 | 
														<Calendar mode="single" selected={field.value}
 | 
				
			||||||
 | 
															onSelect={(e) => { field.onChange(e); setIsRespCalendarOpen(false); }}
 | 
				
			||||||
 | 
															disabled={(date) =>
 | 
				
			||||||
 | 
																date > new Date() || date < new Date("1900-01-01")
 | 
				
			||||||
 | 
															}
 | 
				
			||||||
 | 
															initialFocus
 | 
				
			||||||
 | 
														/>
 | 
				
			||||||
 | 
													</PopoverContent>
 | 
				
			||||||
 | 
												</Popover>
 | 
				
			||||||
 | 
												<FormDescription className="text-xs md:text-base">
 | 
				
			||||||
 | 
													The date they wrote back
 | 
				
			||||||
 | 
												</FormDescription>
 | 
				
			||||||
 | 
												<FormMessage />
 | 
				
			||||||
 | 
											</FormItem>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<FormField
 | 
				
			||||||
 | 
										control={form.control}
 | 
				
			||||||
 | 
										name="responseId"
 | 
				
			||||||
 | 
										render={({ field }) => (
 | 
				
			||||||
 | 
											<FormItem>
 | 
				
			||||||
 | 
												<FormLabel className="text-sm md:text-base">Response</FormLabel>
 | 
				
			||||||
 | 
												<Select onValueChange={field.onChange} defaultValue={field.value?.toString()}>
 | 
				
			||||||
 | 
													<FormControl>
 | 
				
			||||||
 | 
														<SelectTrigger>
 | 
				
			||||||
 | 
															<SelectValue>
 | 
				
			||||||
 | 
																<p>{responses?.find(e => e.id === Number(field.value))?.response ?? null}</p>
 | 
				
			||||||
 | 
															</SelectValue>
 | 
				
			||||||
 | 
														</SelectTrigger>
 | 
				
			||||||
 | 
													</FormControl>
 | 
				
			||||||
 | 
													<SelectContent>
 | 
				
			||||||
 | 
														{reponsesSelectItems}
 | 
				
			||||||
 | 
													</SelectContent>
 | 
				
			||||||
 | 
												</Select>
 | 
				
			||||||
 | 
												<FormDescription className="text-xs md:text-base">The market you sent it to</FormDescription>
 | 
				
			||||||
 | 
												<FormMessage />
 | 
				
			||||||
 | 
											</FormItem>
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								</form>
 | 
				
			||||||
 | 
							</Form>
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					import { Genre } from "@prisma/client";
 | 
				
			||||||
 | 
					import { ComponentProps } from "react";
 | 
				
			||||||
 | 
					import { Badge } from "@/components/ui/badge";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function GenreBadges(props: ComponentProps<"div"> & { genres: Array<Genre> }) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={"flex flex-wrap gap-1 justify-center " + props.className}>
 | 
				
			||||||
 | 
					      {props.genres.map((e: Genre) => (<Badge className="text-xs md:text-sm" key={e.name}>{e.name}</Badge>))}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,26 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button"
 | 
				
			||||||
 | 
					import { LogOutIcon } from "lucide-react"
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function LogoutButton() {
 | 
				
			||||||
 | 
					  const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function handleLogout() {
 | 
				
			||||||
 | 
					    const res = await fetch('/api/auth/logout', {
 | 
				
			||||||
 | 
					      method: 'POST',
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        'Content-type': 'application/json',
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    console.log(res)
 | 
				
			||||||
 | 
					    router.refresh()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Button variant="outline" className="w-fit" onClick={handleLogout} >
 | 
				
			||||||
 | 
					      <LogOutIcon />
 | 
				
			||||||
 | 
					    </Button>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,41 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import { MoonIcon, SunIcon } from "@radix-ui/react-icons"
 | 
				
			||||||
 | 
					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">
 | 
				
			||||||
 | 
					          <SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
 | 
				
			||||||
 | 
					          <MoonIcon 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")}>
 | 
				
			||||||
 | 
					          Light
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					        <DropdownMenuItem onClick={() => setTheme("dark")}>
 | 
				
			||||||
 | 
					          Dark
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					        <DropdownMenuItem onClick={() => setTheme("system")}>
 | 
				
			||||||
 | 
					          System
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					      </DropdownMenuContent>
 | 
				
			||||||
 | 
					    </DropdownMenu>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import Link from "next/link";
 | 
				
			||||||
 | 
					import { usePathname } from "next/navigation";
 | 
				
			||||||
 | 
					import { ComponentProps } from "react";
 | 
				
			||||||
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					import { twMerge } from "tailwind-merge";
 | 
				
			||||||
 | 
					import { ArrowUpNarrowWide, BookOpen, BookOpenText } from "lucide-react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function NavLink(props: ComponentProps<"div"> & { href: string }) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Link href={props.href}><h1 className={props.className}>{props.children}</h1></Link>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default function Navlinks(props: ComponentProps<"div">) {
 | 
				
			||||||
 | 
					  const pathname = usePathname()
 | 
				
			||||||
 | 
					  const links = [
 | 
				
			||||||
 | 
					    { link: "/story", label: "STORIES", icon: <BookOpenText /> },
 | 
				
			||||||
 | 
					    { link: "/publication", label: "PUBLICATIONS", icon: <BookOpen /> },
 | 
				
			||||||
 | 
					    { link: "/submission", label: "SUBMISSIONS", icon: <ArrowUpNarrowWide /> },
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={props.className}>
 | 
				
			||||||
 | 
					      <div className="text-secondary-foreground flex flex-row md:flex-col" >
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          links.map(e => (<NavLink key={e.link} href={e.link}
 | 
				
			||||||
 | 
					            className={twMerge(clsx("text-xl drop-shadow  font-black my-2 w-full mr-2 p-2 pl-6 antialiased text-secondary-foreground bg-secondary rounded-3xl md:rounded-l-3xl ",
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                "text-primary-foreground bg-primary": pathname.includes(e.link)
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <p className="drop-shadow-sm hidden md:block">{e.label}</p>
 | 
				
			||||||
 | 
					            <span className="block md:hidden">{e.icon}</span>
 | 
				
			||||||
 | 
					          </NavLink >))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      </ div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					import { ComponentProps } from "react"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function PageHeader(props: ComponentProps<"h1">) {
 | 
				
			||||||
 | 
					  return <h1 className="text-3xl font-bold mt-3">{props.children}</h1>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function PageSubHeader(props: ComponentProps<"h2">) {
 | 
				
			||||||
 | 
					  return <h2 className="text-xl font-bold">{props.children}</h2>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,37 @@
 | 
				
			||||||
 | 
					import { ContextMenuContent, ContextMenuItem, ContextMenuSubTrigger, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent } from "@/components/ui/context-menu"
 | 
				
			||||||
 | 
					import Link from "next/link"
 | 
				
			||||||
 | 
					import { ComponentProps, useState } from "react"
 | 
				
			||||||
 | 
					import { Row, Table, TableState } from "@tanstack/react-table"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function FormContextMenu({ table, row, openEditDialog, openDeleteDialog }: ComponentProps<"div"> & { table: Table<any>, row: Row<any>, openEditDialog: (row: Row<any>) => void, openDeleteDialog: (row: Row<any>) => void }) {
 | 
				
			||||||
 | 
					  //@ts-ignore
 | 
				
			||||||
 | 
					  const pathname = table.options.meta.pathname
 | 
				
			||||||
 | 
					  const selectedRows = table.getSelectedRowModel().flatRows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ContextMenuContent >
 | 
				
			||||||
 | 
					      {pathname !== "/submission" && selectedRows.length <= 1 ?
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <Link href={`${pathname}/${row.original.id}`}>
 | 
				
			||||||
 | 
					            <ContextMenuItem>Inspect</ContextMenuItem>
 | 
				
			||||||
 | 
					          </Link>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					        : ""
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ContextMenuItem onClick={() => openEditDialog(row)}>
 | 
				
			||||||
 | 
					        Edit
 | 
				
			||||||
 | 
					      </ContextMenuItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        selectedRows.length > 0 ?
 | 
				
			||||||
 | 
					          <ContextMenuItem onClick={() => { table.resetRowSelection() }}>Deselect</ContextMenuItem>
 | 
				
			||||||
 | 
					          : ""
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      <ContextMenuSeparator />
 | 
				
			||||||
 | 
					      <ContextMenuItem className="text-destructive" onClick={() => openDeleteDialog(row)}>Delete</ContextMenuItem>
 | 
				
			||||||
 | 
					    </ContextMenuContent>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,372 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ContextMenu,
 | 
				
			||||||
 | 
					  ContextMenuContent,
 | 
				
			||||||
 | 
					  ContextMenuItem,
 | 
				
			||||||
 | 
					  ContextMenuTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/context-menu"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DropdownMenu,
 | 
				
			||||||
 | 
					  DropdownMenuCheckboxItem,
 | 
				
			||||||
 | 
					  DropdownMenuContent,
 | 
				
			||||||
 | 
					  DropdownMenuTrigger,
 | 
				
			||||||
 | 
					  DropdownMenuRadioItem,
 | 
				
			||||||
 | 
					  DropdownMenuRadioGroup
 | 
				
			||||||
 | 
					} from "@/components/ui/dropdown-menu"
 | 
				
			||||||
 | 
					import { Input } from "@/components/ui/input"
 | 
				
			||||||
 | 
					import { Component, ComponentProps, use, useState } from "react"
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ColumnDef,
 | 
				
			||||||
 | 
					  flexRender,
 | 
				
			||||||
 | 
					  SortingState,
 | 
				
			||||||
 | 
					  getSortedRowModel,
 | 
				
			||||||
 | 
					  ColumnFiltersState,
 | 
				
			||||||
 | 
					  VisibilityState,
 | 
				
			||||||
 | 
					  getFilteredRowModel,
 | 
				
			||||||
 | 
					  getCoreRowModel,
 | 
				
			||||||
 | 
					  getPaginationRowModel,
 | 
				
			||||||
 | 
					  useReactTable,
 | 
				
			||||||
 | 
					  Row
 | 
				
			||||||
 | 
					} from "@tanstack/react-table"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Table,
 | 
				
			||||||
 | 
					  TableBody,
 | 
				
			||||||
 | 
					  TableCell,
 | 
				
			||||||
 | 
					  TableHead,
 | 
				
			||||||
 | 
					  TableHeader,
 | 
				
			||||||
 | 
					  TableRow,
 | 
				
			||||||
 | 
					} from "@/components/ui/table"
 | 
				
			||||||
 | 
					import { EyeIcon, Trash2 } from "lucide-react"
 | 
				
			||||||
 | 
					import { usePathname, useSearchParams } from "next/navigation"
 | 
				
			||||||
 | 
					import FormContextMenu from "./contextMenu"
 | 
				
			||||||
 | 
					import { deleteRecord, deleteRecords } from "app/lib/del"
 | 
				
			||||||
 | 
					import { Pathname } from "app/types"
 | 
				
			||||||
 | 
					import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTrigger } from "@/components/ui/dialog"
 | 
				
			||||||
 | 
					import pluralize from "app/lib/pluralize"
 | 
				
			||||||
 | 
					import { updateField, updatePub, updateStory } from "app/lib/update"
 | 
				
			||||||
 | 
					import { tableNameToItemName } from "app/lib/nameMaps"
 | 
				
			||||||
 | 
					import { Genre, Pub, Response, Story } from "@prisma/client"
 | 
				
			||||||
 | 
					import EditSubmissionDialog from "app/submission/edit"
 | 
				
			||||||
 | 
					import { DialogTitle } from "@radix-ui/react-dialog"
 | 
				
			||||||
 | 
					import { toast } from "@/components/ui/use-toast"
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation"
 | 
				
			||||||
 | 
					import EditStoryDialog from "app/story/edit"
 | 
				
			||||||
 | 
					import EditPubDialog from "app/publication/edit"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface DataTableProps<TData, TValue> {
 | 
				
			||||||
 | 
					  columns: ColumnDef<TData, TValue>[]
 | 
				
			||||||
 | 
					  data: TData[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function DataTable<TData, TValue>({
 | 
				
			||||||
 | 
					  columns,
 | 
				
			||||||
 | 
					  data,
 | 
				
			||||||
 | 
					  children,
 | 
				
			||||||
 | 
					  tableName,
 | 
				
			||||||
 | 
					  stories,
 | 
				
			||||||
 | 
					  pubs,
 | 
				
			||||||
 | 
					  responses,
 | 
				
			||||||
 | 
					  genres
 | 
				
			||||||
 | 
					}: DataTableProps<TData, TValue> & ComponentProps<"div"> & { tableName: string, stories?: Story[], pubs?: Pub[], responses?: Response[], genres?: Genre[] }) {
 | 
				
			||||||
 | 
					  //STATE
 | 
				
			||||||
 | 
					  const [isEditDialogVisible, setIsEditDialogVisible] = useState<boolean>(false)
 | 
				
			||||||
 | 
					  const [isDeleteDialogVisible, setIsDeleteDialogVisible] = useState<boolean>(false)
 | 
				
			||||||
 | 
					  const [dialogRow, SetDialogRow] = useState<Row<any> | null>(null)
 | 
				
			||||||
 | 
					  const [sorting, setSorting] = useState<SortingState>([])
 | 
				
			||||||
 | 
					  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(
 | 
				
			||||||
 | 
					    []
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					  const [columnVisibility, setColumnVisibility] =
 | 
				
			||||||
 | 
					    useState<VisibilityState>({})
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  const pathname: string = usePathname()
 | 
				
			||||||
 | 
					  const table = useReactTable({
 | 
				
			||||||
 | 
					    data,
 | 
				
			||||||
 | 
					    columns,
 | 
				
			||||||
 | 
					    enableRowSelection: true,
 | 
				
			||||||
 | 
					    enableMultiRowSelection: true,
 | 
				
			||||||
 | 
					    getCoreRowModel: getCoreRowModel(),
 | 
				
			||||||
 | 
					    getPaginationRowModel: getPaginationRowModel(),
 | 
				
			||||||
 | 
					    onSortingChange: setSorting,
 | 
				
			||||||
 | 
					    getSortedRowModel: getSortedRowModel(),
 | 
				
			||||||
 | 
					    onColumnFiltersChange: setColumnFilters,
 | 
				
			||||||
 | 
					    getFilteredRowModel: getFilteredRowModel(),
 | 
				
			||||||
 | 
					    onColumnVisibilityChange: setColumnVisibility,
 | 
				
			||||||
 | 
					    state: {
 | 
				
			||||||
 | 
					      sorting,
 | 
				
			||||||
 | 
					      columnFilters,
 | 
				
			||||||
 | 
					      columnVisibility,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    //this is where you put arbitrary functions etc to make them accessible via the table api
 | 
				
			||||||
 | 
					    meta: {
 | 
				
			||||||
 | 
					      updateTextField: updateField,
 | 
				
			||||||
 | 
					      tableName,
 | 
				
			||||||
 | 
					      pathname,
 | 
				
			||||||
 | 
					      stories,
 | 
				
			||||||
 | 
					      pubs,
 | 
				
			||||||
 | 
					      responses,
 | 
				
			||||||
 | 
					      genres
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function openEditDialog(row) {
 | 
				
			||||||
 | 
					    setIsEditDialogVisible(true)
 | 
				
			||||||
 | 
					    SetDialogRow(row)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  function openDeleteDialog(row) {
 | 
				
			||||||
 | 
					    setIsDeleteDialogVisible(true)
 | 
				
			||||||
 | 
					    SetDialogRow(row)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  function closeEditDialog() {
 | 
				
			||||||
 | 
					    setIsEditDialogVisible(false)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [filterBy, setFilterBy] = useState(table.getAllColumns()[0])
 | 
				
			||||||
 | 
					  const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
 | 
				
			||||||
 | 
					  return (<>
 | 
				
			||||||
 | 
					    <div className="flex justify-between items-center py-1 md:py-4">
 | 
				
			||||||
 | 
					      <div className="flex gap-2">
 | 
				
			||||||
 | 
					        <DropdownMenu>
 | 
				
			||||||
 | 
					          <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					            <Button variant="outline" className="hidden sm:block ml-auto">
 | 
				
			||||||
 | 
					              Filter by
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					          <DropdownMenuContent align="end">
 | 
				
			||||||
 | 
					            {/*@ts-ignore*/}
 | 
				
			||||||
 | 
					            <DropdownMenuRadioGroup value={filterBy} onValueChange={setFilterBy} >
 | 
				
			||||||
 | 
					              {table
 | 
				
			||||||
 | 
					                .getAllColumns()
 | 
				
			||||||
 | 
					                .filter((column) => column.getCanFilter())
 | 
				
			||||||
 | 
					                //@ts-ignore
 | 
				
			||||||
 | 
					                .map((column) => { return (<DropdownMenuRadioItem value={column} className="capitalize" key={column.id}> {column.id} </DropdownMenuRadioItem>) })}
 | 
				
			||||||
 | 
					            </DropdownMenuRadioGroup>
 | 
				
			||||||
 | 
					          </DropdownMenuContent>
 | 
				
			||||||
 | 
					        </DropdownMenu>
 | 
				
			||||||
 | 
					        <Input
 | 
				
			||||||
 | 
					          placeholder={`${filterBy.id}`}
 | 
				
			||||||
 | 
					          value={(table.getColumn(filterBy.id)?.getFilterValue() as string) ?? ""}
 | 
				
			||||||
 | 
					          onChange={(event) =>
 | 
				
			||||||
 | 
					            table.getColumn(filterBy.id)?.setFilterValue(event.target.value)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          className="max-w-sm"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Dialog open={isEditDialogVisible} onOpenChange={setIsEditDialogVisible}>
 | 
				
			||||||
 | 
					        <DialogContent>
 | 
				
			||||||
 | 
					          {tableName === "sub" ?
 | 
				
			||||||
 | 
					            <EditSubmissionDialog
 | 
				
			||||||
 | 
					              stories={stories}
 | 
				
			||||||
 | 
					              pubs={pubs}
 | 
				
			||||||
 | 
					              responses={responses}
 | 
				
			||||||
 | 
					              defaults={dialogRow?.original}
 | 
				
			||||||
 | 
					              closeDialog={closeEditDialog}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            : tableName === "story" ?
 | 
				
			||||||
 | 
					              < EditStoryDialog
 | 
				
			||||||
 | 
					                dbAction={updateStory}
 | 
				
			||||||
 | 
					                genres={genres}
 | 
				
			||||||
 | 
					                defaults={dialogRow?.original}
 | 
				
			||||||
 | 
					                closeDialog={closeEditDialog}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              : tableName === "pub" ?
 | 
				
			||||||
 | 
					                <EditPubDialog
 | 
				
			||||||
 | 
					                  dbAction={updatePub}
 | 
				
			||||||
 | 
					                  genres={genres}
 | 
				
			||||||
 | 
					                  defaults={dialogRow?.original}
 | 
				
			||||||
 | 
					                  closeDialog={closeEditDialog}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					                : ""
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        </DialogContent>
 | 
				
			||||||
 | 
					      </Dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Dialog open={isDeleteDialogVisible} onOpenChange={setIsDeleteDialogVisible}>
 | 
				
			||||||
 | 
					        <DialogContent>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <DialogHeader>
 | 
				
			||||||
 | 
					            <DialogTitle>Are you sure?</DialogTitle>
 | 
				
			||||||
 | 
					            <DialogDescription>
 | 
				
			||||||
 | 
					              Deleting a {tableNameToItemName(tableName)} cannot be undone!
 | 
				
			||||||
 | 
					            </DialogDescription>
 | 
				
			||||||
 | 
					          </DialogHeader>
 | 
				
			||||||
 | 
					          <DialogFooter>
 | 
				
			||||||
 | 
					            <DialogClose asChild>
 | 
				
			||||||
 | 
					              <Button variant="destructive"
 | 
				
			||||||
 | 
					                onClick={async () => {
 | 
				
			||||||
 | 
					                  const res = await deleteRecord(dialogRow.original.id, pathname)
 | 
				
			||||||
 | 
					                  if (!res) toast({ title: "Oh dear...", description: "Failed to delete." })
 | 
				
			||||||
 | 
					                  if (res) toast({ title: "Successfully deleted record of id:", description: dialogRow.original.id })
 | 
				
			||||||
 | 
					                  table.resetRowSelection()
 | 
				
			||||||
 | 
					                  router.refresh()
 | 
				
			||||||
 | 
					                }}>Yes, delete it!
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            </DialogClose>
 | 
				
			||||||
 | 
					          </DialogFooter>
 | 
				
			||||||
 | 
					        </DialogContent>
 | 
				
			||||||
 | 
					      </Dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Dialog>
 | 
				
			||||||
 | 
					        <DialogTrigger asChild>
 | 
				
			||||||
 | 
					          <Button variant="destructive" disabled={!(table.getIsSomeRowsSelected() || table.getIsAllRowsSelected())}>
 | 
				
			||||||
 | 
					            <Trash2 />
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </DialogTrigger>
 | 
				
			||||||
 | 
					        <DialogContent>
 | 
				
			||||||
 | 
					          <DialogHeader>
 | 
				
			||||||
 | 
					            <DialogTitle>Delete items?</DialogTitle>
 | 
				
			||||||
 | 
					            {`Delete ${Object.keys(table.getState().rowSelection).length} ${pluralize(pathname.slice(1))}?`}
 | 
				
			||||||
 | 
					          </DialogHeader>
 | 
				
			||||||
 | 
					          <DialogDescription>
 | 
				
			||||||
 | 
					            {/* @ts-ignore */}
 | 
				
			||||||
 | 
					            {`Deleting ${pluralize(tableNameToItemName(table.options.meta.tableName))} cannot be undone!`}
 | 
				
			||||||
 | 
					          </DialogDescription>
 | 
				
			||||||
 | 
					          <DialogFooter>
 | 
				
			||||||
 | 
					            <Button variant="destructive"
 | 
				
			||||||
 | 
					              onClick={async () => {
 | 
				
			||||||
 | 
					                const selectedRows = table.getState().rowSelection
 | 
				
			||||||
 | 
					                const rowIds = Object.keys(selectedRows)
 | 
				
			||||||
 | 
					                //@ts-ignore
 | 
				
			||||||
 | 
					                const recordIds = rowIds.map(id => Number(table.getRow(id).original.id))
 | 
				
			||||||
 | 
					                //@ts-ignore
 | 
				
			||||||
 | 
					                const res = await deleteRecords(recordIds, pathname)
 | 
				
			||||||
 | 
					                if (!res) toast({ title: "Oh dear...", description: "Failed to delete." })
 | 
				
			||||||
 | 
					                if (res) toast({ title: "Successfully deleted records of id:", description: JSON.stringify(recordIds) })
 | 
				
			||||||
 | 
					                table.resetRowSelection()
 | 
				
			||||||
 | 
					                setIsDeleteDialogVisible(false)
 | 
				
			||||||
 | 
					                router.refresh()
 | 
				
			||||||
 | 
					              }}>
 | 
				
			||||||
 | 
					              Yes, delete them!</Button>
 | 
				
			||||||
 | 
					          </DialogFooter>
 | 
				
			||||||
 | 
					        </DialogContent>
 | 
				
			||||||
 | 
					      </Dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <DropdownMenu>
 | 
				
			||||||
 | 
					        <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					          <Button variant="outline" className="justify-self-end">
 | 
				
			||||||
 | 
					            <EyeIcon />
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					        <DropdownMenuContent align="end">
 | 
				
			||||||
 | 
					          {table
 | 
				
			||||||
 | 
					            .getAllColumns()
 | 
				
			||||||
 | 
					            .filter(
 | 
				
			||||||
 | 
					              (column) => column.getCanHide()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .map((column) => {
 | 
				
			||||||
 | 
					              return (
 | 
				
			||||||
 | 
					                <DropdownMenuCheckboxItem
 | 
				
			||||||
 | 
					                  key={column.id}
 | 
				
			||||||
 | 
					                  className="capitalize"
 | 
				
			||||||
 | 
					                  checked={column.getIsVisible()}
 | 
				
			||||||
 | 
					                  onCheckedChange={(value) =>
 | 
				
			||||||
 | 
					                    column.toggleVisibility(!!value)
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {column.id}
 | 
				
			||||||
 | 
					                </DropdownMenuCheckboxItem>
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            })}
 | 
				
			||||||
 | 
					        </DropdownMenuContent>
 | 
				
			||||||
 | 
					      </DropdownMenu>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div className="rounded-md">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Table>
 | 
				
			||||||
 | 
					        <TableHeader>
 | 
				
			||||||
 | 
					          {table.getHeaderGroups().map((headerGroup) => (
 | 
				
			||||||
 | 
					            <TableRow key={headerGroup.id}>
 | 
				
			||||||
 | 
					              {headerGroup.headers.map((header) => {
 | 
				
			||||||
 | 
					                return (
 | 
				
			||||||
 | 
					                  <TableHead key={header.id}>
 | 
				
			||||||
 | 
					                    {header.isPlaceholder
 | 
				
			||||||
 | 
					                      ? null
 | 
				
			||||||
 | 
					                      : flexRender(
 | 
				
			||||||
 | 
					                        header.column.columnDef.header,
 | 
				
			||||||
 | 
					                        header.getContext()
 | 
				
			||||||
 | 
					                      )}
 | 
				
			||||||
 | 
					                  </TableHead>
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              })}
 | 
				
			||||||
 | 
					            </TableRow>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </TableHeader>
 | 
				
			||||||
 | 
					        <TableBody>
 | 
				
			||||||
 | 
					          {table.getRowModel().rows?.length ? (
 | 
				
			||||||
 | 
					            table.getRowModel().rows.map((row) => (
 | 
				
			||||||
 | 
					              <ContextMenu key={row.id + "contextMenu"}>
 | 
				
			||||||
 | 
					                <ContextMenuTrigger asChild>
 | 
				
			||||||
 | 
					                  <TableRow
 | 
				
			||||||
 | 
					                    key={row.id}
 | 
				
			||||||
 | 
					                    data-state={row.getIsSelected() && "selected"}
 | 
				
			||||||
 | 
					                    tabIndex={0}
 | 
				
			||||||
 | 
					                    onDoubleClick={() => {
 | 
				
			||||||
 | 
					                      if (tableName === "sub") {
 | 
				
			||||||
 | 
					                        openEditDialog(row)
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    {row.getVisibleCells().map((cell) => (
 | 
				
			||||||
 | 
					                      <TableCell key={cell.id}>
 | 
				
			||||||
 | 
					                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
 | 
				
			||||||
 | 
					                      </TableCell>
 | 
				
			||||||
 | 
					                    ))}
 | 
				
			||||||
 | 
					                    <FormContextMenu
 | 
				
			||||||
 | 
					                      key={"formContextMenu" + row.id}
 | 
				
			||||||
 | 
					                      row={row}
 | 
				
			||||||
 | 
					                      table={table}
 | 
				
			||||||
 | 
					                      openEditDialog={openEditDialog}
 | 
				
			||||||
 | 
					                      openDeleteDialog={openDeleteDialog}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </TableRow>
 | 
				
			||||||
 | 
					                </ContextMenuTrigger>
 | 
				
			||||||
 | 
					              </ContextMenu>
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					          ) : (
 | 
				
			||||||
 | 
					            <TableRow>
 | 
				
			||||||
 | 
					              <TableCell colSpan={columns.length} className="h-24 text-center">
 | 
				
			||||||
 | 
					                No results.
 | 
				
			||||||
 | 
					              </TableCell>
 | 
				
			||||||
 | 
					            </TableRow>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </TableBody>
 | 
				
			||||||
 | 
					      </Table>
 | 
				
			||||||
 | 
					      <div className="flex items-center justify-end space-x-2 py-4">
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="outline"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          onClick={() => table.previousPage()}
 | 
				
			||||||
 | 
					          disabled={!table.getCanPreviousPage()}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Previous
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          variant="outline"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          onClick={() => table.nextPage()}
 | 
				
			||||||
 | 
					          disabled={!table.getCanNextPage()}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Next
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,143 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { FormField, FormItem, FormLabel, FormMessage, FormControl, Form } from "@/components/ui/form"
 | 
				
			||||||
 | 
					import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button"
 | 
				
			||||||
 | 
					import { Checkbox } from "@/components/ui/checkbox"
 | 
				
			||||||
 | 
					import { BaseSyntheticEvent, ComponentProps, EventHandler, useState } from "react"
 | 
				
			||||||
 | 
					import { EventType, useForm, UseFormReturn } from "react-hook-form"
 | 
				
			||||||
 | 
					import { CellContext } from "@tanstack/react-table"
 | 
				
			||||||
 | 
					import { z } from "zod"
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod"
 | 
				
			||||||
 | 
					import { toast } from "@/components/ui/use-toast"
 | 
				
			||||||
 | 
					import GenreBadges from "app/ui/genreBadges"
 | 
				
			||||||
 | 
					import { updateField, updateGenres } from "app/lib/update"
 | 
				
			||||||
 | 
					import { Genre } from "@prisma/client"
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation"
 | 
				
			||||||
 | 
					export default function GenrePickerInputCell(props: CellContext<any, any>) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //@ts-ignore
 | 
				
			||||||
 | 
					  const table = props.table.options.meta.tableName
 | 
				
			||||||
 | 
					  //@ts-ignore
 | 
				
			||||||
 | 
					  const pathname = props.table.options.meta.pathname
 | 
				
			||||||
 | 
					  const id = props.row.original.id
 | 
				
			||||||
 | 
					  const column = props.column.id
 | 
				
			||||||
 | 
					  const value = props.cell.getValue()
 | 
				
			||||||
 | 
					  //@ts-ignore
 | 
				
			||||||
 | 
					  const genres = props.table.options.meta.genres
 | 
				
			||||||
 | 
					  const [isActive, setIsActive] = useState(false)
 | 
				
			||||||
 | 
					  const router = useRouter()
 | 
				
			||||||
 | 
					  async function onSubmit({ genres }: { genres: number[] }, event: BaseSyntheticEvent) {
 | 
				
			||||||
 | 
					    event.preventDefault()
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const genresArray = genres.map((e) => { return { id: e } })
 | 
				
			||||||
 | 
					      const res = await updateGenres({
 | 
				
			||||||
 | 
					        id,
 | 
				
			||||||
 | 
					        table,
 | 
				
			||||||
 | 
					        genres: genresArray,
 | 
				
			||||||
 | 
					        pathname
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      if (res === undefined) throw new Error("Something went wrong.")
 | 
				
			||||||
 | 
					      toast({ title: "Field updated successfully." })
 | 
				
			||||||
 | 
					      router.refresh()
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(error)
 | 
				
			||||||
 | 
					      toast({ title: "Something went wrong." })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setIsActive(false)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function onErrors(errors) {
 | 
				
			||||||
 | 
					    toast({
 | 
				
			||||||
 | 
					      title: "You have errors",
 | 
				
			||||||
 | 
					      description: (
 | 
				
			||||||
 | 
					        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
 | 
				
			||||||
 | 
					          <code className="text-white">{JSON.stringify(errors, null, 2)}</code>
 | 
				
			||||||
 | 
					        </pre>
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    console.log(JSON.stringify(errors))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const formSchema = z.object({
 | 
				
			||||||
 | 
					    genres: z.array(z.coerce.number())
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  const form = useForm<z.infer<typeof formSchema>>({
 | 
				
			||||||
 | 
					    resolver: zodResolver(formSchema),
 | 
				
			||||||
 | 
					    defaultValues: {
 | 
				
			||||||
 | 
					      genres: value.map(e => e.id)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const formId = "editGenresForm" + props.cell.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <Form {...form}>
 | 
				
			||||||
 | 
					      <form onSubmit={form.handleSubmit(onSubmit, onErrors)} id={formId}>
 | 
				
			||||||
 | 
					        <Popover modal={true} open={isActive} onOpenChange={() => setIsActive(prev => !prev)} >
 | 
				
			||||||
 | 
					          <FormField
 | 
				
			||||||
 | 
					            control={form.control}
 | 
				
			||||||
 | 
					            name="genres"
 | 
				
			||||||
 | 
					            render={({ field }) => (
 | 
				
			||||||
 | 
					              <FormItem className="w-full max-w-32 flex flex-col">
 | 
				
			||||||
 | 
					                <PopoverTrigger asChild>
 | 
				
			||||||
 | 
					                  {value.length > 0 ? <Button variant="ghost" className="h-fit p-0"><GenreBadges genres={value} className="w-full" /></Button> : <Button variant="outline" type="button" className="text-xs md:text-sm w-fit m-auto">Add genres</Button>
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                </PopoverTrigger>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <PopoverContent align="start">
 | 
				
			||||||
 | 
					                  {genres.map((item: Genre) => (
 | 
				
			||||||
 | 
					                    < FormField
 | 
				
			||||||
 | 
					                      key={item.id}
 | 
				
			||||||
 | 
					                      control={form.control}
 | 
				
			||||||
 | 
					                      name="genres"
 | 
				
			||||||
 | 
					                      render={({ field }) => {
 | 
				
			||||||
 | 
					                        return (
 | 
				
			||||||
 | 
					                          <FormItem
 | 
				
			||||||
 | 
					                            key={item.id}
 | 
				
			||||||
 | 
					                            className="flex flex-row items-start space-x-3 space-y-0"
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            <FormControl>
 | 
				
			||||||
 | 
					                              <Checkbox
 | 
				
			||||||
 | 
					                                checked={field.value?.includes(item.id)}
 | 
				
			||||||
 | 
					                                onCheckedChange={(checked) => {
 | 
				
			||||||
 | 
					                                  return checked
 | 
				
			||||||
 | 
					                                    ? field.onChange(
 | 
				
			||||||
 | 
					                                      [...field.value, item.id]
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    : field.onChange(
 | 
				
			||||||
 | 
					                                      field.value?.filter(
 | 
				
			||||||
 | 
					                                        (value) => value !== item.id
 | 
				
			||||||
 | 
					                                      )
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                }}
 | 
				
			||||||
 | 
					                              />
 | 
				
			||||||
 | 
					                            </FormControl>
 | 
				
			||||||
 | 
					                            <FormLabel className="text-sm font-normal">
 | 
				
			||||||
 | 
					                              {item.name}
 | 
				
			||||||
 | 
					                            </FormLabel>
 | 
				
			||||||
 | 
					                          </FormItem>
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                      }}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  ))}
 | 
				
			||||||
 | 
					                  <Button variant="ghost" form={formId}>Submit</Button>
 | 
				
			||||||
 | 
					                  <Button variant="link" className="p-0" onClick={() => form.setValue("genres", [])}>Clear</Button>
 | 
				
			||||||
 | 
					                </PopoverContent>
 | 
				
			||||||
 | 
					                <FormMessage />
 | 
				
			||||||
 | 
					              </FormItem>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Popover>
 | 
				
			||||||
 | 
					      </form>
 | 
				
			||||||
 | 
					    </Form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,97 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { Input } from "@/components/ui/input";
 | 
				
			||||||
 | 
					import { CellContext } from "@tanstack/react-table";
 | 
				
			||||||
 | 
					import { updateField } from "app/lib/update";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
 | 
					import { toast } from "@/components/ui/use-toast";
 | 
				
			||||||
 | 
					import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function NumberInputCell({ cellContext, className }: { cellContext: CellContext<any, any>, className: string }) {
 | 
				
			||||||
 | 
					  const [isActive, setIsActive] = useState(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //@ts-ignore
 | 
				
			||||||
 | 
					  const table = cellContext.table.options.meta.tableName
 | 
				
			||||||
 | 
					  const id = cellContext.row.original.id
 | 
				
			||||||
 | 
					  const column = cellContext.column.id
 | 
				
			||||||
 | 
					  //@ts-ignore
 | 
				
			||||||
 | 
					  const pathname = cellContext.table.options.meta.pathname
 | 
				
			||||||
 | 
					  const value = cellContext.cell.getValue()
 | 
				
			||||||
 | 
					  //@ts-ignore
 | 
				
			||||||
 | 
					  const formSchema = cellContext.column.columnDef.meta.formSchema.pick({ [column]: true })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const form = useForm<z.infer<typeof formSchema>>({
 | 
				
			||||||
 | 
					    resolver: zodResolver(formSchema),
 | 
				
			||||||
 | 
					    defaultValues: {
 | 
				
			||||||
 | 
					      [column]: cellContext.cell.getValue()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function onSubmit(value: z.infer<typeof formSchema>) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const res = await updateField({
 | 
				
			||||||
 | 
					        id,
 | 
				
			||||||
 | 
					        table,
 | 
				
			||||||
 | 
					        datum: value[column],
 | 
				
			||||||
 | 
					        column,
 | 
				
			||||||
 | 
					        pathname
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      if (res === undefined) throw new Error("something went wrong")
 | 
				
			||||||
 | 
					      toast({ title: "Field updated successfully." })
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(error)
 | 
				
			||||||
 | 
					      toast({ title: "Something went wrong." })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setIsActive(false)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function onErrors(errors: Error) {
 | 
				
			||||||
 | 
					    toast({
 | 
				
			||||||
 | 
					      title: "You have errors",
 | 
				
			||||||
 | 
					      description: errors.message,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    console.log(JSON.stringify(errors))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      onDoubleClick={() => setIsActive(true)}
 | 
				
			||||||
 | 
					      className={className + " w-full h-fit flex items-center justify-center"}
 | 
				
			||||||
 | 
					      tabIndex={0}
 | 
				
			||||||
 | 
					      onKeyDown={e => {
 | 
				
			||||||
 | 
					        if (e.code === "Enter" && !isActive) {
 | 
				
			||||||
 | 
					          e.preventDefault()
 | 
				
			||||||
 | 
					          setIsActive(true)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {isActive ?
 | 
				
			||||||
 | 
					        <Form {...form}>
 | 
				
			||||||
 | 
					          <form onSubmit={form.handleSubmit(onSubmit, onErrors)}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <FormField
 | 
				
			||||||
 | 
					              control={form.control}
 | 
				
			||||||
 | 
					              name={column}
 | 
				
			||||||
 | 
					              render={({ field }) => (
 | 
				
			||||||
 | 
					                <FormItem
 | 
				
			||||||
 | 
					                  onBlur={() => setIsActive(false)}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <FormControl
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    {/* @ts-ignore */}
 | 
				
			||||||
 | 
					                    <Input className="md:w-24" type="number" autoFocus={true} step={cellContext.column.columnDef.meta?.step} {...field} />
 | 
				
			||||||
 | 
					                  </FormControl>
 | 
				
			||||||
 | 
					                  <FormMessage />
 | 
				
			||||||
 | 
					                </FormItem>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          </form>
 | 
				
			||||||
 | 
					        </Form>
 | 
				
			||||||
 | 
					        : <p>{cellContext.cell.getValue()}</p>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    </div >
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,106 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					import { Input } from "@/components/ui/input";
 | 
				
			||||||
 | 
					import { CellContext } from "@tanstack/react-table";
 | 
				
			||||||
 | 
					import { updateField } from "app/lib/update";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
 | 
					import { toast } from "@/components/ui/use-toast";
 | 
				
			||||||
 | 
					import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
 | 
				
			||||||
 | 
					import TitleContainer from "app/ui/titleContainer";
 | 
				
			||||||
 | 
					import { useRouter } from "next/navigation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function TextInputCell({ cellContext, className }: { className: string, cellContext: CellContext<any, any> }) {
 | 
				
			||||||
 | 
					  const [isActive, setIsActive] = useState(false)
 | 
				
			||||||
 | 
					  //@ts-ignore
 | 
				
			||||||
 | 
					  const table = cellContext.table.options.meta.tableName
 | 
				
			||||||
 | 
					  const id = cellContext.row.original.id
 | 
				
			||||||
 | 
					  const column = cellContext.column.id
 | 
				
			||||||
 | 
					  //@ts-ignore
 | 
				
			||||||
 | 
					  const pathname = cellContext.table.options.meta.pathname
 | 
				
			||||||
 | 
					  const value = cellContext.cell.getValue()
 | 
				
			||||||
 | 
					  //@ts-ignore
 | 
				
			||||||
 | 
					  const formSchema = cellContext.column.columnDef.meta.formSchema.pick({ [column]: true })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const form = useForm<z.infer<typeof formSchema>>({
 | 
				
			||||||
 | 
					    resolver: zodResolver(formSchema),
 | 
				
			||||||
 | 
					    defaultValues: {
 | 
				
			||||||
 | 
					      [column]: cellContext.cell.getValue()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  const router = useRouter()
 | 
				
			||||||
 | 
					  async function onSubmit(value: z.infer<typeof formSchema>) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const res = await updateField({
 | 
				
			||||||
 | 
					        id,
 | 
				
			||||||
 | 
					        table,
 | 
				
			||||||
 | 
					        datum: value[column],
 | 
				
			||||||
 | 
					        column,
 | 
				
			||||||
 | 
					        pathname
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      if (res === undefined) throw new Error("something went wrong")
 | 
				
			||||||
 | 
					      toast({ title: "Field updated successfully." })
 | 
				
			||||||
 | 
					      router.refresh()
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(error)
 | 
				
			||||||
 | 
					      toast({ title: "Something went wrong." })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setIsActive(false)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function onErrors(errors) {
 | 
				
			||||||
 | 
					    toast({
 | 
				
			||||||
 | 
					      title: "You have errors",
 | 
				
			||||||
 | 
					      description: (
 | 
				
			||||||
 | 
					        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
 | 
				
			||||||
 | 
					          <code className="text-white">{JSON.stringify(errors, null, 2)}</code>
 | 
				
			||||||
 | 
					        </pre>
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    console.log(JSON.stringify(errors))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      onDoubleClick={() => setIsActive(prev => !prev)}
 | 
				
			||||||
 | 
					      className={className + " w-full h-fit flex items-center justify-left"}
 | 
				
			||||||
 | 
					      tabIndex={0}
 | 
				
			||||||
 | 
					      onKeyDown={e => {
 | 
				
			||||||
 | 
					        if (e.code === "Enter" && !isActive) {
 | 
				
			||||||
 | 
					          e.preventDefault()
 | 
				
			||||||
 | 
					          setIsActive(true)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {isActive ?
 | 
				
			||||||
 | 
					        <Form {...form}>
 | 
				
			||||||
 | 
					          <form onSubmit={form.handleSubmit(onSubmit, onErrors)}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <FormField
 | 
				
			||||||
 | 
					              control={form.control}
 | 
				
			||||||
 | 
					              name={column}
 | 
				
			||||||
 | 
					              render={({ field }) => (
 | 
				
			||||||
 | 
					                <FormItem
 | 
				
			||||||
 | 
					                  onBlur={() => setIsActive(false)}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <FormControl
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <Input
 | 
				
			||||||
 | 
					                      className="w-full"
 | 
				
			||||||
 | 
					                      type="text"
 | 
				
			||||||
 | 
					                      autoFocus={true}
 | 
				
			||||||
 | 
					                      {...field}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </FormControl>
 | 
				
			||||||
 | 
					                  <FormMessage />
 | 
				
			||||||
 | 
					                </FormItem>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          </form>
 | 
				
			||||||
 | 
					        </Form>
 | 
				
			||||||
 | 
					        : <TitleContainer>{cellContext.cell.getValue()}</TitleContainer>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    </div >
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					import { Checkbox } from "@/components/ui/checkbox";
 | 
				
			||||||
 | 
					import { CellContext, Column, ColumnDef, ColumnMeta, Header, HeaderContext, RowSelectionTableState, Table, TableState } from "@tanstack/react-table";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const selectCol = {
 | 
				
			||||||
 | 
					  id: "select",
 | 
				
			||||||
 | 
					  header: (props: HeaderContext<any, any>) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className="flex items-start justify-left mx-auto">
 | 
				
			||||||
 | 
					        <Checkbox
 | 
				
			||||||
 | 
					          className="mr-4 ml-2"
 | 
				
			||||||
 | 
					          checked={props.table.getIsAllRowsSelected()}
 | 
				
			||||||
 | 
					          onCheckedChange={props.table.toggleAllRowsSelected}
 | 
				
			||||||
 | 
					          aria-label="select/deselect all rows"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  cell: (props: CellContext<any, any>) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className="flex items-start justify-left">
 | 
				
			||||||
 | 
					        <Checkbox
 | 
				
			||||||
 | 
					          className="mr-4 ml-2"
 | 
				
			||||||
 | 
					          checked={props.row.getIsSelected()}
 | 
				
			||||||
 | 
					          onCheckedChange={props.row.toggleSelected}
 | 
				
			||||||
 | 
					          aria-label="select/deselect row"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					"use client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import * as React from "react"
 | 
				
			||||||
 | 
					import { ThemeProvider as NextThemesProvider } from "next-themes"
 | 
				
			||||||
 | 
					import { type ThemeProviderProps } from "next-themes/dist/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
 | 
				
			||||||
 | 
					  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue