tweak refresh/notifications

This commit is contained in:
andrzej 2024-09-25 12:23:16 +02:00
parent 3998180830
commit 7e6a60388b
11 changed files with 134 additions and 65 deletions

Binary file not shown.

View File

@ -33,7 +33,7 @@ export default function RootLayout({
>
<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 w-full justify-between"> <header className="">
<div id="sidebar" className=" flex flex-row md:flex-col justify-between"> <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 hidden md:block">SubMan</h1>
<p className="mt-2 mx-1 text-sm antialiased w-40 hidden md:block">The self-hosted literary submission tracker.</p>
</header>

View File

@ -7,26 +7,16 @@ import { z } from "zod"
import { storySchema } from "app/ui/forms/schemas"
import { pubSchema } from "app/ui/forms/schemas"
import { subSchema } from "app/ui/forms/schemas"
import { prepGenreData, prepStoryData } from "./validate"
//TODO - data validation, error handling, unauthorized access handling
export async function createStory(data: Story & { genres: number[] }): Promise<Story | undefined> {
export async function createStory(data: Story & { genres: number[] }): Promise<Story | boolean | undefined> {
// will return undefined if middleware authorization fails
"use server"
//Prepare data
const genresArray = data.genres.map((e) => { return { id: e } })
const storyData = {
title: data.title,
word_count: data.word_count,
}
//prepare schemas
const schema = storySchema.omit({ id: true, genres: true })
const genreSchema = z.object({ id: z.number() })
const genresSchema = z.array(genreSchema)
try {
//validate
schema.safeParse(storyData)
genresSchema.safeParse(genresArray)
const storyData = await prepStoryData(data)
const genresArray = await prepGenreData(data.genres)
//submit
const res = await prisma.story.create({ data: storyData })
@ -40,12 +30,13 @@ export async function createStory(data: Story & { genres: number[] }): Promise<S
return res
} catch (error) {
console.error(error)
return undefined
return false
}
}
export async function createPub(data: Pub & { genres: number[] }): Promise<Pub | undefined> {
export async function createPub(data: Pub & { genres: number[] }): Promise<Pub | boolean | undefined> {
"use server"
//prepare data
const pubData = {
@ -79,7 +70,7 @@ export async function createPub(data: Pub & { genres: number[] }): Promise<Pub |
return genresRes
} catch (error) {
console.error(error)
return undefined
return false
}
}

View File

@ -10,24 +10,33 @@ const tableMap = {
"/submission": "sub"
}
export async function deleteRecord(id: number, pathname: Pathname) {
export async function deleteRecord(id: number, pathname: Pathname): Promise<undefined | boolean> {
const table = tableMap[pathname]
const res = await prisma[table].delete({ where: { id } })
console.log(`deleted from ${table}: ${res.id}`)
console.log("revalidating: " + pathname)
revalidatePath(pathname)
redirect(pathname)
}
export async function deleteRecords(ids: number[], pathname: "/story" | "/publication" | "/submission") {
const table = tableMap[pathname]
ids.forEach(async (id) => {
const res = await prisma[table].delete({
where: { id }
})
try {
const res = await prisma[table].delete({ where: { id } })
console.log(`deleted from ${table}: ${res.id}`)
})
revalidatePath(pathname)
redirect(pathname)
revalidatePath(pathname)
return true
} catch (error) {
console.error(error)
return undefined
}
}
export async function deleteRecords(ids: number[], pathname: "/story" | "/publication" | "/submission"): Promise<boolean | undefined> {
try {
const table = tableMap[pathname]
ids.forEach(async (id) => {
const res = await prisma[table].delete({
where: { id }
})
console.log(`deleted from ${table}: ${res.id}`)
})
revalidatePath(pathname)
return true
} catch (error) {
console.error(error)
return undefined
}
}

View File

@ -1,9 +1,10 @@
"use server"
import { Genre, Sub } from "@prisma/client"
import { Genre, Story, Sub } from "@prisma/client"
import prisma from "./db"
import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
import { subSchema } from "app/ui/forms/schemas"
import { storySchema, subSchema } from "app/ui/forms/schemas"
import { z } from "zod"
export async function updateField({ datum, table, column, id, pathname }: { datum?: string | number | Genre[], table: string, column: string, id: number, pathname: string }) {
@ -56,7 +57,28 @@ export async function updateSub(data: Sub): Promise<Sub | undefined> {
}
export async function updateStory(data: Story & { genres: number[] }): Promise<Story | undefined> {
"use server"
//prepare data
const genresArray = data.genres.map((e) => { return { id: e } })
const storyData = {
title: data.title,
word_count: data.word_count,
}
//prepare schemas
const schema = storySchema.omit({ id: true, genres: true })
const genreSchema = z.object({ id: z.number() })
const genresSchema = z.array(genreSchema)
try {
//validate
schema.safeParse(storyData)
genresSchema.safeParse(genresArray)
} catch (error) {
console.error(error)
return undefined
}
}

36
src/app/lib/validate.ts Normal file
View File

@ -0,0 +1,36 @@
import { z } from "zod";
import { storySchema } from "app/ui/forms/schemas";
import { Story } from "@prisma/client";
//schemas
const schema = storySchema.omit({ id: true, genres: true })
const genreSchema = z.object({ id: z.number() })
const genresSchema = z.array(genreSchema)
export async function prepStoryData(data: Story & { genres: number[] }): Promise<{ title: string, word_count: number }> {
const storyData = {
title: data.title,
word_count: data.word_count,
}
//prepare schemas
//throw an error if validation fails
schema.safeParse(storyData)
return storyData
}
export async function prepGenreData(data: number[]): Promise<{ id: number }[]> {
"use server"
//prepare data
const genresArray = data.map((e) => { return { id: e } })
//prepare schemas
//throws error if validation fails
genresSchema.safeParse(genresArray)
return genresArray
}

View File

@ -25,7 +25,9 @@ export default function CreatePubDialog({ genres }: ComponentProps<"div"> & { ge
</DialogHeader>
<PubForm createPub={createPub} genres={genres} />
<DialogFooter>
<Button form="pubform">Submit</Button>
<DialogClose >
<Button form="pubform">Submit</Button>
</DialogClose>
</DialogFooter>
</DialogContent>

View File

@ -25,7 +25,9 @@ export default function CreateStoryDialog({ genres }: ComponentProps<"div"> & {
</DialogHeader>
<StoryForm createStory={createStory} genres={genres} existingData={null} />
<DialogFooter>
<Button form="storyform">Submit</Button>
<DialogClose>
<Button form="storyform">Submit</Button>
</DialogClose>
</DialogFooter>
</DialogContent>

View File

@ -20,6 +20,7 @@ import { ComponentProps } from "react"
import { Genre } from "@prisma/client"
import GenrePicker from "./genrePicker"
import { pubSchema } from "./schemas"
import { useRouter } from "next/navigation"
export default function PubForm({ genres, createPub, className }: ComponentProps<"div"> & { genres: Array<Genre>, createPub: (data: any) => void }) {
const form = useForm<z.infer<typeof pubSchema>>({
@ -32,21 +33,20 @@ export default function PubForm({ genres, createPub, className }: ComponentProps
},
})
const router = useRouter()
async function onSubmit(values: z.infer<typeof pubSchema>) {
try {
const res = await createPub(values)
if (res === undefined) throw new Error("something went wrong")
toast({
title: "Successfuly submitted:",
description: values.title
})
if (!res) throw new Error("something went wrong")
toast({ title: "Successfully submitted:", description: values.title })
router.refresh()
} catch (error) {
toast({
title: "UH-OH",
title: "Oh dear... ",
description: error.message
})
}
window.location.reload()
}
function onErrors(errors) {

View File

@ -19,6 +19,7 @@ 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"
export const formSchema = z.object({
id: z.number().optional(),
@ -27,35 +28,30 @@ export const formSchema = z.object({
genres: z.array(z.number())
})
export default function StoryForm({ genres, createStory, className, existingData }: ComponentProps<"div"> & { genres: Array<Genre>, createStory: (data: any) => void, existingData: Story & { genres: number[] } | null }) {
export default function StoryForm({ genres, createStory, className }: ComponentProps<"div"> & { genres: Array<Genre>, createStory: (data: any) => void, className: string }) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: existingData?.id,
title: existingData?.title ?? "",
word_count: existingData?.word_count ?? 500,
genres: existingData?.genres ?? []
word_count: 500,
genres: []
},
})
const router = useRouter()
async function onSubmit(values: z.infer<typeof formSchema>) {
try {
const res = await createStory(values)
//server actions return undefined if middleware authentication fails
if (res === undefined) throw new Error("something went wrong")
toast({
title: "Successfully submitted:",
description: values.title,
})
if (!res) throw new Error("something went wrong")
toast({ title: "Sucessfully submitted:", description: values.title })
router.refresh()
} catch (error) {
toast({
title: "UH-OH",
title: "Oh dear... ",
description: error.message
})
}
window.location.reload()
}

View File

@ -39,7 +39,7 @@ import {
TableRow,
} from "@/components/ui/table"
import { EyeIcon, Trash2 } from "lucide-react"
import { usePathname } from "next/navigation"
import { usePathname, useSearchParams } from "next/navigation"
import FormContextMenu from "./contextMenu"
import { deleteRecord, deleteRecords } from "app/lib/del"
import { Pathname } from "app/types"
@ -50,6 +50,8 @@ 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"
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
@ -119,6 +121,9 @@ export function DataTable<TData, TValue>({
}
const router = useRouter()
const [filterBy, setFilterBy] = useState(table.getAllColumns()[0])
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
return (<>
@ -181,8 +186,11 @@ export function DataTable<TData, TValue>({
<DialogFooter>
<DialogClose asChild>
<Button variant="destructive"
onClick={() => {
deleteRecord(dialogRow.original.id, pathname)
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 })
router.refresh()
}}>Yes, delete it!
</Button>
</DialogClose>
@ -206,11 +214,14 @@ export function DataTable<TData, TValue>({
<DialogFooter>
<DialogClose asChild>
<Button variant="destructive"
onClick={() => {
onClick={async () => {
const selectedRows = table.getState().rowSelection
const rowIds = Object.keys(selectedRows)
const recordIds = rowIds.map(id => Number(table.getRow(id).original.id))
deleteRecords(recordIds, pathname)
const res = await deleteRecords(recordIds, pathname)
if (!res) toast({ title: "Oh dear...", description: "Failed to delete." })
if (res) toast({ title: "Sucessfully deleted records of id:", description: JSON.stringify(recordIds) })
router.refresh()
}}>
Yes, delete them!</Button>
</DialogClose>