Compare commits
	
		
			140 Commits
		
	
	
		
			009433f483
			...
			3b6652c617
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
								 | 
						3b6652c617 | |
| 
							
							
								
								 | 
						89e338a0ac | |
| 
							
							
								
								 | 
						17578d50d6 | |
| 
							
							
								
								 | 
						c8879b04c9 | |
| 
							
							
								
								 | 
						b7bca4cacc | |
| 
							
							
								
								 | 
						71f5a44c8e | |
| 
							
							
								
								 | 
						cd90c92c6d | |
| 
							
							
								
								 | 
						055b3c254d | |
| 
							
							
								
								 | 
						79e3403902 | |
| 
							
							
								
								 | 
						41951a2ac6 | |
| 
							
							
								
								 | 
						0fa28a46eb | |
| 
							
							
								
								 | 
						6ee4128c85 | |
| 
							
							
								
								 | 
						aec413ba7a | |
| 
							
							
								
								 | 
						57cc55f414 | |
| 
							
							
								
								 | 
						bc244497cd | |
| 
							
							
								
								 | 
						29ab837aca | |
| 
							
							
								
								 | 
						1bad3ba5f8 | |
| 
							
							
								
								 | 
						2294d0c0b0 | |
| 
							
							
								
								 | 
						2e1409cf46 | |
| 
							
							
								
								 | 
						f3dbd2cb9e | |
| 
							
							
								
								 | 
						5206e415ed | |
| 
							
							
								
								 | 
						fe9878cb7a | |
| 
							
							
								
								 | 
						b6a56fca2b | |
| 
							
							
								
								 | 
						8c4b9d27f2 | |
| 
							
							
								
								 | 
						5ea7a61915 | |
| 
							
							
								
								 | 
						7c6f06e194 | |
| 
							
							
								
								 | 
						540413173c | |
| 
							
							
								
								 | 
						330226ecd6 | |
| 
							
							
								
								 | 
						23584a0a50 | |
| 
							
							
								
								 | 
						8ad3583c4e | |
| 
							
							
								
								 | 
						c6496130e3 | |
| 
							
							
								
								 | 
						97a537f5a2 | |
| 
							
							
								
								 | 
						1fca1a2b81 | |
| 
							
							
								
								 | 
						4aa7194427 | |
| 
							
							
								
								 | 
						f163de99c8 | |
| 
							
							
								
								 | 
						10408f604a | |
| 
							
							
								
								 | 
						ee2a7c4cbf | |
| 
							
							
								
								 | 
						06fb2831ef | |
| 
							
							
								
								 | 
						5b919db59b | |
| 
							
							
								
								 | 
						06b69b5ce7 | |
| 
							
							
								
								 | 
						54a001183a | |
| 
							
							
								
								 | 
						d210b13bde | |
| 
							
							
								
								 | 
						45af32d091 | |
| 
							
							
								
								 | 
						0c39838f6a | |
| 
							
							
								
								 | 
						788051fa10 | |
| 
							
							
								
								 | 
						191457d6c1 | |
| 
							
							
								
								 | 
						b5745a3c05 | |
| 
							
							
								
								 | 
						878daf35bb | |
| 
							
							
								
								 | 
						494521d51d | |
| 
							
							
								
								 | 
						aeb7bc1f6f | |
| 
							
							
								
								 | 
						bea291aa92 | |
| 
							
							
								
								 | 
						a691250637 | |
| 
							
							
								
								 | 
						7dd912d6f6 | |
| 
							
							
								
								 | 
						be83489ea6 | |
| 
							
							
								
								 | 
						4145d84d65 | |
| 
							
							
								
								 | 
						ef70bd9d92 | |
| 
							
							
								
								 | 
						e1e4ce23f1 | |
| 
							
							
								
								 | 
						5563de438a | |
| 
							
							
								
								 | 
						c2bfee6b87 | |
| 
							
							
								
								 | 
						a4a2ba35cd | |
| 
							
							
								
								 | 
						21bee8cc8b | |
| 
							
							
								
								 | 
						447b4a7edd | |
| 
							
							
								
								 | 
						26eb4cd9eb | |
| 
							
							
								
								 | 
						e91caeb51c | |
| 
							
							
								
								 | 
						10b512bb5c | |
| 
							
							
								
								 | 
						c2107b14a3 | |
| 
							
							
								
								 | 
						27b368e0cb | |
| 
							
							
								
								 | 
						c4b61069fd | |
| 
							
							
								
								 | 
						be32c7e0a6 | |
| 
							
							
								
								 | 
						8d2bf53a1c | |
| 
							
							
								
								 | 
						be765fda2a | |
| 
							
							
								
								 | 
						b32aabcd08 | |
| 
							
							
								
								 | 
						f245b8d72d | |
| 
							
							
								
								 | 
						04688feb28 | |
| 
							
							
								
								 | 
						febaec3220 | |
| 
							
							
								
								 | 
						40f2360ebd | |
| 
							
							
								
								 | 
						1546a2ff31 | |
| 
							
							
								
								 | 
						c8f374f754 | |
| 
							
							
								
								 | 
						c8b25c36f5 | |
| 
							
							
								
								 | 
						1db71fb21b | |
| 
							
							
								
								 | 
						a9257c2825 | |
| 
							
							
								
								 | 
						4f41415a80 | |
| 
							
							
								
								 | 
						98b2b1e3cc | |
| 
							
							
								
								 | 
						c3ee490ce5 | |
| 
							
							
								
								 | 
						e1391bec62 | |
| 
							
							
								
								 | 
						5aaa45cade | |
| 
							
							
								
								 | 
						96db18580e | |
| 
							
							
								
								 | 
						782ccb76f5 | |
| 
							
							
								
								 | 
						c63175a0f8 | |
| 
							
							
								
								 | 
						d87eb3b342 | |
| 
							
							
								
								 | 
						c7149fc8af | |
| 
							
							
								
								 | 
						4a8b6f72df | |
| 
							
							
								
								 | 
						8c62f7addf | |
| 
							
							
								
								 | 
						6ceb035b19 | |
| 
							
							
								
								 | 
						fdf05f2b4c | |
| 
							
							
								
								 | 
						ec96a1e988 | |
| 
							
							
								
								 | 
						6dc05d2610 | |
| 
							
							
								
								 | 
						d8a2f3df7e | |
| 
							
							
								
								 | 
						b891780881 | |
| 
							
							
								
								 | 
						525f716f16 | |
| 
							
							
								
								 | 
						7b68a7451e | |
| 
							
							
								
								 | 
						f454f6739e | |
| 
							
							
								
								 | 
						99b2fb9628 | |
| 
							
							
								
								 | 
						7b994ec06e | |
| 
							
							
								
								 | 
						52a30ec141 | |
| 
							
							
								
								 | 
						3a91fd7cb4 | |
| 
							
							
								
								 | 
						6839c1c369 | |
| 
							
							
								
								 | 
						285cef524c | |
| 
							
							
								
								 | 
						9583d0da16 | |
| 
							
							
								
								 | 
						b5b8d8ad09 | |
| 
							
							
								
								 | 
						13a9407caa | |
| 
							
							
								
								 | 
						0eb09073ca | |
| 
							
							
								
								 | 
						de2c8991c6 | |
| 
							
							
								
								 | 
						fff436f87c | |
| 
							
							
								
								 | 
						e7f0cf3fb6 | |
| 
							
							
								
								 | 
						1f3655f14c | |
| 
							
							
								
								 | 
						6080037d83 | |
| 
							
							
								
								 | 
						688c260f4b | |
| 
							
							
								
								 | 
						3a56b1f31e | |
| 
							
							
								
								 | 
						2d340983e6 | |
| 
							
							
								
								 | 
						3151236ca0 | |
| 
							
							
								
								 | 
						d4d73750b3 | |
| 
							
							
								
								 | 
						5ad03054a9 | |
| 
							
							
								
								 | 
						7539b8a577 | |
| 
							
							
								
								 | 
						02d111098d | |
| 
							
							
								
								 | 
						b604ed48da | |
| 
							
							
								
								 | 
						f503647469 | |
| 
							
							
								
								 | 
						f3e5233171 | |
| 
							
							
								
								 | 
						47756280d9 | |
| 
							
							
								
								 | 
						58d1fd1ed4 | |
| 
							
							
								
								 | 
						ce8b52cb87 | |
| 
							
							
								
								 | 
						c3ae4721d4 | |
| 
							
							
								
								 | 
						438599a530 | |
| 
							
							
								
								 | 
						50409895c0 | |
| 
							
							
								
								 | 
						ed8e71694f | |
| 
							
							
								
								 | 
						d54b8180ce | |
| 
							
							
								
								 | 
						b36f0edfb1 | |
| 
							
							
								
								 | 
						25f8f728c9 | |
| 
							
							
								
								 | 
						d1c69c9c15 | |
| 
							
							
								
								 | 
						dd07c259ac | 
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
# Environment variables declared in this file are automatically made available to Prisma.
 | 
			
		||||
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
 | 
			
		||||
 | 
			
		||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
 | 
			
		||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
 | 
			
		||||
 | 
			
		||||
