tweak refresh/notifications

This commit is contained in:
andrzej 2024-09-25 12:23:16 +02:00
parent 3998180830
commit 81b36d0c8c
15 changed files with 153 additions and 76 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 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 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> <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> <p className="mt-2 mx-1 text-sm antialiased w-40 hidden md:block">The self-hosted literary submission tracker.</p>
</header> </header>

View File

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

View File

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

View File

@ -1,9 +1,10 @@
"use server" "use server"
import { Genre, Sub } from "@prisma/client" import { Genre, Story, Sub } from "@prisma/client"
import prisma from "./db" import prisma from "./db"
import { revalidatePath } from "next/cache" import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation" 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 }) { 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> </DialogHeader>
<PubForm createPub={createPub} genres={genres} /> <PubForm createPub={createPub} genres={genres} />
<DialogFooter> <DialogFooter>
<DialogClose >
<Button form="pubform">Submit</Button> <Button form="pubform">Submit</Button>
</DialogClose>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@ -2,7 +2,7 @@
import { createStory } from "app/lib/create" import { createStory } from "app/lib/create"
import { Dialog, DialogHeader, DialogTrigger, DialogContent, DialogClose, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Dialog, DialogHeader, DialogTrigger, DialogContent, DialogClose, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ComponentProps } from "react"; import { ComponentProps, useState } from "react";
import { Genre } from "@prisma/client"; import { Genre } from "@prisma/client";
import StoryForm from "app/ui/forms/story"; import StoryForm from "app/ui/forms/story";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
@ -10,8 +10,10 @@ import { Plus } from "lucide-react";
export default function CreateStoryDialog({ genres }: ComponentProps<"div"> & { genres: Genre[] }) { export default function CreateStoryDialog({ genres }: ComponentProps<"div"> & { genres: Genre[] }) {
const [isOpen, setIsOpen] = useState(false)
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<div> <div>
<Button className="hidden md:block">Create new story</Button> <Button className="hidden md:block">Create new story</Button>
@ -23,9 +25,12 @@ export default function CreateStoryDialog({ genres }: ComponentProps<"div"> & {
<DialogTitle>New story</DialogTitle> <DialogTitle>New story</DialogTitle>
<DialogDescription>Create an entry for a new story i.e. a thing you intend to submit for publication.</DialogDescription> <DialogDescription>Create an entry for a new story i.e. a thing you intend to submit for publication.</DialogDescription>
</DialogHeader> </DialogHeader>
<StoryForm createStory={createStory} genres={genres} existingData={null} /> <StoryForm createStory={createStory} genres={genres} className="" />
<DialogFooter> <DialogFooter>
<DialogClose>
{/* TODO: pass setIsOpen to form as prop, to be handled post-verification */}
<Button form="storyform">Submit</Button> <Button form="storyform">Submit</Button>
</DialogClose>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>

View File

@ -35,6 +35,7 @@ import { useState } from "react"
import { createSub } from "app/lib/create" import { createSub } from "app/lib/create"
import { subSchema } from "./schemas" import { subSchema } from "./schemas"
import { updateSub } from "app/lib/update" import { updateSub } from "app/lib/update"
import { useRouter } from "next/navigation"
export type SubForm = z.infer<typeof subSchema> export type SubForm = z.infer<typeof subSchema>
@ -67,13 +68,14 @@ export default function EditSubmissionForm({ stories, pubs, responses, defaults
)) ))
// 2. Define a submit handler. const router = useRouter()
async function onSubmit(values: z.infer<typeof subSchema>) { async function onSubmit(values: z.infer<typeof subSchema>) {
try { try {
const res = await updateSub(values) const res = await updateSub(values)
if (res === undefined) throw new Error("something went wrong") if (res === undefined) throw new Error("something went wrong")
toast({ title: "Successfully created new submission!" }) toast({ title: "Successfully created new submission!" })
window.location.reload() router.refresh()
} catch (error) { } catch (error) {
toast({ toast({
title: "UH-OH", title: "UH-OH",

View File

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

View File

@ -19,6 +19,7 @@ import { ComponentProps } from "react"
import { Genre, Story } from "@prisma/client" import { Genre, Story } from "@prisma/client"
import { randomStoryTitle } from "app/lib/shortStoryTitleGenerator" import { randomStoryTitle } from "app/lib/shortStoryTitleGenerator"
import GenrePicker from "./genrePicker" import GenrePicker from "./genrePicker"
import { useRouter } from "next/navigation"
export const formSchema = z.object({ export const formSchema = z.object({
id: z.number().optional(), id: z.number().optional(),
@ -27,35 +28,30 @@ export const formSchema = z.object({
genres: z.array(z.number()) 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>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
id: existingData?.id, word_count: 500,
title: existingData?.title ?? "", genres: []
word_count: existingData?.word_count ?? 500,
genres: existingData?.genres ?? []
}, },
}) })
const router = useRouter()
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: z.infer<typeof formSchema>) {
try { try {
const res = await createStory(values) const res = await createStory(values)
//server actions return undefined if middleware authentication fails //server actions return undefined if middleware authentication fails
if (res === undefined) throw new Error("something went wrong") if (!res) throw new Error("something went wrong")
toast({ toast({ title: "Sucessfully submitted:", description: values.title })
title: "Successfully submitted:", router.refresh()
description: values.title,
})
} catch (error) { } catch (error) {
toast({ toast({
title: "UH-OH", title: "Oh dear... ",
description: error.message description: error.message
}) })
} }
window.location.reload()
} }

View File

@ -34,6 +34,7 @@ import {
import { useState } from "react" import { useState } from "react"
import { createSub } from "app/lib/create" import { createSub } from "app/lib/create"
import { subSchema } from "./schemas" import { subSchema } from "./schemas"
import { useRouter } from "next/navigation"
export type SubForm = z.infer<typeof subSchema> export type SubForm = z.infer<typeof subSchema>
@ -65,8 +66,8 @@ export default function SubmissionForm({ stories, pubs, responses, defaults }) {
</SelectItem> </SelectItem>
)) ))
const router = useRouter()
// 2. Define a submit handler.
async function onSubmit(values: z.infer<typeof subSchema>) { async function onSubmit(values: z.infer<typeof subSchema>) {
try { try {
const res = await createSub(values) const res = await createSub(values)
@ -78,7 +79,7 @@ export default function SubmissionForm({ stories, pubs, responses, defaults }) {
description: error.message description: error.message
}) })
} }
window.location.reload() router.refresh()
} }
function onErrors(errors) { function onErrors(errors) {

View File

@ -39,7 +39,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { EyeIcon, Trash2 } from "lucide-react" import { EyeIcon, Trash2 } from "lucide-react"
import { usePathname } from "next/navigation" import { usePathname, useSearchParams } from "next/navigation"
import FormContextMenu from "./contextMenu" import FormContextMenu from "./contextMenu"
import { deleteRecord, deleteRecords } from "app/lib/del" import { deleteRecord, deleteRecords } from "app/lib/del"
import { Pathname } from "app/types" import { Pathname } from "app/types"
@ -50,6 +50,8 @@ import { tableNameToItemName } from "app/lib/nameMaps"
import { Genre, Pub, Response, Story } from "@prisma/client" import { Genre, Pub, Response, Story } from "@prisma/client"
import EditSubmissionDialog from "app/submission/edit" import EditSubmissionDialog from "app/submission/edit"
import { DialogTitle } from "@radix-ui/react-dialog" import { DialogTitle } from "@radix-ui/react-dialog"
import { toast } from "@/components/ui/use-toast"
import { useRouter } from "next/navigation"
export interface DataTableProps<TData, TValue> { export interface DataTableProps<TData, TValue> {
columns: ColumnDef<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 [filterBy, setFilterBy] = useState(table.getAllColumns()[0])
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
return (<> return (<>
@ -181,8 +186,11 @@ export function DataTable<TData, TValue>({
<DialogFooter> <DialogFooter>
<DialogClose asChild> <DialogClose asChild>
<Button variant="destructive" <Button variant="destructive"
onClick={() => { onClick={async () => {
deleteRecord(dialogRow.original.id, pathname) 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! }}>Yes, delete it!
</Button> </Button>
</DialogClose> </DialogClose>
@ -206,11 +214,14 @@ export function DataTable<TData, TValue>({
<DialogFooter> <DialogFooter>
<DialogClose asChild> <DialogClose asChild>
<Button variant="destructive" <Button variant="destructive"
onClick={() => { onClick={async () => {
const selectedRows = table.getState().rowSelection const selectedRows = table.getState().rowSelection
const rowIds = Object.keys(selectedRows) const rowIds = Object.keys(selectedRows)
const recordIds = rowIds.map(id => Number(table.getRow(id).original.id)) 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> Yes, delete them!</Button>
</DialogClose> </DialogClose>

View File

@ -12,6 +12,7 @@ import { toast } from "@/components/ui/use-toast"
import GenreBadges from "app/ui/genreBadges" import GenreBadges from "app/ui/genreBadges"
import { updateField, updateGenres } from "app/lib/update" import { updateField, updateGenres } from "app/lib/update"
import { Genre } from "@prisma/client" import { Genre } from "@prisma/client"
import { useRouter } from "next/navigation"
export default function GenrePickerInputCell(props: CellContext<any, any>) { export default function GenrePickerInputCell(props: CellContext<any, any>) {
@ -23,7 +24,7 @@ export default function GenrePickerInputCell(props: CellContext<any, any>) {
const value = props.cell.getValue() const value = props.cell.getValue()
const genres = props.table.options.meta.genres const genres = props.table.options.meta.genres
const [isActive, setIsActive] = useState(false) const [isActive, setIsActive] = useState(false)
const router = useRouter()
async function onSubmit({ genres }: { genres: number[] }, event: Event) { async function onSubmit({ genres }: { genres: number[] }, event: Event) {
event.preventDefault() event.preventDefault()
try { try {
@ -36,7 +37,7 @@ export default function GenrePickerInputCell(props: CellContext<any, any>) {
}) })
if (res === undefined) throw new Error("Something went wrong.") if (res === undefined) throw new Error("Something went wrong.")
toast({ title: "Field updated successfully." }) toast({ title: "Field updated successfully." })
window.location.reload() router.refresh()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
toast({ title: "Something went wrong." }) toast({ title: "Something went wrong." })

View File

@ -9,6 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form"; import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
import TitleContainer from "app/ui/titleContainer"; 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({ cellContext, className }: { className: string, cellContext: CellContext<any, any> }) {
const [isActive, setIsActive] = useState(false) const [isActive, setIsActive] = useState(false)
@ -29,7 +30,7 @@ export function TextInputCell({ cellContext, className }: { className: string, c
[column]: cellContext.cell.getValue() [column]: cellContext.cell.getValue()
}, },
}) })
const router = useRouter()
async function onSubmit(value: z.infer<typeof formSchema>) { async function onSubmit(value: z.infer<typeof formSchema>) {
try { try {
const res = await updateField({ const res = await updateField({
@ -41,7 +42,7 @@ export function TextInputCell({ cellContext, className }: { className: string, c
}) })
if (res === undefined) throw new Error("something went wrong") if (res === undefined) throw new Error("something went wrong")
toast({ title: "Field updated successfully." }) toast({ title: "Field updated successfully." })
window.location.reload() router.refresh()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
toast({ title: "Something went wrong." }) toast({ title: "Something went wrong." })