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
 | 
			
		||||
*.tsbuildinfo
 | 
			
		||||
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
 | 
			
		||||
npm run dev
 | 
			
		||||
# or
 | 
			
		||||
yarn dev
 | 
			
		||||
# or
 | 
			
		||||
pnpm dev
 | 
			
		||||
# or
 | 
			
		||||
bun dev
 | 
			
		||||
```
 | 
			
		||||
My previous attempt at this project was a Nodejs server with a React frontend, but this version is much better!
 | 
			
		||||
 | 
			
		||||
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} */
 | 
			
		||||
const nextConfig = {};
 | 
			
		||||
const nextConfig = {
 | 
			
		||||
  webpack: (config) => {
 | 
			
		||||
    config.externals = [...config.externals, "bcrypt"];
 | 
			
		||||
    return config;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default nextConfig;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										44
									
								
								package.json
								
								
								
								
							
							
						
						
									
										44
									
								
								package.json
								
								
								
								
							| 
						 | 
				
			
			@ -7,21 +7,57 @@
 | 
			
		|||
    "dev": "next dev",
 | 
			
		||||
    "build": "next build",
 | 
			
		||||
    "start": "next start",
 | 
			
		||||
    "lint": "next lint"
 | 
			
		||||
    "lint": "next lint",
 | 
			
		||||
    "tailwind": "npx tailwindcss -i ./src/app/globals.css -o ./src/app/tailwind.css --watch"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@hookform/resolvers": "^3.6.0",
 | 
			
		||||
    "@mapbox/node-pre-gyp": "^1.0.11",
 | 
			
		||||
    "@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-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": {
 | 
			
		||||
    "@types/node": "^20",
 | 
			
		||||
    "@types/react": "^18",
 | 
			
		||||
    "@types/react-dom": "^18",
 | 
			
		||||
    "eslint": "^8",
 | 
			
		||||
    "autoprefixer": "^10.4.19",
 | 
			
		||||
    "eslint-config-next": "14.2.3",
 | 
			
		||||
    "postcss": "^8.4.38",
 | 
			
		||||
    "prisma": "^5.15.0",
 | 
			
		||||
    "tailwindcss": "^3.4.4",
 | 
			
		||||
    "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"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
model User {
 | 
			
		||||
id Int @id @default(autoincrement())
 | 
			
		||||
email String
 | 
			
		||||
password String
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
model Story {
 | 
			
		||||
  id         Int     @id @default(autoincrement())
 | 
			
		||||
  word_count Int
 | 
			
		||||
  title      String
 | 
			
		||||
  deleted    Int     @default(0)
 | 
			
		||||
  subs       Sub[]
 | 
			
		||||
  genres     Genre[]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +30,6 @@ model Pub {
 | 
			
		|||
  title            String
 | 
			
		||||
  link             String  @default("")
 | 
			
		||||
  query_after_days Int
 | 
			
		||||
  deleted          Int     @default(0)
 | 
			
		||||
  subs             Sub[]
 | 
			
		||||
  genres           Genre[]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -44,12 +49,12 @@ model Response {
 | 
			
		|||
 | 
			
		||||
model Sub {
 | 
			
		||||
  id         Int      @id @default(autoincrement())
 | 
			
		||||
  story      Story    @relation(fields: [storyId], references: [id])
 | 
			
		||||
  storyId    Int
 | 
			
		||||
  pub        Pub      @relation(fields: [pubId], references: [id])
 | 
			
		||||
  pubId      Int
 | 
			
		||||
  story      Story?    @relation(fields: [storyId], references: [id])
 | 
			
		||||
  storyId    Int?
 | 
			
		||||
  pub        Pub?      @relation(fields: [pubId], references: [id])
 | 
			
		||||
  pubId      Int?
 | 
			
		||||
  submitted  String
 | 
			
		||||
  responded  String
 | 
			
		||||
  response   Response @relation(fields: [responseId], references: [id])
 | 
			
		||||
  responseId Int
 | 
			
		||||
  responded  String?
 | 
			
		||||
  response   Response? @relation(fields: [responseId], references: [id])
 | 
			
		||||
  responseId Int?
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { PrismaClient } from '@prisma/client'
 | 
			
		||||
import { PrismaClient } from "@prisma/client";
 | 
			
		||||
 | 
			
		||||
const prisma = new PrismaClient()
 | 
			
		||||
const prisma = new PrismaClient();
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
  // ... you will write your Prisma Client queries here
 | 
			
		||||
| 
						 | 
				
			
			@ -8,20 +8,17 @@ async function main() {
 | 
			
		|||
    where: { id: 1 },
 | 
			
		||||
    data: {
 | 
			
		||||
      title: "Ghost Aliens of Mars",
 | 
			
		||||
			genres: { set: [{ id: 1 }, { id: 2 }], create: { name: "alien-punk" } }
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	})
 | 
			
		||||
	console.log(story)
 | 
			
		||||
 | 
			
		||||
      genres: { set: [{ id: 1 }, { id: 2 }], create: { name: "alien-punk" } },
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main()
 | 
			
		||||
  .then(async () => {
 | 
			
		||||
		await prisma.$disconnect()
 | 
			
		||||
    await prisma.$disconnect();
 | 
			
		||||
  })
 | 
			
		||||
  .catch(async (e) => {
 | 
			
		||||
		console.error(e)
 | 
			
		||||
		await prisma.$disconnect()
 | 
			
		||||
		process.exit(1)
 | 
			
		||||
	})
 | 
			
		||||
    console.error(e);
 | 
			
		||||
    await prisma.$disconnect();
 | 
			
		||||
    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 @@
 | 
			
		|||
@tailwind base;
 | 
			
		||||
@tailwind components;
 | 
			
		||||
@tailwind utilities;
 | 
			
		||||
 | 
			
		||||
/**/
 | 
			
		||||
/* @layer base { */
 | 
			
		||||
/*   :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%; */
 | 
			
		||||
/*   } */
 | 
			
		||||
/* } */
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  :root {
 | 
			
		||||
  --max-width: 1100px;
 | 
			
		||||
  --border-radius: 12px;
 | 
			
		||||
  --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;
 | 
			
		||||
  --background-end-rgb: 255, 255, 255;
 | 
			
		||||
 | 
			
		||||
  --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;
 | 
			
		||||
  --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;
 | 
			
		||||
    --background: 220 0% 96%;
 | 
			
		||||
    --foreground: 222.2 84% 4.9%;
 | 
			
		||||
    --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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  :root {
 | 
			
		||||
    --foreground-rgb: 255, 255, 255;
 | 
			
		||||
    --background-start-rgb: 0, 0, 0;
 | 
			
		||||
    --background-end-rgb: 0, 0, 0;
 | 
			
		||||
 | 
			
		||||
    --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
 | 
			
		||||
    --secondary-glow: linear-gradient(
 | 
			
		||||
      to bottom right,
 | 
			
		||||
      rgba(1, 65, 255, 0),
 | 
			
		||||
      rgba(1, 65, 255, 0),
 | 
			
		||||
      rgba(1, 65, 255, 0.3)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    --tile-start-rgb: 2, 13, 46;
 | 
			
		||||
    --tile-end-rgb: 2, 5, 19;
 | 
			
		||||
    --tile-border: conic-gradient(
 | 
			
		||||
      #ffffff80,
 | 
			
		||||
      #ffffff40,
 | 
			
		||||
      #ffffff30,
 | 
			
		||||
      #ffffff20,
 | 
			
		||||
      #ffffff10,
 | 
			
		||||
      #ffffff10,
 | 
			
		||||
      #ffffff80
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    --callout-rgb: 20, 20, 20;
 | 
			
		||||
    --callout-border-rgb: 108, 108, 108;
 | 
			
		||||
    --card-rgb: 100, 100, 100;
 | 
			
		||||
    --card-border-rgb: 200, 200, 200;
 | 
			
		||||
  .dark {
 | 
			
		||||
    --background: 222.2 40% 4%;
 | 
			
		||||
    --foreground: 210 40% 98%;
 | 
			
		||||
    --muted: 217.2 32.6% 17.5%;
 | 
			
		||||
    --muted-foreground: 215 20.2% 65.1%;
 | 
			
		||||
    --popover: 230 25% 10%;
 | 
			
		||||
    --popover-foreground: 210 40% 98%;
 | 
			
		||||
    --card: 222.2 20% 6%;
 | 
			
		||||
    --card-foreground: 210 40% 98%;
 | 
			
		||||
    --border: 217.2 20% 10%;
 | 
			
		||||
    --input: 217.2 32.6% 17.5%;
 | 
			
		||||
    --primary: 155 70% 35%;
 | 
			
		||||
    --primary-foreground: 80 10% 97.84%;
 | 
			
		||||
    --secondary: 200 50% 98%;
 | 
			
		||||
    --secondary-foreground: 155 85% 30%;
 | 
			
		||||
    --accent: 170 60% 10%;
 | 
			
		||||
    --accent-foreground: 155 60% 65%;
 | 
			
		||||
    --destructive: 5 90% 65%;
 | 
			
		||||
    --destructive-foreground: 0 100% 10%;
 | 
			
		||||
    --ring: 160 90% 45%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**/
 | 
			
		||||
/* @layer base { */
 | 
			
		||||
/*   :root { */
 | 
			
		||||
/*     --background: 258 70% 100%; */
 | 
			
		||||
/*     --foreground: 258 77% 0%; */
 | 
			
		||||
/*     --muted: 258 29% 85%; */
 | 
			
		||||
/*     --muted-foreground: 258 10% 40%; */
 | 
			
		||||
/*     --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%; */
 | 
			
		||||
/*   } */
 | 
			
		||||
/* } */
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  * {
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html,
 | 
			
		||||
body {
 | 
			
		||||
  max-width: 100vw;
 | 
			
		||||
  overflow-x: hidden;
 | 
			
		||||
    @apply border-border;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  body {
 | 
			
		||||
  color: rgb(var(--foreground-rgb));
 | 
			
		||||
  background: linear-gradient(
 | 
			
		||||
      to bottom,
 | 
			
		||||
      transparent,
 | 
			
		||||
      rgb(var(--background-end-rgb))
 | 
			
		||||
    )
 | 
			
		||||
    rgb(var(--background-start-rgb));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a {
 | 
			
		||||
  color: inherit;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  html {
 | 
			
		||||
    color-scheme: dark;
 | 
			
		||||
    @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 { 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 = {
 | 
			
		||||
  title: "Create Next App",
 | 
			
		||||
  description: "Generated by create next app",
 | 
			
		||||
  title: "Subman",
 | 
			
		||||
  description: "A self-hosted literary submission tracker."
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default function RootLayout({
 | 
			
		||||
  children,
 | 
			
		||||
}: Readonly<{
 | 
			
		||||
| 
						 | 
				
			
			@ -17,10 +25,31 @@ export default function RootLayout({
 | 
			
		|||
  return (
 | 
			
		||||
    <html lang="en">
 | 
			
		||||
      <body className={inter.className}>
 | 
			
		||||
        <div id="sidebar">
 | 
			
		||||
          SIDEBAR
 | 
			
		||||
        <ThemeProvider
 | 
			
		||||
          attribute="class"
 | 
			
		||||
          defaultTheme="system"
 | 
			
		||||
          enableSystem
 | 
			
		||||
          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>
 | 
			
		||||
        {children}</body>
 | 
			
		||||
              <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 (
 | 
			
		||||
    <main className={styles.main}>
 | 
			
		||||
      Hello
 | 
			
		||||
 | 
			
		||||
      <h1 className="text-3xl font-black underline">
 | 
			
		||||
        Hello world!
 | 
			
		||||
      </h1>
 | 
			
		||||
    </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