diff --git a/src/app/admin/[...slug]/page.tsx b/src/app/admin/[...slug]/page.tsx index fb0d9d7..8f56f8f 100644 --- a/src/app/admin/[...slug]/page.tsx +++ b/src/app/admin/[...slug]/page.tsx @@ -59,13 +59,11 @@ export default async function Page(props:Props){ const slug:string|string[] = props.params.slug ? props.params.slug : 'home'; - - return (
- + {await getCurrentView(slug.toString())} diff --git a/src/app/admin/login/page.tsx b/src/app/admin/login/page.tsx deleted file mode 100644 index 18f5708..0000000 --- a/src/app/admin/login/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client' - -import { serverAttemptAuthenticateUser } from '@/app/lib/actions' -import { useFormState, useFormStatus } from "react-dom"; -import { cookies } from 'next/headers'; - - -export default function Page(state:any) { - const [loginResult, dispatch] = useFormState(serverAttemptAuthenticateUser, undefined) - console.log(dispatch); - - console.log(state); - // if(loginResult?.cookie && loginResult.cookie){ - // cookies().set('auth',loginResult.cookie['auth']) - // } - - return ( -
-
- - -
{loginResult?.errorMessage &&

{loginResult?.errorMessage}

}
- - -
-

{""+loginResult?.cookie}

-
-
- ) -} - -function LoginButton() { - const { pending } = useFormStatus() - - return ( - - ) -} \ No newline at end of file diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 4673777..543a858 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -38,7 +38,7 @@ export default async function Page(props:Props){
- + {/*
{JSON.stringify(cookies().getAll())}
*/}
diff --git a/src/app/api/setupDB/route.ts b/src/app/api/setupDB/route.ts new file mode 100644 index 0000000..522f022 --- /dev/null +++ b/src/app/api/setupDB/route.ts @@ -0,0 +1,75 @@ +'use server' + +import { cookies } from "next/headers"; + +import { APIError} from "@/util/api/error" +import { UserAuth, parseBasicAuth, getAssociatedUser } from "@/util/api/user" +import { Auth, Post, Tag, User } from "@/model/Models"; +import { Project } from "@/model/Project"; +import { DBState } from "@/model/DBState"; +import Sequelize, { DataTypes } from "@sequelize/core"; +import { SqliteColumnsDescription, SqliteDialect } from "@sequelize/sqlite3"; + + +async function trySetup(request:Request){ + + const sequelize = await new Sequelize({ + dialect: SqliteDialect, + storage: 'db.sqlite' + }) + const queryInterface = sequelize.queryInterface + // await User.sync(); + await Auth.sync(); + await User.sync(); + await Project.sync() + await Tag.sync(); + await Post.sync(); + await DBState.sync(); + + const version = await (await DBState.findAll()).sort((a,b)=> ((a.version > b.version) ? 1 : -1)).map(a=>a.version)[0]; + + await Project.findOne({where: { + readableIdentifier: 'blog' + }}).then(e=> e ? e : Project.create({name:'General Blog',readableIdentifier:'blog'})); + await User.findOne({where: { + username: 'admin' + }}).then(e=> e ? e : User.create({username: 'admin', password: 'changeme', perms: {isAdmin: true}})); + + + switch(version){ + case 1: + break; + default: + const postsRows:SqliteColumnsDescription = await queryInterface.describeTable('Posts').then(t=>t); + + if (!postsRows['project_id']) queryInterface.addColumn('Posts','project_id',{type: DataTypes.INTEGER, acceptsNull:()=>false,defaultValue:1}) + break; + } + + return new Response( + JSON.stringify( + { + test: await queryInterface.describeTable('Posts').then(t=>t) + }), + { + status: 200, + headers:{ + "Content-Type": "text/JSON" + } + } + ); +} + +export async function GET(request:Request){ + try{ + return await trySetup(request); + } + catch(e){ + if (e instanceof APIError){ + return new Response(e.info.responseText,{status:e.info.status}); + } + else{ + throw e; + } + } +} diff --git a/src/app/article/[...slug]/page.tsx b/src/app/article/[...slug]/page.tsx index 25017b7..b14080e 100644 --- a/src/app/article/[...slug]/page.tsx +++ b/src/app/article/[...slug]/page.tsx @@ -1,6 +1,6 @@ -import Header from "@/components/shared/header"; +import Header from "@/components/shared/brand/header"; import PageContainer from "@/components/shared/page-container"; -import Navbar from "@/components/shared/navbar"; +import Navbar from "@/components/shared/navigation/navbar"; import Sidebar from "@/components/shared/sidebar"; import Article from "@/components/shared/news/article"; import ArticlePreview from "@/components/shared/news/article-preview" diff --git a/src/app/lib/postActions.ts b/src/app/lib/postActions.ts index 279bb55..696a2d9 100644 --- a/src/app/lib/postActions.ts +++ b/src/app/lib/postActions.ts @@ -1,9 +1,12 @@ 'use server'; + +import { revalidatePath, revalidateTag } from 'next/cache' import { Post } from "@/model/Post"; -import { Attributes } from "@sequelize/core"; +import { Attributes, where } from "@sequelize/core"; export async function deletePost(postID: number): Promise { + revalidatePath("/admin/man-post","page") const destroy = await Post.destroy({ where: { id: postID } }); return true; } @@ -13,3 +16,9 @@ export async function getPosts(): Promise { const posts = await Post.findAll(); return JSON.stringify(posts); } + +export async function updatePost(postAttributes: Partial>): Promise { + revalidatePath("/admin/man-post","page") + const post = await Post.update(postAttributes, {where:{id:postAttributes.id}}); + return JSON.stringify(post); +} \ No newline at end of file diff --git a/src/app/lib/projectActions.ts b/src/app/lib/projectActions.ts new file mode 100644 index 0000000..9a55808 --- /dev/null +++ b/src/app/lib/projectActions.ts @@ -0,0 +1,7 @@ +'use server'; +import { Project } from "@/model/Project"; + +export async function getProjects(): Promise { + const posts = await Project.findAll(); + return JSON.stringify(posts); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index e74d1ff..c1ac7ad 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ -import Header from "@/components/shared/header"; +import Header from "@/components/shared/brand/header"; import PageContainer from "@/components/shared/page-container"; -import Navbar from "@/components/shared/navbar"; +import Navbar from "@/components/shared/navigation/navbar"; import Sidebar from "@/components/shared/sidebar"; import ArticlePreview from "@/components/shared/news/article-preview" import ReactDOM from "react"; diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index c164465..308690c 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -1,7 +1,7 @@ // import components -import Header from "@/components/shared/header"; +import Header from "@/components/shared/brand/header"; import PageContainer from "@/components/shared/page-container"; -import Navbar from "@/components/shared/navbar"; +import Navbar from "@/components/shared/navigation/navbar"; import Sidebar from "@/components/shared/sidebar"; import ArticlePreview from "@/components/shared/news/article-preview" // import styles diff --git a/src/components/client/admin/PostTable.tsx b/src/components/client/admin/PostTable.tsx index aa50e26..f5ccd0e 100644 --- a/src/components/client/admin/PostTable.tsx +++ b/src/components/client/admin/PostTable.tsx @@ -1,78 +1,138 @@ 'use client' -import { ReactNode } from "react" +import React, { ChangeEvent, LegacyRef, MutableRefObject, ReactNode, useLayoutEffect, useRef } from "react" import TableGen from "../TableGen" -import { Attributes } from "@sequelize/core"; +import { Attributes, CreationAttributes } from "@sequelize/core"; import { Post } from "@/model/Post"; import { useState } from "react"; +import { Project } from "@/model/Project"; +import { revalidatePath } from "next/cache"; +import { usePathname } from 'next/navigation'; type Actions = { deletePost:any getPosts:()=>Promise + getProjects:()=>Promise + savePost:(data:Partial>)=>Promise } type Props = { children?:ReactNode; headings:Array; - data:any[]; - actions?:Actions; + data:Attributes[]; + projects:Attributes[]; + actions:Actions; } type EditorProps = { open:boolean; - post?:Attributes; + post:Partial>; + projects?:Attributes[] + actionSavePost:(data:Partial>)=>Promise } -function RenderEditor(props:EditorProps){ - let [content,setContent] = useState(props.post?.content) - let [title,setTitle] = useState(props.post?.title) - - return
-

Edit Post

-

Title

- setTitle(e.target.value)} type='text' className="m-2"> -

Content

- - - - -
-} export default function PostTable(props:Props){ + const pathname = usePathname(); + + function RenderEditor(props:EditorProps){ + let [content,setContent] = useState(props.post?.content) + let [title,setTitle] = useState(props.post?.title) + let [projectID,setProjectID] = useState(props.post?.project_id) + let textbox:any = useRef(undefined); + + function adjustHeight() { + if(!textbox.current || !textbox.current.style) return + textbox.current.style.height = "fit-content"; + textbox.current.style.height = `${textbox.current.scrollHeight}px`; + } + + useLayoutEffect(adjustHeight, []); + + return
+

Edit Post

+

Title

+ setTitle(e.target.value)} type='text' className="m-2"> +

Content

+ +

Project

+ + + + +
+ } + + function closeEditor(){ + setEditor({ + open:false + }) + } + function showEditor(entry:Attributes){ setEditor({ open: true, - post: entry + post: entry, + actionSavePost: props.actions.savePost }) } - const initEditorState:EditorProps = { - open: false + + const initEditorState:Partial = { + open: false, + post: {}, + actionSavePost: props.actions.savePost } const [posts, setPosts] = useState(props.data); const [editor, setEditor] = useState(initEditorState) + const [projects, setProjects] = useState(props.projects); function deletePost(entry:Attributes){ props.actions?.deletePost(entry.id); props.actions?.getPosts().then((p)=>setPosts(JSON.parse(p))); } + const aifa = (a:ReactNode,b:ReactNode) => a ? a : b + + function refetch(){ + props.actions.getPosts().then(e=>setPosts(JSON.parse(e))) + props.actions.getProjects().then(e=>setProjects(JSON.parse(e))) + console.log(pathname); + } return <> - {posts.map((d)=>{ + {posts.map((d:Attributes)=>{ return <> - {d.id} + {d.id} {d.title} {d.content.length<255 ? d.content :`${d.content.substring(0,255)}...`} - {d.createdAt} - {d.updatedAt} + { + aifa((projects.find((e)=>{ + return (e.id == d.project_id) + })?.readableIdentifier), 'uncategorized') + } + {d.createdAt?.toString()} + {d.updatedAt?.toString()} {(editor.open && editor.post && editor.post.id == d.id)? - :""} + :""} })} diff --git a/src/components/server/admin/ServerAdminPanel.tsx b/src/components/server/admin/ServerAdminPanel.tsx index 61ba1cc..85a4c3d 100644 --- a/src/components/server/admin/ServerAdminPanel.tsx +++ b/src/components/server/admin/ServerAdminPanel.tsx @@ -8,10 +8,9 @@ import { ReactNode } from "react"; interface Props { children?: ReactNode; - auth?: AuthProps; slug?: string; } export default async function ServerAdminPanel(props:Props){ - return {props.children} + return {props.children} } \ No newline at end of file diff --git a/src/components/server/admin/adminPanel.tsx b/src/components/server/admin/adminPanel.tsx index e7bf7d0..42c2535 100644 --- a/src/components/server/admin/adminPanel.tsx +++ b/src/components/server/admin/adminPanel.tsx @@ -9,7 +9,7 @@ import PostView from "./views/PostView"; interface Props { children?: ReactNode; - auth?: AuthProps; + // auth?: AuthProps; slug?: string; } diff --git a/src/components/server/admin/views/PostEditor.tsx b/src/components/server/admin/views/PostEditor.tsx deleted file mode 100644 index 28505b7..0000000 --- a/src/components/server/admin/views/PostEditor.tsx +++ /dev/null @@ -1,10 +0,0 @@ - - -export default async function(){ - -
-

Edit Post

-

Title

- -
-} \ No newline at end of file diff --git a/src/components/server/admin/views/PostView.tsx b/src/components/server/admin/views/PostView.tsx index 8576898..6fe762b 100644 --- a/src/components/server/admin/views/PostView.tsx +++ b/src/components/server/admin/views/PostView.tsx @@ -5,7 +5,9 @@ import { constructAPIUrl } from "@/util/Utils"; import { ReactNode, useEffect } from "react"; import TableGen from "../../../client/TableGen"; import PostTable from "@/components/client/admin/PostTable"; -import { deletePost, getPosts } from "@/app/lib/postActions"; +import { deletePost, getPosts, updatePost } from "@/app/lib/postActions"; +import { getProjects } from "@/app/lib/projectActions"; +import { Project } from "@/model/Project"; type Props = { children?:ReactNode @@ -17,18 +19,25 @@ export default async function PostView(props:Props){ '#', 'Title', 'Content', + 'Project', 'Date Created', 'Date Modified', 'Edit', 'Delete', ] - const data = await Post.findAll(); - const passData:any[] = data.map((e)=>JSON.parse(JSON.stringify(e))); + const actions = { + deletePost: deletePost, + getPosts:getPosts, getProjects: + getProjects, + savePost:updatePost + }; + const posts:Post[] = await Post.findAll().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 (

Post Management

- +
); diff --git a/src/components/shared/AutoSizeTextArea.tsx b/src/components/shared/AutoSizeTextArea.tsx new file mode 100644 index 0000000..8ebf477 --- /dev/null +++ b/src/components/shared/AutoSizeTextArea.tsx @@ -0,0 +1,29 @@ +import { ChangeEvent, ChangeEventHandler, ReactElement, useRef, useState } from "react" + +type Props = { + children:Array + onChange?:ChangeEventHandler +} + +export const AutoSizeTextArea = (props:Props) => { + + let textbox:any = useRef(undefined); + + const [content, setContent] = useState(''); + + const adjustHeight = () => { + if(!textbox.current || !textbox.current.style) return; + textbox.current.style.height = "fit-content"; + textbox.current.style.height = `${textbox.current.scrollHeight}px`; + } + + const onChange = (e:ChangeEvent) => { + setContent(e.target.value); + adjustHeight(); + if (props.onChange) props.onChange(e); + } + + return <> + + +} \ No newline at end of file diff --git a/src/components/shared/header.module.css b/src/components/shared/brand/header.module.css similarity index 100% rename from src/components/shared/header.module.css rename to src/components/shared/brand/header.module.css diff --git a/src/components/shared/header.tsx b/src/components/shared/brand/header.tsx similarity index 100% rename from src/components/shared/header.tsx rename to src/components/shared/brand/header.tsx diff --git a/src/components/shared/navbar.module.css b/src/components/shared/navigation/navbar.module.css similarity index 100% rename from src/components/shared/navbar.module.css rename to src/components/shared/navigation/navbar.module.css diff --git a/src/components/shared/navbar.tsx b/src/components/shared/navigation/navbar.tsx similarity index 100% rename from src/components/shared/navbar.tsx rename to src/components/shared/navigation/navbar.tsx diff --git a/src/components/shared/page-template.tsx b/src/components/shared/page-template.tsx index 8d8033d..6e46c9d 100644 --- a/src/components/shared/page-template.tsx +++ b/src/components/shared/page-template.tsx @@ -1,6 +1,6 @@ import { ReactNode } from "react" -import Header from "./header"; -import Navbar from "./navbar"; +import Header from "./brand/header"; +import Navbar from "./navigation/navbar"; import PageContainer from "./page-container"; interface Props { diff --git a/src/lib/swagger.ts b/src/lib/swagger.ts new file mode 100644 index 0000000..1b991b9 --- /dev/null +++ b/src/lib/swagger.ts @@ -0,0 +1,25 @@ +import { createSwaggerSpec } from "next-swagger-doc"; + +export const getApiDocs = async () => { + const spec = createSwaggerSpec({ + apiFolder: "app/api", // define api folder under app folder + definition: { + openapi: "3.0.0", + info: { + title: "Next Swagger API Example", + version: "1.0", + }, + components: { + securitySchemes: { + BearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + security: [], + }, + }); + return spec; +}; \ No newline at end of file diff --git a/src/model/DBState.ts b/src/model/DBState.ts new file mode 100644 index 0000000..8b080a3 --- /dev/null +++ b/src/model/DBState.ts @@ -0,0 +1,40 @@ +import { Association, Attributes, CreationAttributes, CreationOptional, DataTypes, HasManyGetAssociationsMixin, HasOneCreateAssociationMixin, HasOneGetAssociationMixin, InferAttributes, InferCreationAttributes, Model, NonAttribute, Sequelize } from "@sequelize/core"; +import { + PrimaryKey, + Attribute, + AutoIncrement, + NotNull, + BelongsTo, + Unique, + HasMany, + HasOne, + UpdatedAt, + CreatedAt, +} from '@sequelize/core/decorators-legacy'; + +import { SqliteDialect } from '@sequelize/sqlite3'; + +export class DBState extends Model, InferCreationAttributes>{ + + @Attribute(DataTypes.INTEGER) + @PrimaryKey + @AutoIncrement + @Unique + declare id: CreationOptional; + @Attribute(DataTypes.STRING) + declare version:number; + // Date thingies + + @CreatedAt + declare createdAt: CreationOptional; + @UpdatedAt + declare updatedAt: CreationOptional; + +} + +const sequelize = new Sequelize({ + dialect: SqliteDialect, + storage: 'db.sqlite', + models: [DBState] +}); + diff --git a/src/model/Post.ts b/src/model/Post.ts index e9452cb..7d3032c 100644 --- a/src/model/Post.ts +++ b/src/model/Post.ts @@ -35,13 +35,13 @@ export class Post extends Model, InferCreationAttributes

; @Attribute(DataTypes.INTEGER) - declare project_id?:CreationOptional>; + declare project_id:CreationOptional>; @BelongsTo(()=>User, { foreignKey: 'user_id', inverse: { type: 'hasMany', as: 'posts' } }) declare user:NonAttribute; - @BelongsTo(()=>Project, { inverse: { type: 'hasMany', as: 'posts' } }) - declare project:NonAttribute; + @BelongsTo(()=>Project, { foreignKey:'project_id', inverse: { type: 'hasMany', as: 'posts' } }) + declare project:CreationOptional; @BelongsToMany(()=>Tag, { through: { model: ()=>PostTag, unique: false}, inverse: {as: 'taggedPosts'} }) declare postTags?:NonAttribute; diff --git a/src/model/Project.ts b/src/model/Project.ts index 1c40357..344b9f7 100644 --- a/src/model/Project.ts +++ b/src/model/Project.ts @@ -5,8 +5,8 @@ import { Attribute, AutoIncrement, PrimaryKey, Unique } from "@sequelize/core/de import { Post } from "./Post"; export class Project extends Model, InferCreationAttributes> { - @Attribute(DataTypes.INTEGER) @PrimaryKey @Unique @AutoIncrement - declare id: number; + @Attribute(DataTypes.INTEGER) @PrimaryKey @Unique @AutoIncrement + declare id: CreationOptional; @Attribute(DataTypes.STRING) @Unique declare readableIdentifier: string; @Attribute(DataTypes.STRING) @@ -20,5 +20,6 @@ export class Project extends Model, InferCreationAttrib const sequelize = new Sequelize({ dialect: SqliteDialect, + storage: 'db.sqlite', models: [Project] })