Refactoring

This commit is contained in:
Andreas 2024-06-25 08:09:39 +02:00
parent 57375bd902
commit 943267be04
10 changed files with 274 additions and 135 deletions

View File

@ -5,6 +5,7 @@ import Sidebar, { SidebarEntry } from "@/components/server/admin/views/sidebar";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import PostView from "@/components/server/admin/views/PostView"; import PostView from "@/components/server/admin/views/PostView";
import { ReactNode } from "react"; import { ReactNode } from "react";
import ProjectView from "@/components/server/admin/views/ProjectView";
type Props = { type Props = {
params: { params: {
@ -20,12 +21,16 @@ function Home(){
function PostManager(){ function PostManager(){
return <div>posts</div> return <div>posts</div>
} }
function ProjectManager(){
return <div>projects</div>
}
async function getViewMap():Promise<Map<string, JSX.Element>>{ async function getViewMap():Promise<Map<string, JSX.Element>>{
return new Map([ return new Map([
['home', <Home></Home>], ['home', <Home></Home>],
['man-post', <PostView></PostView>] ['man-post', <PostView></PostView>],
['man-proj', <ProjectView></ProjectView>]
]); ]);
} }

View File

@ -11,7 +11,7 @@ export default function TableGen(props:Props){
<table className="table overflow-scroll"> <table className="table overflow-scroll">
<thead> <thead>
<tr> <tr>
{props.headings.map((h)=> {[...props.headings,'Edit', 'Delete'].map((h)=>
<th key={h} scope="col">{h}</th> <th key={h} scope="col">{h}</th>
)} )}
</tr> </tr>

View File

@ -14,17 +14,17 @@ export type PostTableCallbacks = {
} }
export type EditorProps = { export type EditorProps = {
open:boolean; editorOpenState:boolean;
post:Partial<PostAttributesWithBuckets>; openedPost:Partial<PostAttributesWithBuckets>;
projects?:Attributes<Project>[]; projects?:Attributes<Project>[];
callbacks:PostTableCallbacks; callbacks:PostTableCallbacks;
} }
export default function PostEditor(props:EditorProps){ export default function PostEditor(props:EditorProps){
let [content,setContent] = useState(props.post?.content) let [content,setContent] = useState(props.openedPost?.content)
let [title,setTitle] = useState(props.post?.title) let [title,setTitle] = useState(props.openedPost?.title)
let [projectID,setProjectID] = useState(props.post?.project_id) let [projectID,setProjectID] = useState(props.openedPost?.project_id)
let textbox:any = useRef(undefined); let textbox:any = useRef(undefined);
function adjustHeight(): void { function adjustHeight(): void {
@ -39,76 +39,83 @@ export default function PostEditor(props:EditorProps){
const projectSelectionChange:ChangeEventHandler<HTMLSelectElement> = (e)=>setProjectID(parseInt(e.target.value)); const projectSelectionChange:ChangeEventHandler<HTMLSelectElement> = (e)=>setProjectID(parseInt(e.target.value));
const onClickSaveButton:MouseEventHandler<HTMLButtonElement> = (e)=>{ const onClickSaveButton:MouseEventHandler<HTMLButtonElement> = (e)=>{
props.callbacks.savePost({ props.callbacks.savePost({
id: props.post.id as number, id: props.openedPost.id as number,
content:content as string, content:content as string,
title:title as string, title:title as string,
project_id:projectID
}); });
} }
const onClickCancelButton:MouseEventHandler<HTMLButtonElement> = (e)=>{props.callbacks.closeEditor();} const onClickCancelButton:MouseEventHandler<HTMLButtonElement> = (e)=>{props.callbacks.closeEditor();}
return <> return <>
<form className="bg-light w-[100%] h-content p-1"> <form className="bg-light w-[100%] h-content p-1">
<h1 className="m-2">Edit Post</h1> <h1 key="heading-editpost" className="m-2">Edit Post</h1>
<h2 className="m-2">Title</h2> <h2 key="label-title" className="m-2">Title</h2>
<input value={title} onChange={e => setTitle(e.target.value)} type='text' className="m-2"></input> <input key="input-title"
<h2 className="m-2">Content</h2> value={title}
<textarea onChange={onTextAreaChange} onChange={e => setTitle(e.target.value)}
type='text' className="m-2"/>
<h2 key="label-content" className="m-2">Content</h2>
<textarea key="input-content" onChange={onTextAreaChange}
ref={textbox} ref={textbox}
value={content} value={content}
style={{"height" : "100%"}} style={{"height" : "100%"}}
className="w-[100%] min-h-auto h-content align-top text-start text-base line-clamp-6 m-2"/> className="w-[100%] min-h-auto h-content align-top text-start text-base line-clamp-6 m-2"/>
<h2 className="m-2">Project</h2> <h2 key="label-project" className="m-2">Project</h2>
<select onChange={projectSelectionChange} <select key="input-project"
name="projects" onChange={projectSelectionChange}
id="projects" name="projects"
defaultValue={props.post?.project?.id} id="projects"
placeholder={props.post?.project?.name} defaultValue={props.openedPost?.project?.id}
value={projectID} className="m-2"> placeholder={props.openedPost?.project?.name}
<option value={0}>unassigned</option> value={projectID} className="m-2">
<option key="projectSelectionOpt-0" value={0}>unassigned</option>
{props.projects?.map(p=><option key={`projectSelectionOpt-${p.id}`} value={p.id}>{p.readableIdentifier}</option>)} {props.projects?.map(p=><option key={`projectSelectionOpt-${p.id}`} value={p.id}>{p.readableIdentifier}</option>)}
</select> </select>
<h2>Attachments</h2> <h2 key="label-attachments">Attachments</h2>
<table className="table table-striped"> <table key="table-buckets" className="table table-striped">
<thead> <thead>
<tr><td>Buckets</td></tr> <tr><td>Buckets</td></tr>
</thead> </thead>
<tbody> <tbody>
{ {
props.post.buckets props.openedPost.buckets
? (()=>{ ? (()=>{
let bucketMap:Map<UUID,Attributes<Bucket>> = new Map(props.post.buckets.map((b)=>[b.id as UUID,b])); let bucketMap:Map<UUID,Attributes<Bucket>> = new Map(props.openedPost.buckets.map((b)=>[b.id as UUID,b]));
let bucketList = [...props.post.buckets.map((b)=>b.id)]; let bucketList = [...props.openedPost.buckets.map((b)=>b.id)];
return bucketList.map((e)=>{ return bucketList.map((e)=>{
return <> return <>
<tr key={`bucketAccordionRow-${bucketList.indexOf(e).toString()}`}><Accordion> <tr key={`bucketAccordionRow-${bucketList.indexOf(e).toString()}`}><Accordion>
<AccordionItem eventKey={bucketList.indexOf(e).toString()}> <AccordionItem eventKey={bucketList.indexOf(e).toString()}>
<AccordionHeader>{e}</AccordionHeader> <AccordionHeader>{e}</AccordionHeader>
<AccordionBody> <AccordionBody>
<ul> <ul>
<li><input type="file" className='btn btn-success' onChange={(e)=>{ <li><input type="file" className='btn btn-success' onChange={(e)=>{
for (let index = 0; index < ((e.target.files as FileList).length as number); index++) { for (let index = 0; index < ((e.target.files as FileList).length as number); index++) {
const element = (e.target.files as FileList)[index]; const element = (e.target.files as FileList)[index];
const fReader = new FileReader() const fReader = new FileReader()
fReader.readAsDataURL(element) fReader.readAsDataURL(element)
fReader.onloadend = (event)=>{ fReader.onloadend = (event)=>{
console.log(event.target?.result); console.log(event.target?.result);
}; };
} }
}}/></li> }}/></li>
{(()=>{ {(()=>{
const bucket = bucketMap.get(e as UUID) const bucket = bucketMap.get(e as UUID)
return bucket && bucket.attachments ? bucket.attachments.map((attachment)=><li key={`listItem-file-${attachment.filename}`}>{attachment.filename}</li>) : <></> return bucket && bucket.attachments
})()} ? bucket.attachments.map((attachment)=><li key={`listItem-file-${attachment.filename}`}>{attachment.filename}</li>)
</ul> : <></>
</AccordionBody> })()}
</AccordionItem> </ul>
</Accordion></tr></> </AccordionBody>
}) </AccordionItem>
})() </Accordion></tr></>
: <></> })
} })()
</tbody> : <></>
}
</tbody>
</table> </table>
<button type="button" className="m-2 btn btn-primary">Preview</button> <button type="button" className="m-2 btn btn-primary">Preview</button>
<button type="button" className="m-2 btn btn-success" onClick={onClickSaveButton}>Save</button> <button type="button" className="m-2 btn btn-success" onClick={onClickSaveButton}>Save</button>

View File

@ -1,13 +1,25 @@
'use client' 'use client'
import React, { ChangeEvent, ChangeEventHandler, LegacyRef, MouseEventHandler, MutableRefObject, ReactNode, useLayoutEffect, useRef } from "react" import React, {
import TableGen from "../TableGen" ChangeEvent,
import { Attributes, CreationAttributes } from "@sequelize/core"; ChangeEventHandler,
LegacyRef,
MouseEventHandler,
MutableRefObject,
ReactNode,
useLayoutEffect,
useRef
} from "react";
import TableGen from "../TableGen";
import toast from "react-hot-toast";
import PostEditor, { EditorProps } from "./PostEditor";
import {
Attributes,
CreationAttributes
} from "@sequelize/core";
import { useState } from "react"; import { useState } from "react";
import { Project, Post, Bucket } from "@/model/Models"; import { Project, Post, Bucket } from "@/model/Models";
import toast from "react-hot-toast"
import { ActionResult } from "@/app/lib/actions/ActionResult"; import { ActionResult } from "@/app/lib/actions/ActionResult";
import { handleActionResult } from "@/app/lib/actions/clientActionHandler"; import { handleActionResult } from "@/app/lib/actions/clientActionHandler";
import PostEditor, { EditorProps } from "./PostEditor";
import { getPostsWithBuckets } from "@/app/lib/actions/entityManagement/postActions"; import { getPostsWithBuckets } from "@/app/lib/actions/entityManagement/postActions";
import { PostAttributesWithBuckets } from "@/model/Post"; import { PostAttributesWithBuckets } from "@/model/Post";
@ -17,7 +29,7 @@ type Actions = {
getProjects: () => Promise<ActionResult<Attributes<Project>[]>> getProjects: () => Promise<ActionResult<Attributes<Project>[]>>
savePost: (data: Partial<Attributes<Post>>) => Promise<ActionResult<Attributes<Post>[]>> savePost: (data: Partial<Attributes<Post>>) => Promise<ActionResult<Attributes<Post>[]>>
// uploadAttachment: ()=> Promise<ActionResult<any>> // uploadAttachment: ()=> Promise<ActionResult<any>>
} };
type Props = { type Props = {
children?: ReactNode; children?: ReactNode;
@ -27,53 +39,57 @@ type Props = {
actions: Actions; actions: Actions;
} }
const aifa = (a: ReactNode, b: ReactNode) => a ? a : b
export default function PostTable(props: Props) { export default function PostTable(props: Props) {
function closeEditor(): void {
setEditor({
open: false
})
}
function showEditor(entry: Partial<PostAttributesWithBuckets>): void {
setEditor({
open: true,
post: entry
})
}
const initEditorState: Partial<EditorProps> = { const initEditorState: Partial<EditorProps> = {
open: false editorOpenState: false
} }
// Set up required state hooks
const [posts, setPosts] = useState(props.data); const [posts, setPosts] = useState(props.data);
const [editor, setEditor] = useState(initEditorState) const [editor, setEditor] = useState(initEditorState)
const [projects, setProjects] = useState(props.projects); const [projects, setProjects] = useState(props.projects);
// Define editor controls
function deletePost(entry: Partial<PostAttributesWithBuckets>) { function closeEditor(): void {
if(! entry.id) return; setEditor({
props.actions?.deletePost(entry.id) editorOpenState: false
.then(actionResult => { })
const result = handleActionResult(actionResult); }
if (!result) return; function showEditor(entry: Partial<PostAttributesWithBuckets>): void {
posts.splice(posts.indexOf(entry), 1); setEditor({
setPosts([...posts]) editorOpenState: true,
toast.success('Removed Post:' + entry.id); openedPost: entry
}); })
} }
const aifa = (a: ReactNode, b: ReactNode) => a ? a : b function deletePost(entry: Partial<PostAttributesWithBuckets>) {
if (!entry.id) return;
props.actions?.deletePost(entry.id)
.then(actionResult => {
const result = handleActionResult(actionResult);
if (!result) return;
posts.splice(posts.indexOf(entry), 1);
setPosts([...posts])
toast.success('Removed Post:' + entry.id);
});
}
function refetch() { function refetch() {
props.actions?.getPosts().then((p) => { props.actions.getPosts()
const result = handleActionResult(p) .then((p) => {
if (result) setPosts(result) const result = handleActionResult(p)
}); if (result) setPosts(result)
props.actions.getProjects().then(e => { }).then(props.actions.getProjects)
const result = handleActionResult(e); .then(e => {
if (result) setProjects(result) const result = handleActionResult(e);
}) if (result) setProjects(result)
})
} }
function savePost(e: Partial<PostAttributesWithBuckets>) { function savePost(e: Partial<PostAttributesWithBuckets>) {
@ -83,37 +99,80 @@ export default function PostTable(props: Props) {
title: e.title, title: e.title,
project_id: e.project_id project_id: e.project_id
}) })
.then(res => handleActionResult(res)) .then(res => handleActionResult(res))
.then(getPostsWithBuckets) .then(getPostsWithBuckets)
.then(res => { .then(res => {
if (!res.error && res.result) setPosts(res.result); if (!res.error && res.result) setPosts(res.result);
}) })
closeEditor(); closeEditor();
}; };
return <> function TableFields({ postData }: { postData: Partial<PostAttributesWithBuckets> }) {
<TableGen headings={props.headings}> return [
{posts.map((d: Partial<PostAttributesWithBuckets>) => <> {
<tr key={d.id}> key: 'post-title',
<th key={`row${d.id}`} scope="row">{d.id}</th> content: postData.title
<td key='titlefield'>{d.title}</td> }, {
<td key='contentfield'>{d.content? (d.content.length < 255 ? d.content : `${d.content.substring(0, 255)}...`) : "No Content Found"}</td> key: 'post-content',
<td key='project'>{ content:
aifa((projects.find((e) => { !postData.content ? "No Content Found" :
return (e.id == d.project_id) postData.content.length < 255 ? postData.content :
})?.readableIdentifier), 'uncategorized') `${postData.content.substring(0, 255)}...`
}</td>
<td key='createdatfield'>{d.createdAt?.toString()}</td> }, {
<td key='updatedatfield'>{d.updatedAt?.toString()}</td> key: 'post-project',
<td key='btnedit'><button type="button" className="btn btn-primary" onClick={() => showEditor(d)}>Edit</button></td> content: aifa((projects.find((e) => {
<td key='btndelete'><button type="button" className="btn btn-danger" onClick={() => deletePost(d)}> Delete</button></td> return (e.id == postData.project_id)
})?.readableIdentifier), 'uncategorized')
}, {
key: 'post-createdat',
content: postData.createdAt?.toString()
}, {
key: 'post-updatedat',
content: postData.updatedAt?.toString()
}, {
key: 'post-editbutton',
content: <button key="editbutton" type="button" className="btn btn-primary" onClick={() => showEditor(postData)}>Edit</button>
}, {
key: 'post-deletebutton',
content: <button key="deletebutton" type="button" className="btn btn-danger" onClick={() => deletePost(postData)}> Delete</button>
}
].map(field =>
<td key={field.key}>{field.content}</td>
);
}
function TableRow(props: {postData: Partial<PostAttributesWithBuckets>, headings: string | any[]}){
return <><tr key={props.postData.id}>
<th key={`rowheading-post-${props.postData.id}`} scope="row">{props.postData.id}</th>
<TableFields key={`tableFields-${props.postData.id}`} postData={props.postData} />
</tr> </tr>
{ {(editor.editorOpenState
(editor.open && editor.post && editor.post.id == d.id) && editor.openedPost
?<tr key={'activeEditor'}><th scope="row" colSpan={props.headings.length}><PostEditor callbacks={{ savePost: savePost, closeEditor: closeEditor, uploadAttachment: ()=>{} }} open={editor.open} post={editor.post} projects={projects} ></PostEditor></th></tr> && editor.openedPost.id == props.postData.id)
: ""
} ? <tr>
</>)} <th scope="row" colSpan={props.headings.length}>
<PostEditor callbacks={{
savePost: savePost,
closeEditor: closeEditor,
uploadAttachment: () => { }
}}
editorOpenState={editor.editorOpenState}
openedPost={editor.openedPost}
projects={projects} />
</th>
</tr>
: ""}
</>
}
return <>
<TableGen key="der-table" headings={props.headings}>
{posts.map((postData: Partial<PostAttributesWithBuckets>) => <TableRow headings={props.headings}
postData={postData}
key={postData.id}/> )}
</TableGen> </TableGen>
</> </>
} }

View File

@ -2,11 +2,9 @@
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import LoginForm from "@/components/client/admin/loginForm"; import LoginForm from "@/components/client/admin/loginForm";
import AdminPanel from "@/components/server/admin/adminPanel";
import ClientAuthHandler from "@/components/client/admin/clientAuthHandler"; import ClientAuthHandler from "@/components/client/admin/clientAuthHandler";
import { serverValidateSessionCookie } from "@/app/lib/actions/actions"; import { serverValidateSessionCookie } from "@/app/lib/actions/actions";
import { constructAPIUrl } from "@/util/Utils";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { AuthContext, AuthProps } from "@/providers/providers"; import { AuthContext, AuthProps } from "@/providers/providers";
@ -21,7 +19,7 @@ interface Props {
export default async function AuthHandler(props: Props) { export default async function AuthHandler(props: Props) {
const protoKoek = await cookies().get('auth')?.value; const protoKoek = await cookies().get('auth')?.value;
const koek = decodeURIComponent(protoKoek ? protoKoek: ""); const koek = decodeURIComponent(protoKoek ? protoKoek: "");
console.log("koekje:" + koek) // console.log("koekje:" + koek)
let p:AuthProps = { let p:AuthProps = {
}; };
if(koek){ if(koek){

View File

@ -25,8 +25,6 @@ export default async function PostView(props:Props){
'Project', 'Project',
'Date Created', 'Date Created',
'Date Modified', 'Date Modified',
'Edit',
'Delete',
] ]
const actions = { const actions = {
deletePost: deletePost, deletePost: deletePost,
@ -43,7 +41,11 @@ export default async function PostView(props:Props){
<div className="w-[100%] min-h-fit bg-gray-100 overflow-scroll"> <div className="w-[100%] min-h-fit bg-gray-100 overflow-scroll">
<span className="flex flex-row flex-grow w-[100%] pl-2 pr-2"><h1 className="p-2 inline-block">Post Management</h1><section className="flex-grow"></section><button className='btn btn-success h-12 mt-auto mb-auto self-end'>New</button></span> <span className="flex flex-row flex-grow w-[100%] pl-2 pr-2"><h1 className="p-2 inline-block">Post Management</h1><section className="flex-grow"></section><button className='btn btn-success h-12 mt-auto mb-auto self-end'>New</button></span>
<div className="w-[100%] m-auto"> <div className="w-[100%] m-auto">
<PostTable data={posts} projects={projects} headings={headings} actions={actions}></PostTable> <PostTable data={posts}
projects={projects}
headings={headings}
actions={actions}>
</PostTable>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,66 @@
cache: 'no-store'
import { tryFetchPosts } from "@/app/api/post/route";
import { constructAPIUrl } from "@/util/Utils";
import { ReactNode, useEffect } from "react";
import TableGen from "../../../client/TableGen";
import PostTable from "@/components/client/admin/PostTable";
import { deletePost, getPostsWithBuckets, updatePost } from "@/app/lib/actions/entityManagement/postActions";
import { getProjects } from "@/app/lib/actions/entityManagement/projectActions";
import { Bucket, Project, Post, dbSync, Attachment } from "@/model/Models";
import { tryCreateAttachment } from "@/app/api/attachment/route";
import { Attributes } from '@sequelize/core';
import * as util from 'util'
type Props = {
children?:ReactNode
}
export default async function ProjectView(props:Props){
await dbSync;
// const headings = [
// '#',
// 'Title',
// 'Content',
// 'Project',
// 'Date Created',
// 'Date Modified',
// 'Edit',
// 'Delete',
// ]
const headings = Project.getAttributes();
// const actions = {
// deletePost: deletePost,
// getPosts:getPostsWithBuckets,
// getProjects:getProjects,
// savePost:updatePost,
// // uploadAttachment:tryCreateAttachment
// };
const posts:Post[] = await Post.findAll({include: {model: Bucket, include: {model: Attachment}}}).then(posts=>posts.map((e)=>JSON.parse(JSON.stringify(e))));
const projects = await Project.findAll().then(projects=>projects.map((e)=>JSON.parse(JSON.stringify(e))));
return <>
<div className="w-[100%] min-h-fit bg-gray-100 overflow-scroll">
<span className="flex flex-row flex-grow w-[100%] pl-2 pr-2"><h1 className="p-2 inline-block">Post Management</h1><section className="flex-grow"></section><button className='btn btn-success h-12 mt-auto mb-auto self-end'>New</button></span>
<div className="w-[100%] m-auto">
{(()=>{
let a:string[] = []
for (const key in headings)
{
a.push(key)
}
// return a.map((e)=> <div key={e}>{e}</div> );
return <TableGen headings={a}><></></TableGen>
})()}
{/* <PostTable data={posts}
projects={projects}
headings={headings}
actions={actions}>
</PostTable> */}
</div>
</div>
</>;
}

View File

@ -5,7 +5,6 @@ import "@/app/index.css"
import styles from "./article.module.css" import styles from "./article.module.css"
import { MDXComponents, MDXContent } from "mdx/types"; import { MDXComponents, MDXContent } from "mdx/types";
import { MDXRemote } from 'next-mdx-remote/rsc'
export async function ExampleComponent(){ export async function ExampleComponent(){
return ( return (
@ -16,6 +15,7 @@ export async function ExampleComponent(){
export default async function Article(params: { id: string|undefined, title: string|undefined, content: string|undefined, date?:string|undefined } ) { export default async function Article(params: { id: string|undefined, title: string|undefined, content: string|undefined, date?:string|undefined } ) {
const components = { ExampleComponent } const components = { ExampleComponent }
const MDXRemote = (await import ('next-mdx-remote/rsc')).MDXRemote;
return ( return (
<article id={`post-${params.id}`} className="bg-background-800 w-[80%] m-auto"> <article id={`post-${params.id}`} className="bg-background-800 w-[80%] m-auto">

View File

@ -53,7 +53,7 @@ export class Post extends Model<PostAttributes, PostCreationAttributes> {
@BelongsTo(()=>User, { foreignKey: 'user_id', inverse: { type: 'hasMany', as: 'posts' } }) @BelongsTo(()=>User, { foreignKey: 'user_id', inverse: { type: 'hasMany', as: 'posts' } })
declare user:NonAttribute<User>; declare user:NonAttribute<User>;
@BelongsTo(()=>Project, { foreignKey:'project_id', inverse: { type: 'hasMany', as: 'posts' } }) @BelongsTo(()=>Project, { foreignKey:'project_id', foreignKeyConstraints: false, inverse: { type: 'hasMany', as: 'posts' } })
declare project:CreationOptional<Project>; declare project:CreationOptional<Project>;
@BelongsToMany(()=>Tag, { through: { model: ()=>PostTag, unique: false}, inverse: {as: 'taggedPosts'} }) @BelongsToMany(()=>Tag, { through: { model: ()=>PostTag, unique: false}, inverse: {as: 'taggedPosts'} })

View File

@ -11,6 +11,8 @@ export class Project extends Model<InferAttributes<Project>, InferCreationAttrib
declare readableIdentifier: string; declare readableIdentifier: string;
@Attribute(DataTypes.STRING) @Attribute(DataTypes.STRING)
declare name: string; declare name: string;
/** Defined by {@link Post.project} */
declare posts?: NonAttribute<Post>[];
declare static associations: { declare static associations: {
posts: Association<Post, Project>; posts: Association<Post, Project>;