DATABASE_URL="file:./dev.db"
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -12,7 +12,6 @@
 | 
			
		|||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@hookform/resolvers": "^3.6.0",
 | 
			
		||||
    "@mapbox/node-pre-gyp": "^1.0.11",
 | 
			
		||||
    "@prisma/client": "^5.15.0",
 | 
			
		||||
    "@radix-ui/react-checkbox": "^1.0.4",
 | 
			
		||||
    "@radix-ui/react-context-menu": "^2.2.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -25,15 +24,11 @@
 | 
			
		|||
    "@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": "14.2.3",
 | 
			
		||||
    "next-themes": "^0.3.0",
 | 
			
		||||
    "react": "^18",
 | 
			
		||||
    "react-day-picker": "^8.10.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +37,6 @@
 | 
			
		|||
    "recharts": "^2.12.7",
 | 
			
		||||
    "tailwind-merge": "^2.3.0",
 | 
			
		||||
    "tailwindcss-animate": "^1.0.7",
 | 
			
		||||
    "text-encoding": "^0.7.0",
 | 
			
		||||
    "zod": "^3.23.8"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										
											BIN
										
									
								
								prisma/dev.db
								
								
								
								
							
							
						
						
									
										
											BIN
										
									
								
								prisma/dev.db
								
								
								
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
 | 
			
		|||
  <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",
 | 
			
		||||
      "peer h-4 w-4 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}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,7 +73,7 @@ const TableHead = React.forwardRef<
 | 
			
		|||
  <th
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "h-12 md:px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
 | 
			
		||||
      "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +87,7 @@ const TableCell = React.forwardRef<
 | 
			
		|||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <td
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("md:p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
 | 
			
		||||
    className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,6 @@ import "./globals.css";
 | 
			
		|||
import Navlinks from "./ui/navLinks";
 | 
			
		||||
import { ModeToggle } from "./ui/modeToggle";
 | 
			
		||||
import { inter } from "./ui/fonts";
 | 
			
		||||
import LogoutButton from "./ui/logoutButton";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +15,7 @@ export const metadata: Metadata = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default function RootLayout({
 | 
			
		||||
  children,
 | 
			
		||||
}: Readonly<{
 | 
			
		||||
| 
						 | 
				
			
			@ -31,16 +30,14 @@ export default function RootLayout({
 | 
			
		|||
          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>
 | 
			
		||||
          <div id="layout-container" className="p-4 w-screen h-screen mt-6 flex justify-center">
 | 
			
		||||
            <div className="w-5/6 flex">
 | 
			
		||||
              <div id="sidebar" className="h-5/6 flex flex-col"> <header className="">
 | 
			
		||||
                <h1 className="font-black text-4xl text-primary-foreground bg-primary antialiased w-full p-2 rounded-tl-3xl pl-6 pr-4">SubMan</h1>
 | 
			
		||||
                <p className="mt-2 mx-1 text-sm antialiased w-40">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>
 | 
			
		||||
                <Navlinks className="mt-6" />
 | 
			
		||||
                <footer className="mt-auto"><ModeToggle /></footer>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="flex justify-center w-full">
 | 
			
		||||
                {children}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,80 +1,59 @@
 | 
			
		|||
"use server"
 | 
			
		||||
import { Pub, Story, Sub } from "@prisma/client"
 | 
			
		||||
import { Genre, Story } 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"
 | 
			
		||||
import { redirect } from "next/navigation"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export async function createStory({ story, genres }: { story: Story, genres: number[] }): Promise<{ success: string }> {
 | 
			
		||||
	// will return undefined if middleware authorization fails
 | 
			
		||||
export async function createStory(data: Story & { genres: number[] }) {
 | 
			
		||||
	"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({
 | 
			
		||||
	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")
 | 
			
		||||
		return { success: `Created the story '${story.title}'.` }
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		console.error(error)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	redirect("/story")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export async function createPub({ pub, genres }: { pub: Pub, genres: number[] }): Promise<{ success: string }> {
 | 
			
		||||
export async function createPub(data) {
 | 
			
		||||
	"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 genresArray = data.genres.map(e => { return { id: e } })
 | 
			
		||||
	const res = await prisma.pub.create({
 | 
			
		||||
			data: pub
 | 
			
		||||
		data: {
 | 
			
		||||
			title: data.title,
 | 
			
		||||
			link: data.link,
 | 
			
		||||
			query_after_days: data.query_after_days
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	console.log(res)
 | 
			
		||||
	const genresRes = await prisma.pub.update({
 | 
			
		||||
		where: { id: res.id },
 | 
			
		||||
		data:
 | 
			
		||||
			{ genres: { set: genresArray } }
 | 
			
		||||
	})
 | 
			
		||||
	console.log(genresRes)
 | 
			
		||||
	revalidatePath("/publication")
 | 
			
		||||
		return { success: `Created the publication '${pub.title}'.` }
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		console.error(error)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	redirect("/publication")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export async function createSub(data: Sub): Promise<Sub | boolean> {
 | 
			
		||||
export async function createSub(data) {
 | 
			
		||||
	"use server"
 | 
			
		||||
	try {
 | 
			
		||||
		subSchema.parse(data)
 | 
			
		||||
	const res = await prisma.sub.create({ data })
 | 
			
		||||
	console.log(res)
 | 
			
		||||
	revalidatePath("/submission")
 | 
			
		||||
		return res
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		console.error(error)
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	redirect("/submission")
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,22 +10,16 @@ const tableMap = {
 | 
			
		|||
	"/submission": "sub"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function deleteRecord(id: number, pathname: string): Promise<undefined | boolean> {
 | 
			
		||||
export async function deleteRecord(id: number, pathname: Pathname) {
 | 
			
		||||
	const table = tableMap[pathname]
 | 
			
		||||
	try {
 | 
			
		||||
		//@ts-ignore
 | 
			
		||||
	const res = await prisma[table].delete({ where: { id } })
 | 
			
		||||
	console.log(`deleted from ${table}: ${res.id}`)
 | 
			
		||||
	console.log("revalidating: " + pathname)
 | 
			
		||||
	revalidatePath(pathname)
 | 
			
		||||
		return true
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		console.error(error)
 | 
			
		||||
		return undefined
 | 
			
		||||
	}
 | 
			
		||||
	redirect(pathname)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function deleteRecords(ids: number[], pathname: "/story" | "/publication" | "/submission"): Promise<boolean | undefined> {
 | 
			
		||||
	try {
 | 
			
		||||
export async function deleteRecords(ids: number[], pathname: "/story" | "/publication" | "/submission") {
 | 
			
		||||
	const table = tableMap[pathname]
 | 
			
		||||
	ids.forEach(async (id) => {
 | 
			
		||||
		const res = await prisma[table].delete({
 | 
			
		||||
| 
						 | 
				
			
			@ -34,10 +28,6 @@ export async function deleteRecords(ids: number[], pathname: "/story" | "/public
 | 
			
		|||
		console.log(`deleted from ${table}: ${res.id}`)
 | 
			
		||||
	})
 | 
			
		||||
	revalidatePath(pathname)
 | 
			
		||||
		return true
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		console.error(error)
 | 
			
		||||
		return undefined
 | 
			
		||||
	}
 | 
			
		||||
	redirect(pathname)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
"use server"
 | 
			
		||||
import prisma from "./db"
 | 
			
		||||
import { revalidatePath } from "next/cache"
 | 
			
		||||
import { redirect } from "next/navigation"
 | 
			
		||||
import { SubForm } from "app/ui/forms/sub"
 | 
			
		||||
 | 
			
		||||
export async function editSubmission(data: SubForm) {
 | 
			
		||||
	const res = await prisma.sub.update({
 | 
			
		||||
		where: { id: data.id },
 | 
			
		||||
		data
 | 
			
		||||
	})
 | 
			
		||||
	console.log(`updated ${data} to ${res}`)
 | 
			
		||||
	revalidatePath("/submission")
 | 
			
		||||
	redirect("/submission")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
export default function pluralize(word: string): string {
 | 
			
		||||
export default function pluralize(word: "story" | "publication" | "submission"): string {
 | 
			
		||||
	const map = {
 | 
			
		||||
		story: "stories",
 | 
			
		||||
		publication: "publications",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,11 @@
 | 
			
		|||
"use server"
 | 
			
		||||
import { prepGenreData, prepPubData, prepStoryData } from "./validate"
 | 
			
		||||
import { Genre, Pub, Story, Sub } from "@prisma/client"
 | 
			
		||||
import { Genre } 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"
 | 
			
		||||
import { redirect } from "next/navigation"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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: {
 | 
			
		||||
| 
						 | 
				
			
			@ -18,16 +14,10 @@ export async function updateField({ datum, table, column, id, pathname }: { datu
 | 
			
		|||
	})
 | 
			
		||||
	console.log(`updated record in ${table}: ${JSON.stringify(res)}`)
 | 
			
		||||
	revalidatePath(pathname)
 | 
			
		||||
		return res
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		console.error(error)
 | 
			
		||||
		return null
 | 
			
		||||
	}
 | 
			
		||||
	redirect(pathname)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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: {
 | 
			
		||||
| 
						 | 
				
			
			@ -36,78 +26,5 @@ export async function updateGenres({ genres, table, id, pathname }: { genres: {
 | 
			
		|||
	})
 | 
			
		||||
	console.log(`updated record in ${table}: ${JSON.stringify(res)}`)
 | 
			
		||||
	revalidatePath(pathname)
 | 
			
		||||
		return res
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		console.error(error)
 | 
			
		||||
		return null
 | 
			
		||||
	redirect(pathname)
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ export default async function Page({ params }: { params: { id: string } }) {
 | 
			
		|||
      <PageHeader>{pub.title}</PageHeader>
 | 
			
		||||
      <GenreBadges genres={pub.genres} className="my-6" />
 | 
			
		||||
      <PageSubHeader>Submissions:</PageSubHeader>
 | 
			
		||||
      <DataTable columns={columns} data={pubSubs} tableName="sub" />
 | 
			
		||||
      <DataTable columns={columns} data={pubSubs} type="submission" />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,19 @@
 | 
			
		|||
"use client"
 | 
			
		||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
 | 
			
		||||
import { ArrowUpDown, BookType, Clock, Drama, SquareArrowOutUpRight } from "lucide-react"
 | 
			
		||||
import { ArrowUpDown } from "lucide-react"
 | 
			
		||||
import { Button } from "@/components/ui/button"
 | 
			
		||||
import { PubWithGenres } from "./page"
 | 
			
		||||
import { Badge } from "@/components/ui/badge"
 | 
			
		||||
import { PubsWithGenres } 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 { formSchema } from "app/ui/forms/pub"
 | 
			
		||||
import GenrePickerInputCell from "app/ui/tables/inputs/genrePickerInput"
 | 
			
		||||
import { genrePickerFilterFn } from "app/lib/filterFns"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const columnHelper = createColumnHelper<PubWithGenres>()
 | 
			
		||||
const columnHelper = createColumnHelper<PubsWithGenres>()
 | 
			
		||||
 | 
			
		||||
export const columns: ColumnDef<PubWithGenres>[] = [
 | 
			
		||||
export const columns: ColumnDef<PubsWithGenres>[] = [
 | 
			
		||||
  selectCol,
 | 
			
		||||
  {
 | 
			
		||||
    accessorKey: "title",
 | 
			
		||||
| 
						 | 
				
			
			@ -23,73 +23,35 @@ export const columns: ColumnDef<PubWithGenres>[] = [
 | 
			
		|||
          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" />
 | 
			
		||||
          <ArrowUpDown className="ml-2 h-4 w-4" />
 | 
			
		||||
        </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 }
 | 
			
		||||
    cell: TextInputCell,
 | 
			
		||||
    meta: { formSchema }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    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 }
 | 
			
		||||
    header: "Link",
 | 
			
		||||
    cell: TextInputCell,
 | 
			
		||||
    meta: { formSchema }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
    filterFn: "arrIncludes"
 | 
			
		||||
    //TODO - write custom filter function, to account for an array of objects
 | 
			
		||||
  }),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    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" />
 | 
			
		||||
      </>
 | 
			
		||||
    ),
 | 
			
		||||
    header: "Query After (days)",
 | 
			
		||||
    cell: NumberInputCell,
 | 
			
		||||
    meta: {
 | 
			
		||||
      step: 10,
 | 
			
		||||
      formSchema: pubSchema
 | 
			
		||||
      formSchema
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,31 +5,21 @@ 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}>
 | 
			
		||||
    <Dialog>
 | 
			
		||||
      <DialogTrigger asChild>
 | 
			
		||||
        <Button>
 | 
			
		||||
          <span className="hidden md:block">Create new publication</span>
 | 
			
		||||
          <Plus className="block md:hidden" />
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button>Create new publication</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} />
 | 
			
		||||
        <PubForm createPub={createPub} genres={genres} />
 | 
			
		||||
        <DialogFooter>
 | 
			
		||||
          <Button form="pubform">Submit</Button>
 | 
			
		||||
        </DialogFooter>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ import { columns } from "./columns";
 | 
			
		|||
import { DataTable } from "app/ui/tables/data-table";
 | 
			
		||||
import CreatePubDialog from "./create";
 | 
			
		||||
 | 
			
		||||
export type PubWithGenres = Pub & { genres: Array<Genre> }
 | 
			
		||||
export type PubsWithGenres = Pub & { genres: Array<Genre> }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ export default async function Page() {
 | 
			
		|||
  const genres = await getGenres()
 | 
			
		||||
  const pubs = await getPubsWithGenres()
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="container px-0 md:px-4 mx-auto">
 | 
			
		||||
    <div className="container mx-auto">
 | 
			
		||||
      <DataTable data={pubs} columns={columns} tableName="pub" genres={genres}>
 | 
			
		||||
        <CreatePubDialog genres={genres} />
 | 
			
		||||
      </DataTable>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,14 @@
 | 
			
		|||
"use client"
 | 
			
		||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
 | 
			
		||||
import { StoryWithGenres } from "./page"
 | 
			
		||||
import { ArrowUpDown, BookType, Drama, Tally5 } from "lucide-react"
 | 
			
		||||
import { ArrowUpDown } from "lucide-react"
 | 
			
		||||
import { Button } from "@/components/ui/button"
 | 
			
		||||
import GenreBadges from "app/ui/genreBadges"
 | 
			
		||||
import { selectCol } from "app/ui/tables/selectColumn"
 | 
			
		||||
import NumberInputCell from "app/ui/tables/inputs/numberInput"
 | 
			
		||||
import { storySchema } from "app/ui/forms/schemas"
 | 
			
		||||
import { formSchema } from "app/ui/forms/story"
 | 
			
		||||
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>[] = [
 | 
			
		||||
| 
						 | 
				
			
			@ -19,25 +19,16 @@ export const columns: ColumnDef<StoryWithGenres>[] = [
 | 
			
		|||
      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" />
 | 
			
		||||
          <ArrowUpDown className="ml-2 h-4 w-4" />
 | 
			
		||||
        </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 }
 | 
			
		||||
    cell: TextInputCell,
 | 
			
		||||
    meta: { formSchema }
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    accessorKey: "word_count",
 | 
			
		||||
| 
						 | 
				
			
			@ -45,41 +36,23 @@ export const columns: ColumnDef<StoryWithGenres>[] = [
 | 
			
		|||
      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" />
 | 
			
		||||
          <ArrowUpDown className="ml-2 h-4 w-4" />
 | 
			
		||||
        </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" />
 | 
			
		||||
      </>
 | 
			
		||||
    ),
 | 
			
		||||
    cell: NumberInputCell,
 | 
			
		||||
    meta: {
 | 
			
		||||
      step: 50,
 | 
			
		||||
      formSchema: storySchema
 | 
			
		||||
      formSchema
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  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,
 | 
			
		||||
    filterFn: "arrIncludes",
 | 
			
		||||
    meta: {}
 | 
			
		||||
  }),
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,33 +2,24 @@
 | 
			
		|||
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 { ComponentProps } 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}>
 | 
			
		||||
    <Dialog>
 | 
			
		||||
      <DialogTrigger asChild>
 | 
			
		||||
        <div>
 | 
			
		||||
          <Button className="hidden md:block">Create new story</Button>
 | 
			
		||||
          <Button className="block md:hidden"><Plus /> </Button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <Button>Create new story</Button>
 | 
			
		||||
      </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} />
 | 
			
		||||
        <StoryForm createStory={createStory} genres={genres} existingData={null} />
 | 
			
		||||
        <DialogFooter>
 | 
			
		||||
          <Button form="storyform">Submit</Button>
 | 
			
		||||
        </DialogFooter>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,10 +4,9 @@ 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 }> {
 | 
			
		||||
	async function createStory(data) {
 | 
			
		||||
		"use server"
 | 
			
		||||
		const genresArray = data.genres.map(e => { return { id: e } })
 | 
			
		||||
		const res = await prisma.story.create({
 | 
			
		||||
| 
						 | 
				
			
			@ -27,14 +26,12 @@ export default async function Page() {
 | 
			
		|||
		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" />
 | 
			
		||||
				<StoryForm genres={genres} createStory={createStory} className="mt-6" />
 | 
			
		||||
			</CreateContainerContent>
 | 
			
		||||
		</CreateContainer>
 | 
			
		||||
	)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ export default async function Page() {
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="container px-1 md:px-4 mx-auto">
 | 
			
		||||
    <div className="container mx-auto">
 | 
			
		||||
      <DataTable columns={columns} data={storiesWithGenres} tableName="story"
 | 
			
		||||
        genres={genres}
 | 
			
		||||
      >
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
"use client"
 | 
			
		||||
import { LineChart, Line, CartesianGrid, XAxis, YAxis, PieChart, Pie } from "recharts"
 | 
			
		||||
import { SubComplete } from "./page"
 | 
			
		||||
export function SubsChart({ data }: { data: Array<SubComplete> }) {
 | 
			
		||||
  const pieData: Array<{ story: string, occurrences: number }> = []
 | 
			
		||||
  data.forEach(dataRow => {
 | 
			
		||||
    const story = dataRow.story.title
 | 
			
		||||
    const exists = pieData.findIndex(pieRow => story === pieRow.story)
 | 
			
		||||
    if (exists === -1) {
 | 
			
		||||
      //add the story to pieData if it doesn't already exist
 | 
			
		||||
      pieData.push({ story: story, occurrences: 0 })
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    pieData[exists].occurrences++
 | 
			
		||||
  })
 | 
			
		||||
  console.log(pieData)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <PieChart width={400} height={400}>
 | 
			
		||||
        <Pie data={pieData} dataKey="story" outerRadius={50} fill="teal" />
 | 
			
		||||
      </PieChart>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
      <LineChart width={400} height={400} data={data}>
 | 
			
		||||
        <Line type="monotone" dataKey="id" stroke="#8884d8" />
 | 
			
		||||
        <CartesianGrid />
 | 
			
		||||
        <XAxis dataKey="submitted" />
 | 
			
		||||
        <YAxis />
 | 
			
		||||
 | 
			
		||||
      </LineChart>
 | 
			
		||||
    </>
 | 
			
		||||
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +1,10 @@
 | 
			
		|||
"use client"
 | 
			
		||||
import { CellContext, ColumnDef, createColumnHelper } from "@tanstack/react-table"
 | 
			
		||||
import { ArrowUpDown, BookText, CalendarMinus, CalendarPlus, MessageCircleReply, NotepadText } from "lucide-react"
 | 
			
		||||
import { ArrowUpDown } 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"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19,12 +18,7 @@ export const columns: ColumnDef<SubComplete>[] = [
 | 
			
		|||
      return "RECORD DELETED"
 | 
			
		||||
    },
 | 
			
		||||
    id: "story",
 | 
			
		||||
    header: () => (
 | 
			
		||||
      <>
 | 
			
		||||
        <span className="hidden md:block">Story</span>
 | 
			
		||||
        <NotepadText className="block md:hidden" />
 | 
			
		||||
      </>
 | 
			
		||||
    ),
 | 
			
		||||
    header: "Story",
 | 
			
		||||
    cell: (props: CellContext<any, any>) => (<TitleContainer>{props.getValue()}</TitleContainer>)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
| 
						 | 
				
			
			@ -35,12 +29,7 @@ export const columns: ColumnDef<SubComplete>[] = [
 | 
			
		|||
      return "RECORD DELETED"
 | 
			
		||||
    },
 | 
			
		||||
    id: "pub",
 | 
			
		||||
    header: () => (
 | 
			
		||||
      <>
 | 
			
		||||
        <span className="hidden md:block">Publication</span>
 | 
			
		||||
        <BookText className="block md:hidden" />
 | 
			
		||||
      </>
 | 
			
		||||
    ),
 | 
			
		||||
    header: "Publication",
 | 
			
		||||
    cell: (props: CellContext<any, any>) => (<TitleContainer>{props.getValue()}</TitleContainer>)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
| 
						 | 
				
			
			@ -50,24 +39,16 @@ export const columns: ColumnDef<SubComplete>[] = [
 | 
			
		|||
      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" />
 | 
			
		||||
          Date Submitted
 | 
			
		||||
          <ArrowUpDown className="ml-2 h-4 w-4" />
 | 
			
		||||
        </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>
 | 
			
		||||
    )
 | 
			
		||||
    cell: (props: CellContext<any, any>) => (<p className="w-full text-center">{props.getValue().toLocaleDateString()}</p>)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    accessorFn: row => row.responded ? new Date(row.responded) : null,
 | 
			
		||||
| 
						 | 
				
			
			@ -76,22 +57,16 @@ export const columns: ColumnDef<SubComplete>[] = [
 | 
			
		|||
      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" />
 | 
			
		||||
          Date Responded
 | 
			
		||||
          <ArrowUpDown className="ml-2 h-4 w-4" />
 | 
			
		||||
        </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>)
 | 
			
		||||
    cell: (props: CellContext<any, any>) => (<p className="w-full text-center">{props.getValue()?.toLocaleDateString()}</p>)
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    accessorFn: row => {
 | 
			
		||||
| 
						 | 
				
			
			@ -101,20 +76,8 @@ export const columns: ColumnDef<SubComplete>[] = [
 | 
			
		|||
      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>)
 | 
			
		||||
    header: "Response",
 | 
			
		||||
    cell: (props: CellContext<any, any>) => (<p className="w-full text-center">{props.getValue()}</p>)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,33 +6,33 @@ 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)
 | 
			
		||||
type CreateSubDefaults = {
 | 
			
		||||
  subId?: number,
 | 
			
		||||
  storyId: number,
 | 
			
		||||
  pubId: number,
 | 
			
		||||
  submitted: Date,
 | 
			
		||||
  responded: Date,
 | 
			
		||||
  respoonseId: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function CreateSubmissionDialog({ stories, pubs, responses, defaults }: ComponentProps<"div"> & { stories: Story[], pubs: Pub[], responses: Response[], defaults?: CreateSubDefaults }) {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog open={isOpen} onOpenChange={setIsOpen}>
 | 
			
		||||
    <Dialog>
 | 
			
		||||
      <DialogTrigger asChild>
 | 
			
		||||
        <Button>
 | 
			
		||||
          <span className="hidden md:block">Create new submission</span>
 | 
			
		||||
          <Plus className="block md:hidden" />
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button>Create new submission</Button>
 | 
			
		||||
      </DialogTrigger>
 | 
			
		||||
      <DialogContent className="text-xs md:text-sm">
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <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} />
 | 
			
		||||
        <SubmissionForm createSub={createSub} pubs={pubs} responses={responses} stories={stories} defaults={defaults} />
 | 
			
		||||
        <DialogFooter>
 | 
			
		||||
          <DialogClose asChild>
 | 
			
		||||
          </DialogClose>
 | 
			
		||||
          <Button form="subform">Submit</Button>
 | 
			
		||||
        </DialogFooter>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,12 +10,19 @@ export default async function Page() {
 | 
			
		|||
	const stories = await getStories()
 | 
			
		||||
	const pubs = await getPubs()
 | 
			
		||||
	const responses = await getResponses()
 | 
			
		||||
	async function createSub(data) {
 | 
			
		||||
		"use server"
 | 
			
		||||
		const res = await prisma.sub.create({ data })
 | 
			
		||||
		console.log(res)
 | 
			
		||||
		revalidatePath("/submission")
 | 
			
		||||
		redirect("/submission")
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	return (
 | 
			
		||||
		<CreateContainer>
 | 
			
		||||
			<CreateContainerHeader>New submission</CreateContainerHeader>
 | 
			
		||||
			<CreateContainerContent>
 | 
			
		||||
				<SubmissionForm stories={stories} pubs={pubs} responses={responses} />
 | 
			
		||||
				<SubmissionForm stories={stories} pubs={pubs} responses={responses} createSub={createSub} />
 | 
			
		||||
			</CreateContainerContent>
 | 
			
		||||
		</CreateContainer>
 | 
			
		||||
	)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,14 @@
 | 
			
		|||
"use client"
 | 
			
		||||
import { DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
 | 
			
		||||
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 { SubForm } from "app/ui/forms/sub";
 | 
			
		||||
import EditSubmissionForm from "app/ui/forms/editSub";
 | 
			
		||||
import SubmissionForm, { SubForm } from "app/ui/forms/sub";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default function EditSubmissionDialog({ stories, pubs, responses, defaults, children, closeDialog }: ComponentProps<"div"> & { stories: Story[], pubs: Pub[], responses: Response[], defaults: SubForm, closeDialog: () => void }) {
 | 
			
		||||
 | 
			
		||||
export default function EditSubmissionDialog({ stories, pubs, responses, defaults, children }: ComponentProps<"div"> & { stories: Story[], pubs: Pub[], responses: Response[], defaults: SubForm }) {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
| 
						 | 
				
			
			@ -16,8 +16,10 @@ export default function EditSubmissionDialog({ stories, pubs, responses, default
 | 
			
		|||
        <DialogTitle>Edit Submission</DialogTitle>
 | 
			
		||||
        <DialogDescription>Change response status, edit dates etc</DialogDescription>
 | 
			
		||||
      </DialogHeader>
 | 
			
		||||
      <EditSubmissionForm pubs={pubs} responses={responses} stories={stories} defaults={defaults} closeDialog={closeDialog} />
 | 
			
		||||
      <SubmissionForm pubs={pubs} responses={responses} stories={stories} defaults={defaults} />
 | 
			
		||||
      <DialogFooter>
 | 
			
		||||
        <DialogClose asChild>
 | 
			
		||||
        </DialogClose>
 | 
			
		||||
        <Button form="subform">Submit</Button>
 | 
			
		||||
      </DialogFooter>
 | 
			
		||||
    </>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ import { DataTable } from "app/ui/tables/data-table"
 | 
			
		|||
import { columns } from "./columns"
 | 
			
		||||
import { Pub, Response, Story, Sub } from "@prisma/client"
 | 
			
		||||
import CreateSubmissionDialog from "./create"
 | 
			
		||||
import { Trash2 } from "lucide-react"
 | 
			
		||||
 | 
			
		||||
export type SubComplete = Sub & {
 | 
			
		||||
  pub: Pub,
 | 
			
		||||
| 
						 | 
				
			
			@ -10,15 +11,13 @@ export type SubComplete = Sub & {
 | 
			
		|||
  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">
 | 
			
		||||
    <div className="container">
 | 
			
		||||
      <DataTable data={subs} columns={columns} tableName="sub"
 | 
			
		||||
        stories={stories}
 | 
			
		||||
        pubs={pubs}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -740,9 +740,8 @@ body {
 | 
			
		|||
  margin-bottom: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.my-auto {
 | 
			
		||||
  margin-top: auto;
 | 
			
		||||
  margin-bottom: auto;
 | 
			
		||||
.mb-4 {
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ml-2 {
 | 
			
		||||
| 
						 | 
				
			
			@ -753,22 +752,10 @@ body {
 | 
			
		|||
  margin-left: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mr-2 {
 | 
			
		||||
  margin-right: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mr-4 {
 | 
			
		||||
  margin-right: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mt-2 {
 | 
			
		||||
  margin-top: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mt-20 {
 | 
			
		||||
  margin-top: 5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mt-3 {
 | 
			
		||||
  margin-top: 0.75rem;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -781,8 +768,8 @@ body {
 | 
			
		|||
  margin-top: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block {
 | 
			
		||||
  display: block;
 | 
			
		||||
.mt-auto {
 | 
			
		||||
  margin-top: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex {
 | 
			
		||||
| 
						 | 
				
			
			@ -805,10 +792,6 @@ body {
 | 
			
		|||
  display: grid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hidden {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.size-full {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -850,6 +833,10 @@ body {
 | 
			
		|||
  height: 1.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.h-5\/6 {
 | 
			
		||||
  height: 83.333333%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.h-7 {
 | 
			
		||||
  height: 1.75rem;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -927,6 +914,10 @@ body {
 | 
			
		|||
  width: 10rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.w-5\/6 {
 | 
			
		||||
  width: 83.333333%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.w-7 {
 | 
			
		||||
  width: 1.75rem;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -981,26 +972,6 @@ body {
 | 
			
		|||
  min-width: fit-content;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-16 {
 | 
			
		||||
  max-width: 4rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-24 {
 | 
			
		||||
  max-width: 6rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-28 {
 | 
			
		||||
  max-width: 7rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-32 {
 | 
			
		||||
  max-width: 8rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-60 {
 | 
			
		||||
  max-width: 15rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-full {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1013,6 +984,10 @@ body {
 | 
			
		|||
  max-width: 24rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.max-w-xs {
 | 
			
		||||
  max-width: 20rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.shrink-0 {
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1121,10 +1096,6 @@ body {
 | 
			
		|||
  justify-content: space-between;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.justify-around {
 | 
			
		||||
  justify-content: space-around;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.gap-1 {
 | 
			
		||||
  gap: 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1204,12 +1175,6 @@ body {
 | 
			
		|||
  margin-bottom: calc(1rem * var(--tw-space-y-reverse));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.space-y-6 > :not([hidden]) ~ :not([hidden]) {
 | 
			
		||||
  --tw-space-y-reverse: 0;
 | 
			
		||||
  margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
 | 
			
		||||
  margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.space-y-8 > :not([hidden]) ~ :not([hidden]) {
 | 
			
		||||
  --tw-space-y-reverse: 0;
 | 
			
		||||
  margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
 | 
			
		||||
| 
						 | 
				
			
			@ -1228,24 +1193,10 @@ body {
 | 
			
		|||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.truncate {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.whitespace-nowrap {
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.break-words {
 | 
			
		||||
  overflow-wrap: break-word;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rounded-3xl {
 | 
			
		||||
  border-radius: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rounded-full {
 | 
			
		||||
  border-radius: 9999px;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1258,6 +1209,11 @@ body {
 | 
			
		|||
  border-radius: calc(var(--radius) - 4px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rounded-l-3xl {
 | 
			
		||||
  border-top-left-radius: 1.5rem;
 | 
			
		||||
  border-bottom-left-radius: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rounded-t-3xl {
 | 
			
		||||
  border-top-left-radius: 1.5rem;
 | 
			
		||||
  border-top-right-radius: 1.5rem;
 | 
			
		||||
| 
						 | 
				
			
			@ -1388,16 +1344,6 @@ body {
 | 
			
		|||
  padding: 1.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.px-0 {
 | 
			
		||||
  padding-left: 0px;
 | 
			
		||||
  padding-right: 0px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.px-1 {
 | 
			
		||||
  padding-left: 0.25rem;
 | 
			
		||||
  padding-right: 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.px-2 {
 | 
			
		||||
  padding-left: 0.5rem;
 | 
			
		||||
  padding-right: 0.5rem;
 | 
			
		||||
| 
						 | 
				
			
			@ -1498,6 +1444,11 @@ body {
 | 
			
		|||
  line-height: 2.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-4xl {
 | 
			
		||||
  font-size: 2.25rem;
 | 
			
		||||
  line-height: 2.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-\[0\.8rem\] {
 | 
			
		||||
  font-size: 0.8rem;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2352,22 +2303,6 @@ body {
 | 
			
		|||
    top: auto;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .sm\:block {
 | 
			
		||||
    display: block;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .sm\:hidden {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .sm\:h-6 {
 | 
			
		||||
    height: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .sm\:w-6 {
 | 
			
		||||
    width: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .sm\:flex-row {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -2406,87 +2341,15 @@ body {
 | 
			
		|||
    text-align: left;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .sm\:text-4xl {
 | 
			
		||||
    font-size: 2.25rem;
 | 
			
		||||
    line-height: 2.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .data-\[state\=open\]\:sm\:slide-in-from-bottom-full[data-state=open] {
 | 
			
		||||
    --tw-enter-translate-y: 100%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (min-width: 768px) {
 | 
			
		||||
  .md\:mt-6 {
 | 
			
		||||
    margin-top: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:mt-auto {
 | 
			
		||||
    margin-top: auto;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:block {
 | 
			
		||||
    display: block;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:hidden {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:w-24 {
 | 
			
		||||
    width: 6rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:w-5\/6 {
 | 
			
		||||
    width: 83.333333%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:max-w-\[420px\] {
 | 
			
		||||
    max-width: 420px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:flex-row {
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:flex-col {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:space-y-8 > :not([hidden]) ~ :not([hidden]) {
 | 
			
		||||
    --tw-space-y-reverse: 0;
 | 
			
		||||
    margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
 | 
			
		||||
    margin-bottom: calc(2rem * var(--tw-space-y-reverse));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:rounded-l-3xl {
 | 
			
		||||
    border-top-left-radius: 1.5rem;
 | 
			
		||||
    border-bottom-left-radius: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:p-4 {
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:px-4 {
 | 
			
		||||
    padding-left: 1rem;
 | 
			
		||||
    padding-right: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:py-4 {
 | 
			
		||||
    padding-top: 1rem;
 | 
			
		||||
    padding-bottom: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:text-base {
 | 
			
		||||
    font-size: 1rem;
 | 
			
		||||
    line-height: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .md\:text-sm {
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
    line-height: 1.25rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.\[\&\:has\(\[aria-selected\]\)\]\:bg-accent:has([aria-selected]) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import { CheckboxReactHookFormMultiple } from "app/ui/forms/Checkboxdemo";
 | 
			
		||||
 | 
			
		||||
export default function Page() {
 | 
			
		||||
  return (
 | 
			
		||||
    <CheckboxReactHookFormMultiple />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,121 @@
 | 
			
		|||
"use client"
 | 
			
		||||
 | 
			
		||||
import { z } from "zod"
 | 
			
		||||
import { zodResolver } from "@hookform/resolvers/zod"
 | 
			
		||||
import { useForm } from "react-hook-form"
 | 
			
		||||
import { Genre } from "@prisma/client"
 | 
			
		||||
import { Button } from "@/components/ui/button"
 | 
			
		||||
import {
 | 
			
		||||
	Form,
 | 
			
		||||
	FormControl,
 | 
			
		||||
	FormDescription,
 | 
			
		||||
	FormField,
 | 
			
		||||
	FormItem,
 | 
			
		||||
	FormLabel,
 | 
			
		||||
	FormMessage,
 | 
			
		||||
} from "@/components/ui/form"
 | 
			
		||||
import { Input } from "@/components/ui/input"
 | 
			
		||||
import { Checkbox } from "@/components/ui/checkbox"
 | 
			
		||||
 | 
			
		||||
const formSchema = z.object({
 | 
			
		||||
	title: z.string().min(2).max(50),
 | 
			
		||||
	word_count: z.number(),
 | 
			
		||||
	genres: z.object({ id: z.number(), name: z.string() }).array()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default function FancyForm({ genres }) {
 | 
			
		||||
	// 1. Define your form.
 | 
			
		||||
	const form = useForm<z.infer<typeof formSchema>>({
 | 
			
		||||
		resolver: zodResolver(formSchema),
 | 
			
		||||
		defaultValues: {
 | 
			
		||||
			title: "",
 | 
			
		||||
			word_count: 0,
 | 
			
		||||
			genres: genres
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	// 2. Define a submit handler.
 | 
			
		||||
	function onSubmit(values: z.infer<typeof formSchema>) {
 | 
			
		||||
		// Do something with the form values.
 | 
			
		||||
		// ✅ This will be type-safe and validated.
 | 
			
		||||
		console.log(values)
 | 
			
		||||
	}
 | 
			
		||||
	return (
 | 
			
		||||
		<Form {...form}>
 | 
			
		||||
			<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
 | 
			
		||||
				<FormField
 | 
			
		||||
					control={form.control}
 | 
			
		||||
					name="title"
 | 
			
		||||
					render={({ field }) => (
 | 
			
		||||
						<FormItem>
 | 
			
		||||
							<FormLabel>Title</FormLabel>
 | 
			
		||||
							<FormControl>
 | 
			
		||||
								<Input placeholder="title goes here..." {...field} />
 | 
			
		||||
							</FormControl>
 | 
			
		||||
							<FormMessage />
 | 
			
		||||
						</FormItem>
 | 
			
		||||
					)}
 | 
			
		||||
				/>
 | 
			
		||||
				<FormField
 | 
			
		||||
					control={form.control}
 | 
			
		||||
					name="word_count"
 | 
			
		||||
					render={({ field }) => (
 | 
			
		||||
						<FormItem>
 | 
			
		||||
							<FormLabel>Word count</FormLabel>
 | 
			
		||||
							<FormControl>
 | 
			
		||||
								<Input type="number" step={500} min={0} {...field}></Input>
 | 
			
		||||
							</FormControl>
 | 
			
		||||
						</FormItem>
 | 
			
		||||
					)}
 | 
			
		||||
				/>
 | 
			
		||||
				<FormField
 | 
			
		||||
					control={form.control}
 | 
			
		||||
					name="genres"
 | 
			
		||||
					render={({ field }) => (
 | 
			
		||||
						<FormItem>
 | 
			
		||||
							<div className="mb-4">
 | 
			
		||||
								<FormLabel>Genres</FormLabel>
 | 
			
		||||
								<FormDescription>genres baby</FormDescription>
 | 
			
		||||
							</div>
 | 
			
		||||
							{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>
 | 
			
		||||
										)
 | 
			
		||||
									}}
 | 
			
		||||
								/>
 | 
			
		||||
							))}
 | 
			
		||||
						</FormItem>
 | 
			
		||||
					)}
 | 
			
		||||
 | 
			
		||||
				/>
 | 
			
		||||
				<Button type="submit">Submit</Button>
 | 
			
		||||
			</form>
 | 
			
		||||
		</Form>
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ export default function GenrePicker({ genres, form }: ComponentProps<"div"> & {
 | 
			
		|||
							<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",
 | 
			
		||||
									"min-w-fit max-w-full w-fit pl-3 text-left font-normal flex-wrap gap-y-1 h-fit min-h-10",
 | 
			
		||||
									!field.value && "text-muted-foreground"
 | 
			
		||||
								)}
 | 
			
		||||
							>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,53 +17,49 @@ import { toast } from "@/components/ui/use-toast"
 | 
			
		|||
 | 
			
		||||
import { randomPublicationTitle } from "app/lib/shortStoryTitleGenerator"
 | 
			
		||||
import { ComponentProps } from "react"
 | 
			
		||||
import { Genre, Pub } from "@prisma/client"
 | 
			
		||||
import { Genre } 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),
 | 
			
		||||
export const formSchema = z.object({
 | 
			
		||||
	title: z.string().min(2).max(50),
 | 
			
		||||
	link: z.string(),
 | 
			
		||||
	query_after_days: z.coerce.number().min(30),
 | 
			
		||||
	genres: z.array(z.number()),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default function PubForm({ genres, createPub, className }: ComponentProps<"div"> & { genres: Array<Genre>, createPub: (data: any) => void }) {
 | 
			
		||||
	const form = useForm<z.infer<typeof formSchema>>({
 | 
			
		||||
		resolver: zodResolver(formSchema),
 | 
			
		||||
		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) ?? []
 | 
			
		||||
			title: "",
 | 
			
		||||
			link: "",
 | 
			
		||||
			query_after_days: 30,
 | 
			
		||||
			genres: []
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
	function onSubmit(values: z.infer<typeof formSchema>) {
 | 
			
		||||
		// Do something with the form values.
 | 
			
		||||
		// ✅ This will be type-safe and validated.
 | 
			
		||||
		toast({
 | 
			
		||||
				title: "Oh dear... ",
 | 
			
		||||
				description: error.message
 | 
			
		||||
			title: "You submitted the following values:",
 | 
			
		||||
			description: (
 | 
			
		||||
				<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
 | 
			
		||||
					<code className="text-white">{JSON.stringify(values, null, 2)}</code>
 | 
			
		||||
				</pre>
 | 
			
		||||
			),
 | 
			
		||||
		})
 | 
			
		||||
		}
 | 
			
		||||
		createPub(values)
 | 
			
		||||
		console.log(values)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onErrors(errors) {
 | 
			
		||||
		toast({
 | 
			
		||||
			title: "You have errors",
 | 
			
		||||
			description: (
 | 
			
		||||
				<Ban />
 | 
			
		||||
				<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))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,13 +15,10 @@ import {
 | 
			
		|||
import { Input } from "@/components/ui/input"
 | 
			
		||||
import { toast } from "@/components/ui/use-toast"
 | 
			
		||||
 | 
			
		||||
import { ComponentProps, SetStateAction } from "react"
 | 
			
		||||
import { ComponentProps } 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(),
 | 
			
		||||
| 
						 | 
				
			
			@ -30,47 +27,42 @@ export const formSchema = z.object({
 | 
			
		|||
	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 }) {
 | 
			
		||||
export default function StoryForm({ genres, createStory, className, existingData }: ComponentProps<"div"> & { genres: Array<Genre>, createStory: (data: any) => void, existingData: Story & { genres: number[] } | null }) {
 | 
			
		||||
	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) ?? []
 | 
			
		||||
			id: existingData?.id,
 | 
			
		||||
			title: existingData?.title ?? "",
 | 
			
		||||
			word_count: existingData?.word_count ?? 500,
 | 
			
		||||
			genres: existingData?.genres ?? []
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
 | 
			
		||||
	function onSubmit(values: z.infer<typeof formSchema>) {
 | 
			
		||||
		toast({
 | 
			
		||||
				title: "Oh dear... ",
 | 
			
		||||
				description: error.message
 | 
			
		||||
			title: "You submitted the following values:",
 | 
			
		||||
			description: (
 | 
			
		||||
				<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
 | 
			
		||||
					<code className="text-white">{JSON.stringify(values, null, 2)}</code>
 | 
			
		||||
				</pre>
 | 
			
		||||
			),
 | 
			
		||||
		})
 | 
			
		||||
		}
 | 
			
		||||
		createStory(values)
 | 
			
		||||
		console.log(values)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	function onErrors(errors) {
 | 
			
		||||
		toast({
 | 
			
		||||
			description: (<Ban />)
 | 
			
		||||
			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))
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +85,7 @@ export default function StoryForm({ genres, dbAction, className, closeDialog, de
 | 
			
		|||
						)}
 | 
			
		||||
					/>
 | 
			
		||||
 | 
			
		||||
					<div className="inline-flex flex-wrap justify-around items-start w-full gap-x-16 gap-y-8 items-baseline max-w-full">
 | 
			
		||||
					<div className="inline-flex flex-wrap w-full gap-x-16 gap-y-8 items-baseline max-w-full">
 | 
			
		||||
 | 
			
		||||
						<GenrePicker
 | 
			
		||||
							genres={genres}
 | 
			
		||||
| 
						 | 
				
			
			@ -107,7 +99,7 @@ export default function StoryForm({ genres, dbAction, className, closeDialog, de
 | 
			
		|||
								<FormItem className="flex flex-col">
 | 
			
		||||
									<FormLabel className="h-5">Word count</FormLabel>
 | 
			
		||||
									<FormControl>
 | 
			
		||||
										<Input className=" w-24" type="number" step={500} {...field}></Input>
 | 
			
		||||
										<Input className=" w-24" type="number" step={500} min={1} {...field}></Input>
 | 
			
		||||
									</FormControl>
 | 
			
		||||
									<FormMessage />
 | 
			
		||||
								</FormItem>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,18 +32,54 @@ import {
 | 
			
		|||
	SelectValue,
 | 
			
		||||
} from "@/components/ui/select"
 | 
			
		||||
import { useState } from "react"
 | 
			
		||||
import { editSubmission } from "app/lib/edit"
 | 
			
		||||
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 const formSchema = 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"
 | 
			
		||||
		}
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
export type SubForm = z.infer<typeof formSchema>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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),
 | 
			
		||||
export default function SubmissionForm({ stories, pubs, responses, defaults }) {
 | 
			
		||||
	const form = useForm<z.infer<typeof formSchema>>({
 | 
			
		||||
		resolver: zodResolver(formSchema),
 | 
			
		||||
		defaultValues: {
 | 
			
		||||
			responseId: responses[0].id,
 | 
			
		||||
			...defaults
 | 
			
		||||
| 
						 | 
				
			
			@ -51,45 +87,51 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
	})
 | 
			
		||||
	const [isSubCalendarOpen, setIsSubCalendarOpen] = useState(false);
 | 
			
		||||
	const [isRespCalendarOpen, setIsRespCalendarOpen] = useState(false);
 | 
			
		||||
	const storiesSelectItems = stories.map((e: Story) => (
 | 
			
		||||
		<SelectItem value={e.id?.toString()} key={e.title}>
 | 
			
		||||
	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}>
 | 
			
		||||
		<SelectItem value={e.id}>
 | 
			
		||||
			{e.title}
 | 
			
		||||
		</SelectItem>
 | 
			
		||||
	))
 | 
			
		||||
 | 
			
		||||
	const reponsesSelectItems = responses.map(e => (
 | 
			
		||||
		<SelectItem value={e.id} key={e.title}>
 | 
			
		||||
		<SelectItem value={e.id}>
 | 
			
		||||
			{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) {
 | 
			
		||||
	// 2. Define a submit handler.
 | 
			
		||||
	function onSubmit(values: z.infer<typeof formSchema>) {
 | 
			
		||||
		// Do something with the form values.
 | 
			
		||||
		// ✅ This will be type-safe and validated.
 | 
			
		||||
		toast({
 | 
			
		||||
				title: "UH-OH",
 | 
			
		||||
				description: error.message
 | 
			
		||||
			title: "You submitted the following values:",
 | 
			
		||||
			description: (
 | 
			
		||||
				<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
 | 
			
		||||
					<code className="text-white">{JSON.stringify(values, null, 2)}</code>
 | 
			
		||||
				</pre>
 | 
			
		||||
			),
 | 
			
		||||
		})
 | 
			
		||||
		if (values.id) {
 | 
			
		||||
			editSubmission(values)
 | 
			
		||||
		} else {
 | 
			
		||||
			createSub(values)
 | 
			
		||||
		}
 | 
			
		||||
		console.log(values)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function onErrors(errors) {
 | 
			
		||||
		toast({
 | 
			
		||||
			title: "You have errors",
 | 
			
		||||
			description: (
 | 
			
		||||
				<Ban />
 | 
			
		||||
				<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))
 | 
			
		||||
| 
						 | 
				
			
			@ -97,14 +139,14 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
 | 
			
		||||
	return (
 | 
			
		||||
		<Form {...form}>
 | 
			
		||||
			<form id="subform" onSubmit={form.handleSubmit(onSubmit, onErrors)} className="space-y-2 md:space-y-8 text-xs">
 | 
			
		||||
			<form id="subform" onSubmit={form.handleSubmit(onSubmit, onErrors)} className="space-y-8">
 | 
			
		||||
				<FormField
 | 
			
		||||
					control={form.control}
 | 
			
		||||
					name="storyId"
 | 
			
		||||
					render={({ field }) => (
 | 
			
		||||
						<FormItem>
 | 
			
		||||
							<FormLabel>Story</FormLabel>
 | 
			
		||||
							<Select onValueChange={field.onChange} defaultValue={field.value?.toString()}>
 | 
			
		||||
							<Select onValueChange={field.onChange} defaultValue={field.value}>
 | 
			
		||||
								<FormControl>
 | 
			
		||||
									<SelectTrigger>
 | 
			
		||||
										<SelectValue placeholder="Select something">
 | 
			
		||||
| 
						 | 
				
			
			@ -116,7 +158,7 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
									{storiesSelectItems}
 | 
			
		||||
								</SelectContent>
 | 
			
		||||
							</Select>
 | 
			
		||||
							<FormDescription className="text-xs md:text-base">The piece you submitted</FormDescription>
 | 
			
		||||
							<FormDescription>The piece you submitted</FormDescription>
 | 
			
		||||
							<FormMessage />
 | 
			
		||||
						</FormItem>
 | 
			
		||||
					)}
 | 
			
		||||
| 
						 | 
				
			
			@ -126,8 +168,8 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
					name="pubId"
 | 
			
		||||
					render={({ field }) => (
 | 
			
		||||
						<FormItem>
 | 
			
		||||
							<FormLabel className="text-sm md:text-base">Publication</FormLabel>
 | 
			
		||||
							<Select onValueChange={field.onChange} defaultValue={field.value?.toString()}>
 | 
			
		||||
							<FormLabel>Publication</FormLabel>
 | 
			
		||||
							<Select onValueChange={field.onChange} defaultValue={field.value}>
 | 
			
		||||
								<FormControl>
 | 
			
		||||
									<SelectTrigger>
 | 
			
		||||
										<SelectValue placeholder="Select something">
 | 
			
		||||
| 
						 | 
				
			
			@ -139,7 +181,7 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
									{pubsSelectItems}
 | 
			
		||||
								</SelectContent>
 | 
			
		||||
							</Select>
 | 
			
		||||
							<FormDescription className="text-xs md:text-base">The market you sent it to</FormDescription>
 | 
			
		||||
							<FormDescription>The market you sent it to</FormDescription>
 | 
			
		||||
							<FormMessage />
 | 
			
		||||
						</FormItem>
 | 
			
		||||
					)}
 | 
			
		||||
| 
						 | 
				
			
			@ -150,7 +192,7 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
					name="submitted"
 | 
			
		||||
					render={({ field }) => (
 | 
			
		||||
						<FormItem className="flex flex-col">
 | 
			
		||||
							<FormLabel className="text-sm md:text-base">Date of submission</FormLabel>
 | 
			
		||||
							<FormLabel>Date of submission</FormLabel>
 | 
			
		||||
							<Popover modal={true} open={isSubCalendarOpen} onOpenChange={setIsSubCalendarOpen}>
 | 
			
		||||
								<PopoverTrigger asChild>
 | 
			
		||||
									<FormControl>
 | 
			
		||||
| 
						 | 
				
			
			@ -171,8 +213,9 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
									</FormControl>
 | 
			
		||||
								</PopoverTrigger>
 | 
			
		||||
								<PopoverContent className="w-auto p-0" align="start">
 | 
			
		||||
									{/* @ts-ignore */}
 | 
			
		||||
									<Calendar mode="single" selected={field.value}
 | 
			
		||||
									<Calendar
 | 
			
		||||
										mode="single"
 | 
			
		||||
										selected={field.value}
 | 
			
		||||
										onSelect={(e) => { field.onChange(e); setIsSubCalendarOpen(false); }}
 | 
			
		||||
										disabled={(date) =>
 | 
			
		||||
											date > new Date() || date < new Date("1900-01-01")
 | 
			
		||||
| 
						 | 
				
			
			@ -181,7 +224,7 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
									/>
 | 
			
		||||
								</PopoverContent>
 | 
			
		||||
							</Popover>
 | 
			
		||||
							<FormDescription className="text-xs md:text-base">
 | 
			
		||||
							<FormDescription>
 | 
			
		||||
								The date you sent it
 | 
			
		||||
							</FormDescription>
 | 
			
		||||
							<FormMessage />
 | 
			
		||||
| 
						 | 
				
			
			@ -194,7 +237,7 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
					name="responded"
 | 
			
		||||
					render={({ field }) => (
 | 
			
		||||
						<FormItem className="flex flex-col">
 | 
			
		||||
							<FormLabel className="text-sm md:text-base">Date of response</FormLabel>
 | 
			
		||||
							<FormLabel>Date of response</FormLabel>
 | 
			
		||||
							<Popover modal={true} open={isRespCalendarOpen} onOpenChange={setIsRespCalendarOpen}>
 | 
			
		||||
								<PopoverTrigger asChild>
 | 
			
		||||
									<FormControl>
 | 
			
		||||
| 
						 | 
				
			
			@ -215,8 +258,9 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
									</FormControl>
 | 
			
		||||
								</PopoverTrigger>
 | 
			
		||||
								<PopoverContent className="w-auto p-0" align="start">
 | 
			
		||||
									{/* @ts-ignore */}
 | 
			
		||||
									<Calendar mode="single" selected={field.value}
 | 
			
		||||
									<Calendar
 | 
			
		||||
										mode="single"
 | 
			
		||||
										selected={field.value}
 | 
			
		||||
										onSelect={(e) => { field.onChange(e); setIsRespCalendarOpen(false); }}
 | 
			
		||||
										disabled={(date) =>
 | 
			
		||||
											date > new Date() || date < new Date("1900-01-01")
 | 
			
		||||
| 
						 | 
				
			
			@ -225,7 +269,7 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
									/>
 | 
			
		||||
								</PopoverContent>
 | 
			
		||||
							</Popover>
 | 
			
		||||
							<FormDescription className="text-xs md:text-base">
 | 
			
		||||
							<FormDescription>
 | 
			
		||||
								The date they wrote back
 | 
			
		||||
							</FormDescription>
 | 
			
		||||
							<FormMessage />
 | 
			
		||||
| 
						 | 
				
			
			@ -239,8 +283,8 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
					name="responseId"
 | 
			
		||||
					render={({ field }) => (
 | 
			
		||||
						<FormItem>
 | 
			
		||||
							<FormLabel className="text-sm md:text-base">Response</FormLabel>
 | 
			
		||||
							<Select onValueChange={field.onChange} defaultValue={field.value?.toString()}>
 | 
			
		||||
							<FormLabel>Response</FormLabel>
 | 
			
		||||
							<Select onValueChange={field.onChange} defaultValue={field.value}>
 | 
			
		||||
								<FormControl>
 | 
			
		||||
									<SelectTrigger>
 | 
			
		||||
										<SelectValue>
 | 
			
		||||
| 
						 | 
				
			
			@ -252,7 +296,7 @@ export default function SubmissionForm({ stories, pubs, responses, defaults, clo
 | 
			
		|||
									{reponsesSelectItems}
 | 
			
		||||
								</SelectContent>
 | 
			
		||||
							</Select>
 | 
			
		||||
							<FormDescription className="text-xs md:text-base">The market you sent it to</FormDescription>
 | 
			
		||||
							<FormDescription>The market you sent it to</FormDescription>
 | 
			
		||||
							<FormMessage />
 | 
			
		||||
						</FormItem>
 | 
			
		||||
					)}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ 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>))}
 | 
			
		||||
      {props.genres.map((e: Genre) => (<Badge key={e.name}>{e.name}</Badge>))}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,6 @@ 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 }) {
 | 
			
		||||
| 
						 | 
				
			
			@ -15,24 +14,21 @@ function NavLink(props: ComponentProps<"div"> & { href: string }) {
 | 
			
		|||
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 /> },
 | 
			
		||||
    { link: "/story", label: "STORIES" },
 | 
			
		||||
    { link: "/publication", label: "PUBLICATIONS" },
 | 
			
		||||
    { link: "/submission", label: "SUBMISSIONS" },
 | 
			
		||||
  ]
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={props.className}>
 | 
			
		||||
      <div className="text-secondary-foreground flex flex-row md:flex-col" >
 | 
			
		||||
      <div className="text-secondary-foreground" >
 | 
			
		||||
        {
 | 
			
		||||
          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 ",
 | 
			
		||||
            className={twMerge(clsx("text-xl drop-shadow  font-black my-2 w-full p-2 pl-6 antialiased text-secondary-foreground bg-secondary 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 >))
 | 
			
		||||
          ><p className="drop-shadow-sm">{e.label}</p></NavLink >))
 | 
			
		||||
        }
 | 
			
		||||
      </ div>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,14 @@
 | 
			
		|||
import { Dialog, DialogTrigger, DialogClose, DialogDescription, DialogContent, DialogTitle, DialogHeader, DialogFooter } from "@/components/ui/dialog"
 | 
			
		||||
import { Button } from "@/components/ui/button"
 | 
			
		||||
import { ContextMenuContent, ContextMenuItem, ContextMenuSubTrigger, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent } from "@/components/ui/context-menu"
 | 
			
		||||
import { deleteRecord } from "app/lib/del"
 | 
			
		||||
import Link from "next/link"
 | 
			
		||||
import { ComponentProps, useState } from "react"
 | 
			
		||||
import { Row, Table, TableState } from "@tanstack/react-table"
 | 
			
		||||
import { tableNameToItemName } from "app/lib/nameMaps"
 | 
			
		||||
import EditSubmissionDialog from "app/submission/edit"
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,9 +24,15 @@ export default function FormContextMenu({ table, row, openEditDialog, openDelete
 | 
			
		|||
        : ""
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        pathname === "/submission" ?
 | 
			
		||||
          <>
 | 
			
		||||
            <ContextMenuItem onClick={() => openEditDialog(row)}>
 | 
			
		||||
              Edit
 | 
			
		||||
            </ContextMenuItem>
 | 
			
		||||
          </>
 | 
			
		||||
          : ""
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        selectedRows.length > 0 ?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,21 +39,17 @@ import {
 | 
			
		|||
  TableRow,
 | 
			
		||||
} from "@/components/ui/table"
 | 
			
		||||
import { EyeIcon, Trash2 } from "lucide-react"
 | 
			
		||||
import { usePathname, useSearchParams } from "next/navigation"
 | 
			
		||||
import { usePathname } 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 { updateField } 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>[]
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +78,7 @@ export function DataTable<TData, TValue>({
 | 
			
		|||
  const [columnVisibility, setColumnVisibility] =
 | 
			
		||||
    useState<VisibilityState>({})
 | 
			
		||||
  //
 | 
			
		||||
  const pathname: string = usePathname()
 | 
			
		||||
  const pathname: Pathname = usePathname()
 | 
			
		||||
  const table = useReactTable({
 | 
			
		||||
    data,
 | 
			
		||||
    columns,
 | 
			
		||||
| 
						 | 
				
			
			@ -121,33 +117,31 @@ export function DataTable<TData, TValue>({
 | 
			
		|||
    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 justify-between items-center py-4">
 | 
			
		||||
      <div className="flex gap-2">
 | 
			
		||||
        <DropdownMenu>
 | 
			
		||||
          <DropdownMenuTrigger asChild>
 | 
			
		||||
            <Button variant="outline" className="hidden sm:block ml-auto">
 | 
			
		||||
            <Button variant="outline" className="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>) })}
 | 
			
		||||
                .map((column) => {
 | 
			
		||||
                  return (
 | 
			
		||||
                    <DropdownMenuRadioItem value={column} className="capitalize" key={column.id}>
 | 
			
		||||
                      {column.id}
 | 
			
		||||
                    </DropdownMenuRadioItem>
 | 
			
		||||
                  )
 | 
			
		||||
                })}
 | 
			
		||||
            </DropdownMenuRadioGroup>
 | 
			
		||||
          </DropdownMenuContent>
 | 
			
		||||
        </DropdownMenu>
 | 
			
		||||
| 
						 | 
				
			
			@ -165,30 +159,12 @@ export function DataTable<TData, TValue>({
 | 
			
		|||
 | 
			
		||||
      <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>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -205,12 +181,8 @@ export function DataTable<TData, TValue>({
 | 
			
		|||
          <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()
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  deleteRecord(dialogRow.original.id, pathname)
 | 
			
		||||
                }}>Yes, delete it!
 | 
			
		||||
              </Button>
 | 
			
		||||
            </DialogClose>
 | 
			
		||||
| 
						 | 
				
			
			@ -226,29 +198,23 @@ export function DataTable<TData, TValue>({
 | 
			
		|||
        </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>
 | 
			
		||||
            <DialogClose asChild>
 | 
			
		||||
              <Button variant="destructive"
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  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()
 | 
			
		||||
                  console.table(recordIds)
 | 
			
		||||
                  deleteRecords(recordIds, pathname)
 | 
			
		||||
                }}>
 | 
			
		||||
                Yes, delete them!</Button>
 | 
			
		||||
            </DialogClose>
 | 
			
		||||
          </DialogFooter>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
| 
						 | 
				
			
			@ -284,8 +250,7 @@ export function DataTable<TData, TValue>({
 | 
			
		|||
        </DropdownMenuContent>
 | 
			
		||||
      </DropdownMenu>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div className="rounded-md">
 | 
			
		||||
 | 
			
		||||
    <div className="rounded-md border">
 | 
			
		||||
 | 
			
		||||
      <Table>
 | 
			
		||||
        <TableHeader>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import { FormField, FormItem, FormLabel, FormMessage, FormControl, Form } from "
 | 
			
		|||
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 { ComponentProps, useState } from "react"
 | 
			
		||||
import { EventType, useForm, UseFormReturn } from "react-hook-form"
 | 
			
		||||
import { CellContext } from "@tanstack/react-table"
 | 
			
		||||
import { z } from "zod"
 | 
			
		||||
| 
						 | 
				
			
			@ -12,38 +12,36 @@ 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) {
 | 
			
		||||
 | 
			
		||||
  async function onSubmit({ genres }: { genres: number[] }) {
 | 
			
		||||
    event.preventDefault()
 | 
			
		||||
    try {
 | 
			
		||||
    const genresArray = genres.map((e) => { return { id: e } })
 | 
			
		||||
    console.log(`genres: ${genres}, genresArray: ${JSON.stringify(genresArray)}`)
 | 
			
		||||
    toast({
 | 
			
		||||
      title: "You submitted the following values:",
 | 
			
		||||
      description: (
 | 
			
		||||
        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
 | 
			
		||||
          <code className="text-white">{JSON.stringify(genres)}</code>
 | 
			
		||||
        </pre>
 | 
			
		||||
      ),
 | 
			
		||||
    })
 | 
			
		||||
    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)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -79,9 +77,9 @@ export default function GenrePickerInputCell(props: CellContext<any, any>) {
 | 
			
		|||
            control={form.control}
 | 
			
		||||
            name="genres"
 | 
			
		||||
            render={({ field }) => (
 | 
			
		||||
              <FormItem className="w-full max-w-32 flex flex-col">
 | 
			
		||||
              <FormItem className="w-full max-w-xs 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>
 | 
			
		||||
                  {value.length > 0 ? <Button variant="ghost" className="h-fit"><GenreBadges genres={value} className="w-full" /></Button> : <Button variant="outline" type="button" className="w-fit m-auto">Add genres</Button>
 | 
			
		||||
                  }
 | 
			
		||||
                </PopoverTrigger>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -103,6 +101,7 @@ export default function GenrePickerInputCell(props: CellContext<any, any>) {
 | 
			
		|||
                              <Checkbox
 | 
			
		||||
                                checked={field.value?.includes(item.id)}
 | 
			
		||||
                                onCheckedChange={(checked) => {
 | 
			
		||||
                                  console.log(field.value)
 | 
			
		||||
                                  return checked
 | 
			
		||||
                                    ? field.onChange(
 | 
			
		||||
                                      [...field.value, item.id]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,56 +9,58 @@ 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 }) {
 | 
			
		||||
export default function NumberInputCell(props: 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 table = props.table.options.meta.tableName
 | 
			
		||||
  const id = props.row.original.id
 | 
			
		||||
  const column = props.column.id
 | 
			
		||||
  const pathname = props.table.options.meta.pathname
 | 
			
		||||
  const value = props.cell.getValue()
 | 
			
		||||
  const formSchema = props.column.columnDef.meta.formSchema.pick({ [column]: true })
 | 
			
		||||
 | 
			
		||||
  const form = useForm<z.infer<typeof formSchema>>({
 | 
			
		||||
    resolver: zodResolver(formSchema),
 | 
			
		||||
    defaultValues: {
 | 
			
		||||
      [column]: cellContext.cell.getValue()
 | 
			
		||||
      [column]: props.cell.getValue()
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  async function onSubmit(value: z.infer<typeof formSchema>) {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await updateField({
 | 
			
		||||
  function onSubmit(value: z.infer<typeof formSchema>) {
 | 
			
		||||
    toast({
 | 
			
		||||
      title: "You submitted the following values:",
 | 
			
		||||
      description: (
 | 
			
		||||
        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
 | 
			
		||||
          <code className="text-white">{JSON.stringify(value, null, 2)}</code>
 | 
			
		||||
        </pre>
 | 
			
		||||
      ),
 | 
			
		||||
    })
 | 
			
		||||
    updateField({
 | 
			
		||||
      id,
 | 
			
		||||
      table,
 | 
			
		||||
        datum: value[column],
 | 
			
		||||
      number: 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) {
 | 
			
		||||
  function onErrors(errors) {
 | 
			
		||||
    toast({
 | 
			
		||||
      title: "You have errors",
 | 
			
		||||
      description: errors.message,
 | 
			
		||||
      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(true)}
 | 
			
		||||
      className={className + " w-full h-fit flex items-center justify-center"}
 | 
			
		||||
      onDoubleClick={() => setIsActive(prev => !prev)}
 | 
			
		||||
      className="w-full h-fit flex items-center justify-center"
 | 
			
		||||
      tabIndex={0}
 | 
			
		||||
      onKeyDown={e => {
 | 
			
		||||
        if (e.code === "Enter" && !isActive) {
 | 
			
		||||
| 
						 | 
				
			
			@ -80,8 +82,13 @@ export default function NumberInputCell({ cellContext, className }: { cellContex
 | 
			
		|||
                >
 | 
			
		||||
                  <FormControl
 | 
			
		||||
                  >
 | 
			
		||||
                    {/* @ts-ignore */}
 | 
			
		||||
                    <Input className="md:w-24" type="number" autoFocus={true} step={cellContext.column.columnDef.meta?.step} {...field} />
 | 
			
		||||
                    <Input
 | 
			
		||||
                      className="w-24"
 | 
			
		||||
                      type="number"
 | 
			
		||||
                      autoFocus={true}
 | 
			
		||||
                      step={props.column.columnDef.meta?.step}
 | 
			
		||||
                      {...field}
 | 
			
		||||
                    />
 | 
			
		||||
                  </FormControl>
 | 
			
		||||
                  <FormMessage />
 | 
			
		||||
                </FormItem>
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +97,7 @@ export default function NumberInputCell({ cellContext, className }: { cellContex
 | 
			
		|||
 | 
			
		||||
          </form>
 | 
			
		||||
        </Form>
 | 
			
		||||
        : <p>{cellContext.cell.getValue()}</p>
 | 
			
		||||
        : <p>{props.cell.getValue()}</p>
 | 
			
		||||
      }
 | 
			
		||||
    </div >
 | 
			
		||||
  )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,29 +9,33 @@ 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> }) {
 | 
			
		||||
export function TextInputCell(props: 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 table = props.table.options.meta.tableName
 | 
			
		||||
  const id = props.row.original.id
 | 
			
		||||
  const column = props.column.id
 | 
			
		||||
  const pathname = props.table.options.meta.pathname
 | 
			
		||||
  const value = props.cell.getValue()
 | 
			
		||||
  const formSchema = props.column.columnDef.meta.formSchema.pick({ [column]: true })
 | 
			
		||||
 | 
			
		||||
  const form = useForm<z.infer<typeof formSchema>>({
 | 
			
		||||
    resolver: zodResolver(formSchema),
 | 
			
		||||
    defaultValues: {
 | 
			
		||||
      [column]: cellContext.cell.getValue()
 | 
			
		||||
      [column]: props.cell.getValue()
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  async function onSubmit(value: z.infer<typeof formSchema>) {
 | 
			
		||||
    try {
 | 
			
		||||
    toast({
 | 
			
		||||
      title: "You submitted the following values:",
 | 
			
		||||
      description: (
 | 
			
		||||
        <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
 | 
			
		||||
          <code className="text-white">{JSON.stringify(value, null, 2)}</code>
 | 
			
		||||
        </pre>
 | 
			
		||||
      ),
 | 
			
		||||
    })
 | 
			
		||||
    const res = await updateField({
 | 
			
		||||
      id,
 | 
			
		||||
      table,
 | 
			
		||||
| 
						 | 
				
			
			@ -39,13 +43,6 @@ export function TextInputCell({ cellContext, className }: { className: string, c
 | 
			
		|||
      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)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +60,7 @@ export function TextInputCell({ cellContext, className }: { className: string, c
 | 
			
		|||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      onDoubleClick={() => setIsActive(prev => !prev)}
 | 
			
		||||
      className={className + " w-full h-fit flex items-center justify-left"}
 | 
			
		||||
      className="w-full h-fit flex items-center justify-left"
 | 
			
		||||
      tabIndex={0}
 | 
			
		||||
      onKeyDown={e => {
 | 
			
		||||
        if (e.code === "Enter" && !isActive) {
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +96,7 @@ export function TextInputCell({ cellContext, className }: { className: string, c
 | 
			
		|||
 | 
			
		||||
          </form>
 | 
			
		||||
        </Form>
 | 
			
		||||
        : <TitleContainer>{cellContext.cell.getValue()}</TitleContainer>
 | 
			
		||||
        : <TitleContainer>{props.cell.getValue()}</TitleContainer>
 | 
			
		||||
      }
 | 
			
		||||
    </div >
 | 
			
		||||
  )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,9 +5,8 @@ export const selectCol = {
 | 
			
		|||
  id: "select",
 | 
			
		||||
  header: (props: HeaderContext<any, any>) => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex items-start justify-left mx-auto">
 | 
			
		||||
      <div className="flex items-center justify-center">
 | 
			
		||||
        <Checkbox
 | 
			
		||||
          className="mr-4 ml-2"
 | 
			
		||||
          checked={props.table.getIsAllRowsSelected()}
 | 
			
		||||
          onCheckedChange={props.table.toggleAllRowsSelected}
 | 
			
		||||
          aria-label="select/deselect all rows"
 | 
			
		||||
| 
						 | 
				
			
			@ -19,9 +18,8 @@ export const selectCol = {
 | 
			
		|||
 | 
			
		||||
  cell: (props: CellContext<any, any>) => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex items-start justify-left">
 | 
			
		||||
      <div className="flex items-center justify-center">
 | 
			
		||||
        <Checkbox
 | 
			
		||||
          className="mr-4 ml-2"
 | 
			
		||||
          checked={props.row.getIsSelected()}
 | 
			
		||||
          onCheckedChange={props.row.toggleSelected}
 | 
			
		||||
          aria-label="select/deselect row"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,11 @@
 | 
			
		|||
import { ComponentProps } from "react";
 | 
			
		||||
 | 
			
		||||
export default function TitleContainer({ children }: ComponentProps<"div">) {
 | 
			
		||||
  let classes = "w-full text-left m-auto h-fit flex align-center text-xs md:text-sm"
 | 
			
		||||
export default function itleContainer({ children }: ComponentProps<"div">) {
 | 
			
		||||
  let classes = "w-full text-left m-auto"
 | 
			
		||||
  console.table(children)
 | 
			
		||||
  if (children == "RECORD DELETED") {
 | 
			
		||||
    console.log("BINGO")
 | 
			
		||||
    classes = classes + " text-destructive font-bold"
 | 
			
		||||
  }
 | 
			
		||||
  return <p className={classes}>{children}</p>
 | 
			
		||||
  return <span className="h-10 flex align-center"><p className={classes}>{children}</p></span>
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue