This commit is contained in:
Janis
2023-02-08 20:56:22 +01:00
parent 97d8de1a44
commit 730f33879b
17 changed files with 794 additions and 452 deletions

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

View File

@@ -1,14 +1,21 @@
import React from "react"; import React from "react";
import Link from "next/link"; import Link from "next/link";
import styles from "@/styles/modules/AdminNav.module.scss"; import styles from "@/styles/modules/AdminNav.module.scss";
import { useSession } from "next-auth/react";
import { redirect, useRouter } from "next/navigation";
function AdminNav() { function AdminNav() {
return ( const { data: session } = useSession();
<div className={styles.adminNav}>
<Link href={"/admin/editor/article/0"}>New article</Link> if (session) {
<Link href={"/admin/editor/category/0"}>New category</Link> return (
</div> <div className={styles.adminNav}>
); <Link href={"/admin/editor/article/0"}>New article</Link>
<Link href={"/admin/editor/category/0"}>New category</Link>
<Link href={"/api/auth/signout"}>Logout</Link>
</div>
);
}
} }
export default AdminNav; export default AdminNav;

View File

@@ -2,8 +2,12 @@ import React from "react";
import { apiUrl } from "@/global"; import { apiUrl } from "@/global";
import urlJoin from "url-join"; import urlJoin from "url-join";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { isElementAccessExpression } from "typescript";
export default function ArticleControl({ articleId }: { articleId: string }) { export default function ArticleControl({ articleId }: { articleId: string }) {
const { data: session } = useSession();
const router = useRouter(); const router = useRouter();
async function deleteArticle() { async function deleteArticle() {
await fetch(urlJoin(apiUrl, `articles/${articleId}`), { method: "DELETE" }) await fetch(urlJoin(apiUrl, `articles/${articleId}`), { method: "DELETE" })
@@ -17,15 +21,18 @@ export default function ArticleControl({ articleId }: { articleId: string }) {
function editArticle() { function editArticle() {
router.push("/admin/editor/article/" + articleId); router.push("/admin/editor/article/" + articleId);
} }
if (session) {
return ( return (
<div> <div>
<button className="danger" onClick={deleteArticle}> <button className="danger" onClick={deleteArticle}>
Delete Delete
</button> </button>
<button className="warning" onClick={editArticle}> <button className="warning" onClick={editArticle}>
Edit Edit
</button> </button>
</div> </div>
); );
} else {
return <></>;
}
} }

View File

@@ -2,8 +2,11 @@ import React from "react";
import { apiUrl } from "@/global"; import { apiUrl } from "@/global";
import urlJoin from "url-join"; import urlJoin from "url-join";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
export default function CategoryControl({ categoryId }: { categoryId: string }) { export default function CategoryControl({ categoryId }: { categoryId: string }) {
const { data: session } = useSession();
const router = useRouter(); const router = useRouter();
async function deleteCategory() { async function deleteCategory() {
await fetch(urlJoin(apiUrl, `categories/${categoryId}`), { method: "DELETE" }) await fetch(urlJoin(apiUrl, `categories/${categoryId}`), { method: "DELETE" })
@@ -18,14 +21,18 @@ export default function CategoryControl({ categoryId }: { categoryId: string })
router.push("/admin/editor/category/" + categoryId); router.push("/admin/editor/category/" + categoryId);
} }
return ( if (session) {
<div> return (
<button className="danger" onClick={deleteCategory}> <div>
Delete <button className="danger" onClick={deleteCategory}>
</button> Delete
<button className="warning" onClick={editCategory}> </button>
Edit <button className="warning" onClick={editCategory}>
</button> Edit
</div> </button>
); </div>
);
} else {
return <></>;
}
} }

View File

@@ -123,7 +123,7 @@ export default function Nav() {
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z" /> <path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z" />
</svg> </svg>
</div> </div>
<input onInput={handleSearchInput} type="text" name="" id="" /> <input onChange={handleSearchInput} type="text" name="" id="" />
</div> </div>
<div className={styles.searchResults}> <div className={styles.searchResults}>
<div className={styles.content}> <div className={styles.content}>

297
package-lock.json generated
View File

@@ -9,13 +9,14 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@next/font": "13.1.6", "@next/font": "13.1.6",
"@prisma/client": "^4.9.0", "@prisma/client": "^4.10.0",
"@types/node": "18.11.19", "@types/node": "18.11.19",
"@types/react": "18.0.27", "@types/react": "18.0.27",
"@types/react-dom": "18.0.10", "@types/react-dom": "18.0.10",
"eslint": "8.33.0", "eslint": "8.33.0",
"eslint-config-next": "13.1.6", "eslint-config-next": "13.1.6",
"next": "13.1.6", "next": "13.1.6",
"next-auth": "^4.19.2",
"next-superjson-plugin": "^0.5.4", "next-superjson-plugin": "^0.5.4",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@@ -26,13 +27,14 @@
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-stringify": "^10.0.2", "remark-stringify": "^10.0.2",
"sass": "^1.58.0", "sass": "^1.58.0",
"swr": "^2.0.3",
"typescript": "4.9.5", "typescript": "4.9.5",
"url-join": "^5.0.0" "url-join": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react-select": "^5.0.1", "@types/react-select": "^5.0.1",
"@types/react-syntax-highlighter": "^15.5.6", "@types/react-syntax-highlighter": "^15.5.6",
"prisma": "^4.9.0" "prisma": "^4.10.0"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@@ -964,6 +966,14 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@panva/hkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.0.2.tgz",
"integrity": "sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/@pkgr/utils": { "node_modules/@pkgr/utils": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
@@ -984,12 +994,12 @@
} }
}, },
"node_modules/@prisma/client": { "node_modules/@prisma/client": {
"version": "4.9.0", "version": "4.10.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.10.0.tgz",
"integrity": "sha512-bz6QARw54sWcbyR1lLnF2QHvRW5R/Jxnbbmwh3u+969vUKXtBkXgSgjDA85nji31ZBlf7+FrHDy5x+5ydGyQDg==", "integrity": "sha512-sBmYb1S6SMKFIESaLMfKqWSalv3pH73cMCsFt9HslJvYjIIcKQCA6PDL2O4SZGWvc4JBef9cg5Gd7d9x3AtKjw==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@prisma/engines-version": "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5" "@prisma/engines-version": "4.10.0-84.ca7fcef713137fa11029d519a9780db130cca91d"
}, },
"engines": { "engines": {
"node": ">=14.17" "node": ">=14.17"
@@ -1004,16 +1014,16 @@
} }
}, },
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "4.9.0", "version": "4.10.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.10.0.tgz",
"integrity": "sha512-t1pt0Gsp+HcgPJrHFc+d/ZSAaKKWar2G/iakrE07yeKPNavDP3iVKPpfXP22OTCHZUWf7OelwKJxQgKAm5hkgw==", "integrity": "sha512-ZPPo7q+nQZdTlPFedS7mFXPE3oZ2kWtTh3GO4sku0XQ8ikLqEyinuTPJbQCw/8qel2xglIEQicsK6yI4Jgh20A==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true "hasInstallScript": true
}, },
"node_modules/@prisma/engines-version": { "node_modules/@prisma/engines-version": {
"version": "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5", "version": "4.10.0-84.ca7fcef713137fa11029d519a9780db130cca91d",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.10.0-84.ca7fcef713137fa11029d519a9780db130cca91d.tgz",
"integrity": "sha512-M16aibbxi/FhW7z1sJCX8u+0DriyQYY5AyeTH7plQm9MLnURoiyn3CZBqAyIoQ+Z1pS77usCIibYJWSgleBMBA==" "integrity": "sha512-UVpmVlvSaGfY4ue+hh8CTkIesbuXCFUfrr8zk//+u85WwkKfWMtt6nLB2tNSzR1YO8eAA8+HqNf8LM7mnXIq5w=="
}, },
"node_modules/@rushstack/eslint-patch": { "node_modules/@rushstack/eslint-patch": {
"version": "1.2.0", "version": "1.2.0",
@@ -1660,6 +1670,14 @@
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
}, },
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/copy-anything": { "node_modules/copy-anything": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.3.tgz", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.3.tgz",
@@ -3414,6 +3432,14 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
}, },
"node_modules/jose": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.11.4.tgz",
"integrity": "sha512-94FdcR8felat4vaTJyL/WVdtlWLlsnLMZP8v+A0Vru18K3bQ22vn7TtpVh3JlgBFNIlYOUlGqwp/MjRPOnIyCQ==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-sdsl": { "node_modules/js-sdsl": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
@@ -4474,6 +4500,33 @@
} }
} }
}, },
"node_modules/next-auth": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.19.2.tgz",
"integrity": "sha512-6V2YG3IJQVhgCAH7mvT3yopTW92gMdUrcwGX7NQ0dCreT/+axGua/JmVdarjec0C/oJukKpIYRgjMlV+L5ZQOQ==",
"dependencies": {
"@babel/runtime": "^7.16.3",
"@panva/hkdf": "^1.0.1",
"cookie": "^0.5.0",
"jose": "^4.9.3",
"oauth": "^0.9.15",
"openid-client": "^5.1.0",
"preact": "^10.6.3",
"preact-render-to-string": "^5.1.19",
"uuid": "^8.3.2"
},
"peerDependencies": {
"next": "^12.2.5 || ^13",
"nodemailer": "^6.6.5",
"react": "^17.0.2 || ^18",
"react-dom": "^17.0.2 || ^18"
},
"peerDependenciesMeta": {
"nodemailer": {
"optional": true
}
}
},
"node_modules/next-superjson-plugin": { "node_modules/next-superjson-plugin": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/next-superjson-plugin/-/next-superjson-plugin-0.5.4.tgz", "resolved": "https://registry.npmjs.org/next-superjson-plugin/-/next-superjson-plugin-0.5.4.tgz",
@@ -4500,6 +4553,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4508,6 +4566,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.12.3", "version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
@@ -4613,6 +4679,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/oidc-token-hash": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz",
"integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==",
"engines": {
"node": "^10.13.0 || >=12.0.0"
}
},
"node_modules/once": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -4637,6 +4711,20 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/openid-client": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.0.tgz",
"integrity": "sha512-hgJa2aQKcM2hn3eyVtN12tEA45ECjTJPXCgUh5YzTzy9qwapCvmDTVPWOcWVL0d34zeQoQ/hbG9lJhl3AYxJlQ==",
"dependencies": {
"jose": "^4.10.0",
"lru-cache": "^6.0.0",
"object-hash": "^2.0.1",
"oidc-token-hash": "^5.0.1"
},
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.1", "version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -4811,6 +4899,26 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/preact": {
"version": "10.12.0",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.0.tgz",
"integrity": "sha512-+w8ix+huD8CNZemheC53IPjMUFk921i02o30u0K6h53spMX41y/QhVDnG/nU2k42/69tvqWmVsgNLIiwRAcmxg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/preact-render-to-string": {
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
"dependencies": {
"pretty-format": "^3.8.0"
},
"peerDependencies": {
"preact": ">=10"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -4819,14 +4927,19 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/pretty-format": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
},
"node_modules/prisma": { "node_modules/prisma": {
"version": "4.9.0", "version": "4.10.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.9.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.10.0.tgz",
"integrity": "sha512-bS96oZ5oDFXYgoF2l7PJ3Mp1wWWfLOo8B/jAfbA2Pn0Wm5Z/owBHzaMQKS3i1CzVBDWWPVnOohmbJmjvkcHS5w==", "integrity": "sha512-xUHcF3Glc8QGgW8x0rfPITvyyTo04fskUdG7pI4kQbvDX/rhzDP4046x/FvazYqYHXMLR5/KTIi2p2Gth5vKOQ==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@prisma/engines": "4.9.0" "@prisma/engines": "4.10.0"
}, },
"bin": { "bin": {
"prisma": "build/index.js", "prisma": "build/index.js",
@@ -5491,6 +5604,20 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/swr": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.0.3.tgz",
"integrity": "sha512-sGvQDok/AHEWTPfhUWXEHBVEXmgGnuahyhmRQbjl9XBYxT/MSlAzvXEKQpyM++bMPaI52vcWS2HiKNaW7+9OFw==",
"dependencies": {
"use-sync-external-store": "^1.2.0"
},
"engines": {
"pnpm": "7"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/synckit": { "node_modules/synckit": {
"version": "0.8.5", "version": "0.8.5",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
@@ -5803,6 +5930,22 @@
} }
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/uvu": { "node_modules/uvu": {
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
@@ -6616,6 +6759,11 @@
"fastq": "^1.6.0" "fastq": "^1.6.0"
} }
}, },
"@panva/hkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.0.2.tgz",
"integrity": "sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA=="
},
"@pkgr/utils": { "@pkgr/utils": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
@@ -6630,23 +6778,23 @@
} }
}, },
"@prisma/client": { "@prisma/client": {
"version": "4.9.0", "version": "4.10.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.10.0.tgz",
"integrity": "sha512-bz6QARw54sWcbyR1lLnF2QHvRW5R/Jxnbbmwh3u+969vUKXtBkXgSgjDA85nji31ZBlf7+FrHDy5x+5ydGyQDg==", "integrity": "sha512-sBmYb1S6SMKFIESaLMfKqWSalv3pH73cMCsFt9HslJvYjIIcKQCA6PDL2O4SZGWvc4JBef9cg5Gd7d9x3AtKjw==",
"requires": { "requires": {
"@prisma/engines-version": "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5" "@prisma/engines-version": "4.10.0-84.ca7fcef713137fa11029d519a9780db130cca91d"
} }
}, },
"@prisma/engines": { "@prisma/engines": {
"version": "4.9.0", "version": "4.10.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.10.0.tgz",
"integrity": "sha512-t1pt0Gsp+HcgPJrHFc+d/ZSAaKKWar2G/iakrE07yeKPNavDP3iVKPpfXP22OTCHZUWf7OelwKJxQgKAm5hkgw==", "integrity": "sha512-ZPPo7q+nQZdTlPFedS7mFXPE3oZ2kWtTh3GO4sku0XQ8ikLqEyinuTPJbQCw/8qel2xglIEQicsK6yI4Jgh20A==",
"devOptional": true "devOptional": true
}, },
"@prisma/engines-version": { "@prisma/engines-version": {
"version": "4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5", "version": "4.10.0-84.ca7fcef713137fa11029d519a9780db130cca91d",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.10.0-84.ca7fcef713137fa11029d519a9780db130cca91d.tgz",
"integrity": "sha512-M16aibbxi/FhW7z1sJCX8u+0DriyQYY5AyeTH7plQm9MLnURoiyn3CZBqAyIoQ+Z1pS77usCIibYJWSgleBMBA==" "integrity": "sha512-UVpmVlvSaGfY4ue+hh8CTkIesbuXCFUfrr8zk//+u85WwkKfWMtt6nLB2tNSzR1YO8eAA8+HqNf8LM7mnXIq5w=="
}, },
"@rushstack/eslint-patch": { "@rushstack/eslint-patch": {
"version": "1.2.0", "version": "1.2.0",
@@ -7103,6 +7251,11 @@
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
}, },
"cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
},
"copy-anything": { "copy-anything": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.3.tgz", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.3.tgz",
@@ -8345,6 +8498,11 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
}, },
"jose": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.11.4.tgz",
"integrity": "sha512-94FdcR8felat4vaTJyL/WVdtlWLlsnLMZP8v+A0Vru18K3bQ22vn7TtpVh3JlgBFNIlYOUlGqwp/MjRPOnIyCQ=="
},
"js-sdsl": { "js-sdsl": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
@@ -9015,6 +9173,22 @@
"styled-jsx": "5.1.1" "styled-jsx": "5.1.1"
} }
}, },
"next-auth": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.19.2.tgz",
"integrity": "sha512-6V2YG3IJQVhgCAH7mvT3yopTW92gMdUrcwGX7NQ0dCreT/+axGua/JmVdarjec0C/oJukKpIYRgjMlV+L5ZQOQ==",
"requires": {
"@babel/runtime": "^7.16.3",
"@panva/hkdf": "^1.0.1",
"cookie": "^0.5.0",
"jose": "^4.9.3",
"oauth": "^0.9.15",
"openid-client": "^5.1.0",
"preact": "^10.6.3",
"preact-render-to-string": "^5.1.19",
"uuid": "^8.3.2"
}
},
"next-superjson-plugin": { "next-superjson-plugin": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/next-superjson-plugin/-/next-superjson-plugin-0.5.4.tgz", "resolved": "https://registry.npmjs.org/next-superjson-plugin/-/next-superjson-plugin-0.5.4.tgz",
@@ -9034,11 +9208,21 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
}, },
"oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
},
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
}, },
"object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="
},
"object-inspect": { "object-inspect": {
"version": "1.12.3", "version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
@@ -9108,6 +9292,11 @@
"es-abstract": "^1.20.4" "es-abstract": "^1.20.4"
} }
}, },
"oidc-token-hash": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz",
"integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ=="
},
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -9126,6 +9315,17 @@
"is-wsl": "^2.2.0" "is-wsl": "^2.2.0"
} }
}, },
"openid-client": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.0.tgz",
"integrity": "sha512-hgJa2aQKcM2hn3eyVtN12tEA45ECjTJPXCgUh5YzTzy9qwapCvmDTVPWOcWVL0d34zeQoQ/hbG9lJhl3AYxJlQ==",
"requires": {
"jose": "^4.10.0",
"lru-cache": "^6.0.0",
"object-hash": "^2.0.1",
"oidc-token-hash": "^5.0.1"
}
},
"optionator": { "optionator": {
"version": "0.9.1", "version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -9239,18 +9439,36 @@
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"preact": {
"version": "10.12.0",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.0.tgz",
"integrity": "sha512-+w8ix+huD8CNZemheC53IPjMUFk921i02o30u0K6h53spMX41y/QhVDnG/nU2k42/69tvqWmVsgNLIiwRAcmxg=="
},
"preact-render-to-string": {
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
"requires": {
"pretty-format": "^3.8.0"
}
},
"prelude-ls": { "prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
}, },
"pretty-format": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
},
"prisma": { "prisma": {
"version": "4.9.0", "version": "4.10.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.9.0.tgz", "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.10.0.tgz",
"integrity": "sha512-bS96oZ5oDFXYgoF2l7PJ3Mp1wWWfLOo8B/jAfbA2Pn0Wm5Z/owBHzaMQKS3i1CzVBDWWPVnOohmbJmjvkcHS5w==", "integrity": "sha512-xUHcF3Glc8QGgW8x0rfPITvyyTo04fskUdG7pI4kQbvDX/rhzDP4046x/FvazYqYHXMLR5/KTIi2p2Gth5vKOQ==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"@prisma/engines": "4.9.0" "@prisma/engines": "4.10.0"
} }
}, },
"prismjs": { "prismjs": {
@@ -9698,6 +9916,14 @@
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
}, },
"swr": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.0.3.tgz",
"integrity": "sha512-sGvQDok/AHEWTPfhUWXEHBVEXmgGnuahyhmRQbjl9XBYxT/MSlAzvXEKQpyM++bMPaI52vcWS2HiKNaW7+9OFw==",
"requires": {
"use-sync-external-store": "^1.2.0"
}
},
"synckit": { "synckit": {
"version": "0.8.5", "version": "0.8.5",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
@@ -9907,6 +10133,17 @@
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
"requires": {} "requires": {}
}, },
"use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"requires": {}
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"uvu": { "uvu": {
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",

View File

@@ -10,13 +10,14 @@
}, },
"dependencies": { "dependencies": {
"@next/font": "13.1.6", "@next/font": "13.1.6",
"@prisma/client": "^4.9.0", "@prisma/client": "^4.10.0",
"@types/node": "18.11.19", "@types/node": "18.11.19",
"@types/react": "18.0.27", "@types/react": "18.0.27",
"@types/react-dom": "18.0.10", "@types/react-dom": "18.0.10",
"eslint": "8.33.0", "eslint": "8.33.0",
"eslint-config-next": "13.1.6", "eslint-config-next": "13.1.6",
"next": "13.1.6", "next": "13.1.6",
"next-auth": "^4.19.2",
"next-superjson-plugin": "^0.5.4", "next-superjson-plugin": "^0.5.4",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@@ -27,12 +28,13 @@
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-stringify": "^10.0.2", "remark-stringify": "^10.0.2",
"sass": "^1.58.0", "sass": "^1.58.0",
"swr": "^2.0.3",
"typescript": "4.9.5", "typescript": "4.9.5",
"url-join": "^5.0.0" "url-join": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react-select": "^5.0.1", "@types/react-select": "^5.0.1",
"@types/react-syntax-highlighter": "^15.5.6", "@types/react-syntax-highlighter": "^15.5.6",
"prisma": "^4.9.0" "prisma": "^4.10.0"
} }
} }

View File

@@ -8,16 +8,17 @@ import Footer from "@/components/Footer";
import Nav from "@/components/Nav"; import Nav from "@/components/Nav";
import { Category } from "@prisma/client"; import { Category } from "@prisma/client";
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { SessionProvider } from "next-auth/react";
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return ( return (
<> <SessionProvider session={session}>
<header> <header>
<Nav /> <Nav />
<AdminNav /> <AdminNav />
</header> </header>
<Component {...pageProps} /> <Component {...pageProps} />
<Footer /> <Footer />
</> </SessionProvider>
); );
} }

View File

@@ -13,9 +13,13 @@ import { apiUrl } from "@/global";
import Markdown from "@/components/Markdown"; import Markdown from "@/components/Markdown";
import prisma, { ArticleWithIncludes, CategoryWithIncludes } from "@/lib/prisma"; import prisma, { ArticleWithIncludes, CategoryWithIncludes } from "@/lib/prisma";
import { CLIENT_RENEG_LIMIT } from "tls"; import { CLIENT_RENEG_LIMIT } from "tls";
import { useSession } from "next-auth/react";
export default function AdminArticlesEditorPage({ article, categories }: { article: ArticleWithIncludes | null; categories: CategoryWithIncludes[] }) { export default function AdminArticlesEditorPage({ article, categories }: { article: ArticleWithIncludes | null; categories: CategoryWithIncludes[] }) {
const router = useRouter(); const router = useRouter();
const { status } = useSession({
required: true,
});
const [title, setTitle] = useState<string>(article?.title ?? ""); const [title, setTitle] = useState<string>(article?.title ?? "");
const [selectCategoriesOptions, setSelectCategoriesOptions] = useState<{ value: string; label: string }[]>( const [selectCategoriesOptions, setSelectCategoriesOptions] = useState<{ value: string; label: string }[]>(
@@ -116,132 +120,136 @@ export default function AdminArticlesEditorPage({ article, categories }: { artic
.catch(console.error); .catch(console.error);
} }
return ( if (status === "authenticated") {
<div className={styles.adminArticlesCreate}> return (
<h1>{article ? "Update article" : "Create new article"}</h1> <div className={styles.adminArticlesCreate}>
<div className={styles.formControl}> <h1>{article ? "Update article" : "Create new article"}</h1>
<p className="text-error" ref={errorTextRef}></p> <div className={styles.formControl}>
<button <p className="text-error" ref={errorTextRef}></p>
type="button" <button
onClick={() => { type="button"
if (article) { onClick={() => {
updateArticle(); if (article) {
} else { updateArticle();
createArticle(); } else {
} createArticle();
}} }
> }}
{article ? "Update article" : "Create article"} >
</button> {article ? "Update article" : "Create article"}
</div> </button>
</div>
<div className={styles.form}> <div className={styles.form}>
<div className={styles.articleEditor}> <div className={styles.articleEditor}>
<div className={styles.title}> <div className={styles.title}>
<label htmlFor="title">Title</label> <label htmlFor="title">Title</label>
<div className={styles.titleInputs}> <div className={styles.titleInputs}>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
}}
className={!isValidText(title) && title ? "error" : ""}
type="text"
name="title"
placeholder="title"
ref={titleRef}
defaultValue={title}
/>
<input readOnly={true} className={""} type="text" name="name" value={title ? formatTextToUrlName(title) : ""} />
</div>
</div>
<div className={styles.category}>
<label htmlFor="title">Category</label>
<Select
ref={categorySelectRef}
className="react-select-container"
classNamePrefix="react-select"
options={selectCategoriesOptions}
defaultValue={article ? { value: article.category.id, label: article.category.title } : {}}
/>
</div>
<div className={styles.introduction}>
<label htmlFor="title">Introduction</label>
<input <input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value); setIntroduction(e.target.value);
}} }}
className={!isValidText(title) && title ? "error" : ""} className={!isValidText(introduction) && introduction ? "error" : ""}
type="text" type="text"
name="title" name="introduction"
placeholder="title" placeholder="Introduction"
ref={titleRef} ref={introductionRef}
defaultValue={title} defaultValue={introduction}
/> />
<input readOnly={true} className={""} type="text" name="name" value={title ? formatTextToUrlName(title) : ""} />
</div> </div>
</div> <div className={styles.markdown}>
<div className={styles.category}> <label htmlFor="">Markdown Editor</label>
<label htmlFor="title">Category</label> <div className={styles.markdownEditor}>
<Select <textarea
ref={categorySelectRef} ref={markdownTextAreaRef}
className="react-select-container" onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
classNamePrefix="react-select" setMarkdown(e.target.value);
options={selectCategoriesOptions}
defaultValue={article ? { value: article.category.id, label: article.category.title } : {}}
/>
</div>
<div className={styles.introduction}>
<label htmlFor="title">Introduction</label>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIntroduction(e.target.value);
}}
className={!isValidText(introduction) && introduction ? "error" : ""}
type="text"
name="introduction"
placeholder="Introduction"
ref={introductionRef}
defaultValue={introduction}
/>
</div>
<div className={styles.markdown}>
<label htmlFor="">Markdown Editor</label>
<div className={styles.markdownEditor}>
<textarea
ref={markdownTextAreaRef}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMarkdown(e.target.value);
}}
defaultValue={markdown}
></textarea>
<Markdown value={markdown} />
</div>
</div>
<div className={styles.contentTable}>
<label htmlFor="">Table of contents</label>
<div className={styles.contentTableEditor}>
<div className={styles.entries}>
{contentTable?.map((entry: IContentTableEntry, i: number) => {
return (
<div key={i}>
<input
onChange={(e) => {
changeContentTableEntryAnchor(i, e.target.value);
}}
type="text"
placeholder={"Anchor"}
defaultValue={entry.anchor}
/>
<input
onChange={(e) => {
changeContentTableEntryTitle(i, e.target.value);
}}
type="text"
placeholder={"Title"}
defaultValue={entry.title}
/>{" "}
<button
onClick={() => {
removeEntry(i);
}}
>
Remove
</button>
</div>
);
})}
<button
onClick={() => {
setContentTable([...contentTable, { title: "", anchor: "" }]);
}} }}
> defaultValue={markdown}
Add ></textarea>
</button> <Markdown value={markdown} />
</div>
</div>
<div className={styles.contentTable}>
<label htmlFor="">Table of contents</label>
<div className={styles.contentTableEditor}>
<div className={styles.entries}>
{contentTable?.map((entry: IContentTableEntry, i: number) => {
return (
<div key={i}>
<input
onChange={(e) => {
changeContentTableEntryAnchor(i, e.target.value);
}}
type="text"
placeholder={"Anchor"}
defaultValue={entry.anchor}
/>
<input
onChange={(e) => {
changeContentTableEntryTitle(i, e.target.value);
}}
type="text"
placeholder={"Title"}
defaultValue={entry.title}
/>{" "}
<button
onClick={() => {
removeEntry(i);
}}
>
Remove
</button>
</div>
);
})}
<button
onClick={() => {
setContentTable([...contentTable, { title: "", anchor: "" }]);
}}
>
Add
</button>
</div>
<Markdown value={markdown} />
</div> </div>
<Markdown value={markdown} />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> );
); } else {
return <></>;
}
} }
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {

View File

@@ -9,8 +9,13 @@ import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { apiUrl } from "@/global"; import { apiUrl } from "@/global";
import prisma, { CategoryWithIncludes } from "@/lib/prisma"; import prisma, { CategoryWithIncludes } from "@/lib/prisma";
import { useSession } from "next-auth/react";
export default function AdminCategoriesEditor({ category }: { category: CategoryWithIncludes | null }) { export default function AdminCategoriesEditor({ category }: { category: CategoryWithIncludes | null }) {
const { status } = useSession({
required: true,
});
const router = useRouter(); const router = useRouter();
const [title, setTitle] = useState<string>(category?.title ?? ""); const [title, setTitle] = useState<string>(category?.title ?? "");
const [color, setColor] = useState<string>(category?.color ?? ""); const [color, setColor] = useState<string>(category?.color ?? "");
@@ -85,80 +90,84 @@ export default function AdminCategoriesEditor({ category }: { category: Category
.catch(console.error); .catch(console.error);
} }
return ( if (status === "authenticated") {
<div className={styles.categoryEditor}> return (
<h1>{category ? "Update category" : "Create new category"}</h1> <div className={styles.categoryEditor}>
<div className={styles.formControl}> <h1>{category ? "Update category" : "Create new category"}</h1>
<p className="text-error" ref={errorTextRef}></p> <div className={styles.formControl}>
<button <p className="text-error" ref={errorTextRef}></p>
type="button" <button
onClick={() => { type="button"
if (category) { onClick={() => {
updateCategory(); if (category) {
} else { updateCategory();
createCategory(); } else {
} createCategory();
}} }
> }}
{category ? "Update category" : "Create category"} >
</button> {category ? "Update category" : "Create category"}
</div> </button>
<div className={styles.form}> </div>
<div className={styles.title}> <div className={styles.form}>
<label htmlFor="title">Title</label> <div className={styles.title}>
<div className={styles.titleInputs}> <label htmlFor="title">Title</label>
<input <div className={styles.titleInputs}>
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
}}
className={!isValidText(title) && title ? "error" : ""}
type="text"
name="title"
placeholder="title"
ref={titleRef}
defaultValue={title}
/>
<input readOnly={true} className={""} type="text" name="name" value={title ? formatTextToUrlName(title) : ""} />
</div>
<div className={styles.svg}>
<label>SVG</label>
<div className={styles.svgInputs}>
<input <input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSvgPath(e.target.value); setTitle(e.target.value);
}} }}
className={!isValidText(title) && title ? "error" : ""}
type="text" type="text"
placeholder="svg path" name="title"
ref={svgPathRef} placeholder="title"
defaultValue={svgPath} ref={titleRef}
defaultValue={title}
/> />
<input readOnly={true} className={""} type="text" name="name" value={title ? formatTextToUrlName(title) : ""} />
</div>
<div className={styles.svg}>
<label>SVG</label>
<div className={styles.svgInputs}>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSvgPath(e.target.value);
}}
type="text"
placeholder="svg path"
ref={svgPathRef}
defaultValue={svgPath}
/>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSvgViewbox(e.target.value);
}}
type="text"
placeholder="0 0 512 512"
ref={svgViewboxRef}
defaultValue={svgViewbox}
/>
</div>
</div>
<div className={styles.color}>
<label>Color</label>
<input <input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSvgViewbox(e.target.value); setColor(e.target.value);
}} }}
type="text" type="color"
placeholder="0 0 512 512" ref={colorRef}
ref={svgViewboxRef} defaultValue={color}
defaultValue={svgViewbox}
/> />
</div> </div>
</div> </div>
<div className={styles.color}>
<label>Color</label>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setColor(e.target.value);
}}
type="color"
ref={colorRef}
defaultValue={color}
/>
</div>
</div> </div>
</div> </div>
</div> );
); } else {
return <></>;
}
} }
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {

View File

@@ -6,66 +6,72 @@ import prisma, { ArticleWithIncludes } from "../../../lib/prisma";
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import { UpdateArticle } from "../../../types/api"; import { UpdateArticle } from "../../../types/api";
import { isValidText } from "../../../validators"; import { isValidText } from "../../../validators";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../auth/[...nextauth]";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const articleId: string = formatTextToUrlName(req.query?.articleId?.toString() ?? "") const session = await getServerSession(req, res, authOptions);
if (session) {
if (req.method == "PUT") {//* PUT const articleId: string = formatTextToUrlName(req.query?.articleId?.toString() ?? "")
console.log("PUT") if (req.method == "PUT") {//* PUT
console.log("PUT")
console.log(`API articleId: ${articleId}`) console.log(`API articleId: ${articleId}`)
const articleData: UpdateArticle = req.body; const articleData: UpdateArticle = req.body;
if (articleData.title && !isValidText(articleData.title)) { if (articleData.title && !isValidText(articleData.title)) {
res.json({ target: "title", error: "Not a valid title" }); res.json({ target: "title", error: "Not a valid title" });
return; return;
}
if (articleData.introduction && !isValidText(articleData.introduction)) {
res.json({ target: "introduction", error: "Not a valid introduction" });
return;
}
const newArticle: Prisma.ArticleUncheckedUpdateInput = {
title: articleData.title ?? undefined,
name: articleData.title ? formatTextToUrlName(articleData.title) : undefined,
introduction: articleData.introduction ?? undefined,
categoryId: articleData.categoryId ?? undefined,
contentTable: articleData.contentTable ?? undefined,
markdown: articleData.markdown ?? undefined,
imageUrl: articleData.imageUrl ?? undefined,
}
console.log(newArticle)
await prisma.article.update({ data: newArticle, where: { id: articleId }, include: { category: true } })
.then(
(data) => {
res.json({ success: true, data: data });
},
(errorReason) => {
console.log(errorReason)
if (errorReason.code === "P2002") {
res.json({ target: errorReason.meta.target[0], error: "Already exists" });
}
}
)
.catch((err) => {
console.error(err);
res.status(500).end();
});
} else if (req.method == "DELETE") {
console.log("DELETE article")
prisma.article.delete({ where: { id: articleId }, include: { category: true } }).then((result: ArticleWithIncludes | null) => {
if (result) {
res.status(200).json(result)
} else {
res.status(500).json({ error: true, message: "No article found" })
} }
}, (err) => {
console.log(err) if (articleData.introduction && !isValidText(articleData.introduction)) {
res.status(500).end(err) res.json({ target: "introduction", error: "Not a valid introduction" });
}) return;
}
const newArticle: Prisma.ArticleUncheckedUpdateInput = {
title: articleData.title ?? undefined,
name: articleData.title ? formatTextToUrlName(articleData.title) : undefined,
introduction: articleData.introduction ?? undefined,
categoryId: articleData.categoryId ?? undefined,
contentTable: articleData.contentTable ?? undefined,
markdown: articleData.markdown ?? undefined,
imageUrl: articleData.imageUrl ?? undefined,
}
console.log(newArticle)
await prisma.article.update({ data: newArticle, where: { id: articleId }, include: { category: true } })
.then(
(data) => {
res.json({ success: true, data: data });
},
(errorReason) => {
console.log(errorReason)
if (errorReason.code === "P2002") {
res.json({ target: errorReason.meta.target[0], error: "Already exists" });
}
}
)
.catch((err) => {
console.error(err);
res.status(500).end();
});
} else if (req.method == "DELETE") {
console.log("DELETE article")
prisma.article.delete({ where: { id: articleId }, include: { category: true } }).then((result: ArticleWithIncludes | null) => {
if (result) {
res.status(200).json(result)
} else {
res.status(500).json({ error: true, message: "No article found" })
}
}, (err) => {
console.log(err)
res.status(500).end(err)
})
}
} else {
res.status(403).json({ error: true, message: "Authorization Required" });
} }
} }

View File

@@ -6,56 +6,64 @@ import { isValidText } from "../../../validators";
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { getServerSession } from 'next-auth';
import { authOptions } from "../auth/[...nextauth]";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method == "POST") { //* POST const session = await getServerSession(req, res, authOptions);
console.log("API new article") if (session) {
const articleData: CreateArticle = req.body if (req.method == "POST") { //* POST
console.log(articleData) console.log("API new article")
const articleData: CreateArticle = req.body
console.log(articleData)
if (!isValidText(articleData.title)) { if (!isValidText(articleData.title)) {
res.status(500).json({ target: "title", error: "Not a valid title" }); res.status(500).json({ target: "title", error: "Not a valid title" });
return; return;
} }
if (!isValidText(articleData.introduction)) { if (!isValidText(articleData.introduction)) {
res.status(500).json({ target: "introduction", error: "Not a valid introduction" }); res.status(500).json({ target: "introduction", error: "Not a valid introduction" });
return; return;
} }
if (!articleData.categoryId) { if (!articleData.categoryId) {
res.status(500).json({ target: "category", error: "Category is required" }); res.status(500).json({ target: "category", error: "Category is required" });
return; return;
} }
const newArticle: Prisma.ArticleUncheckedCreateInput = { const newArticle: Prisma.ArticleUncheckedCreateInput = {
title: articleData.title, title: articleData.title,
name: formatTextToUrlName(articleData.title), name: formatTextToUrlName(articleData.title),
introduction: articleData.introduction, introduction: articleData.introduction,
categoryId: articleData.categoryId, categoryId: articleData.categoryId,
markdown: articleData.markdown ?? "", markdown: articleData.markdown ?? "",
contentTable: articleData.contentTable ?? {}, contentTable: articleData.contentTable ?? {},
imageUrl: articleData.imageUrl ?? "" imageUrl: articleData.imageUrl ?? ""
} }
prisma.article prisma.article
.create({ data: newArticle, include: { category: true } }) .create({ data: newArticle, include: { category: true } })
.then( .then(
(data: ArticleWithIncludes) => { (data: ArticleWithIncludes) => {
res.json({ success: true, data: data }); res.json({ success: true, data: data });
}, },
(errorReason) => { (errorReason) => {
console.log("reason", errorReason) console.log("reason", errorReason)
if (errorReason.code === "P2002") { if (errorReason.code === "P2002") {
res.json({ target: errorReason.meta.target[0], error: "Already exists" }); res.json({ target: errorReason.meta.target[0], error: "Already exists" });
}
} }
} )
) .catch((err) => {
.catch((err) => { console.error(err);
console.error(err); res.status(500).end();
res.status(500).end(); });
}); }
} else {
res.status(403).json({ error: true, message: "Authorization Required" });
} }
} }

View File

@@ -0,0 +1,30 @@
import NextAuth, { AuthOptions } from "next-auth"
import GithubProvider from "next-auth/providers/github"
export const authOptions: AuthOptions = {
// Configure one or more authentication providers
providers: [
GithubProvider({
clientId: "1afc604704e6ac0149e3", //! env vars
clientSecret: "b8f76990fc0a9181eaba23359a27b2d140ab67e7", //! env vars
}),
// ...add more providers here
], callbacks: {
async signIn({ user, account, profile, email, credentials }) {
if (user.id.toString() == "76851529") { //! env vars
return true
} else {
// Return false to display a default error message
return false
// Or you can return a URL to redirect to:
// return '/unauthorized'
}
}
},
secret: "@AWeFkHpv!jzVr^a9nRXS8^PcRFnDaLvt65mJb&*C^pcCgpbHFzzKN",
}
export default NextAuth(authOptions)

View File

@@ -7,76 +7,83 @@ import type { NextApiRequest, NextApiResponse } from 'next'
import { formatTextToUrlName } from "../../../utils"; import { formatTextToUrlName } from "../../../utils";
import { isValidText } from "../../../validators"; import { isValidText } from "../../../validators";
import { UpdateCategory } from '../../../types/api'; import { UpdateCategory } from '../../../types/api';
import { getServerSession } from "next-auth";
import { authOptions } from "../auth/[...nextauth]";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const categoryId: string = req.query.categoryId?.toString() ?? ""; const categoryId: string = req.query.categoryId?.toString() ?? "";
if (req.method == "PUT") { const session = await getServerSession(req, res, authOptions);
if (session) {
if (req.method == "PUT") {
const categoryData: UpdateCategory = req.body; const categoryData: UpdateCategory = req.body;
if (categoryData.title && !isValidText(categoryData.title)) { if (categoryData.title && !isValidText(categoryData.title)) {
res.json({ target: "title", error: "Not a valid title" }); res.json({ target: "title", error: "Not a valid title" });
return; return;
}
const newSvg: Prisma.SvgUncheckedUpdateInput = {
viewbox: categoryData.svg?.viewbox ?? undefined,
path: categoryData.svg?.path ?? undefined,
};
const newCategory: Prisma.CategoryUncheckedUpdateInput = {
title: categoryData.title ?? undefined,
name: categoryData.title ? formatTextToUrlName(categoryData.title) : undefined,
color: categoryData.color ?? undefined,
};
await prisma.category
.update({
data: newCategory,
where: { id: categoryId },
include: { svg: true },
})
.then(
async (categoryData) => {
await prisma.svg
.update({ data: newSvg, where: { id: categoryData.svg.id } })
.then(
(svgData) => {
console.log("3");
res.json({ success: true, data: categoryData });
},
(errorReason) => {
res.status(500).end(errorReason);
}
)
.catch((err) => {
console.error(err);
res.status(500).end();
});
},
(errorReason) => {
console.log(errorReason);
if (errorReason.code === "P2002") {
res.json({ target: errorReason.meta.target[0], error: "Already exists" });
}
}
)
.catch((err) => {
console.error(err);
res.status(500).end();
});
} else if (req.method == "DELETE") {
console.log("DELETE category")
prisma.category.delete({ where: { id: categoryId }, include: { articles: true, svg: true } }).then((result: CategoryWithIncludes | null) => {
if (result) {
res.status(200).json(result)
} else {
res.status(500).json({ error: true, message: "No category found" })
} }
}, (err) => {
console.log(err) const newSvg: Prisma.SvgUncheckedUpdateInput = {
res.status(500).end(err) viewbox: categoryData.svg?.viewbox ?? undefined,
}) path: categoryData.svg?.path ?? undefined,
};
const newCategory: Prisma.CategoryUncheckedUpdateInput = {
title: categoryData.title ?? undefined,
name: categoryData.title ? formatTextToUrlName(categoryData.title) : undefined,
color: categoryData.color ?? undefined,
};
await prisma.category
.update({
data: newCategory,
where: { id: categoryId },
include: { svg: true },
})
.then(
async (categoryData) => {
await prisma.svg
.update({ data: newSvg, where: { id: categoryData.svg.id } })
.then(
(svgData) => {
console.log("3");
res.json({ success: true, data: categoryData });
},
(errorReason) => {
res.status(500).end(errorReason);
}
)
.catch((err) => {
console.error(err);
res.status(500).end();
});
},
(errorReason) => {
console.log(errorReason);
if (errorReason.code === "P2002") {
res.json({ target: errorReason.meta.target[0], error: "Already exists" });
}
}
)
.catch((err) => {
console.error(err);
res.status(500).end();
});
} else if (req.method == "DELETE") {
console.log("DELETE category")
prisma.category.delete({ where: { id: categoryId }, include: { articles: true, svg: true } }).then((result: CategoryWithIncludes | null) => {
if (result) {
res.status(200).json(result)
} else {
res.status(500).json({ error: true, message: "No category found" })
}
}, (err) => {
console.log(err)
res.status(500).end(err)
})
}
} else {
res.status(403).json({ error: true, message: "Authorization Required" });
} }
} }

View File

@@ -8,8 +8,12 @@ import { isValidText } from "../../../validators";
import type { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import { CreateCategory } from "@/types/api"; import { CreateCategory } from "@/types/api";
import { getServerSession } from "next-auth";
import { authOptions } from "../auth/[...nextauth]";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (req.method == "GET") { if (req.method == "GET") {
console.log("API get categories") console.log("API get categories")
await prisma.category.findMany().then((result: Category[]) => { await prisma.category.findMany().then((result: Category[]) => {
@@ -22,65 +26,69 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}) })
} else } else
if (req.method == "POST") { if (req.method == "POST") {
console.log("API new category") if (session) {
const categoryData: CreateCategory = req.body; console.log("API new category")
console.log(categoryData) const categoryData: CreateCategory = req.body;
console.log(categoryData)
if (!isValidText(categoryData.title)) { if (!isValidText(categoryData.title)) {
res.json({ target: "title", error: "Not a valid title" }); res.json({ target: "title", error: "Not a valid title" });
return; return;
} }
categoryData.svg.viewbox = categoryData.svg.viewbox.length > 1 ? categoryData.svg.viewbox : ""; categoryData.svg.viewbox = categoryData.svg.viewbox.length > 1 ? categoryData.svg.viewbox : "";
const newSvg: Prisma.SvgUncheckedCreateInput = { const newSvg: Prisma.SvgUncheckedCreateInput = {
viewbox: categoryData.svg.viewbox, viewbox: categoryData.svg.viewbox,
path: categoryData.svg.path path: categoryData.svg.path
} }
await prisma.svg await prisma.svg
.create({ data: newSvg }) .create({ data: newSvg })
.then( .then(
async (createdSvg: Svg) => { async (createdSvg: Svg) => {
const newCategory: Prisma.CategoryUncheckedCreateInput = { const newCategory: Prisma.CategoryUncheckedCreateInput = {
title: categoryData.title, title: categoryData.title,
name: formatTextToUrlName(categoryData.title), name: formatTextToUrlName(categoryData.title),
color: categoryData.color ?? "teal", color: categoryData.color ?? "teal",
svgId: createdSvg.id, svgId: createdSvg.id,
}
await prisma.category
.create({
data: newCategory,
include: { svg: true, articles: true },
})
.then(
(createdCategory: CategoryWithIncludes | null) => {
if (createdCategory) {
res.json({ success: true, data: createdCategory });
} else {
res.json({ error: true, message: "Could not create category" });
}
},
(errorReason) => {
console.log(errorReason)
if (errorReason.code === "P2002") {
res.json({ target: errorReason.meta.target[0], error: "Already exists" });
}
}
)
.catch((err) => {
console.error(err);
res.status(500).end();
});
},
(errorReason) => {
res.status(500).end(errorReason);
} }
)
await prisma.category .catch((err) => {
.create({ console.error(err);
data: newCategory, res.status(500).end();
include: { svg: true, articles: true }, });
}) } else {
.then( res.status(403).json({ error: true, message: "Authorization Required" });
(createdCategory: CategoryWithIncludes | null) => { }
if (createdCategory) {
res.json({ success: true, data: createdCategory });
} else {
res.json({ error: true, message: "Could not create category" });
}
},
(errorReason) => {
console.log(errorReason)
if (errorReason.code === "P2002") {
res.json({ target: errorReason.meta.target[0], error: "Already exists" });
}
}
)
.catch((err) => {
console.error(err);
res.status(500).end();
});
},
(errorReason) => {
res.status(500).end(errorReason);
}
)
.catch((err) => {
console.error(err);
res.status(500).end();
});
} }
} }

View File

@@ -8,13 +8,14 @@ import prisma, { ArticleWithIncludes, CategoryWithIncludes } from "@/lib/prisma"
import ArticleControl from "../../../../components/ArticleControl"; import ArticleControl from "../../../../components/ArticleControl";
import { IContentTableEntry } from "@/types/contentTable"; import { IContentTableEntry } from "@/types/contentTable";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { useSession, signIn, signOut } from "next-auth/react";
import { CLIENT_RENEG_LIMIT } from "tls";
//* MAIN //* MAIN
export default function ArticlePage({ article }: { article: ArticleWithIncludes }) { export default function ArticlePage({ article }: { article: ArticleWithIncludes }) {
const dateUpdated: Date = new Date(article?.dateUpdated); const dateUpdated: Date = new Date(article?.dateUpdated);
const dateCreated: Date = new Date(article?.dateCreated); const dateCreated: Date = new Date(article?.dateCreated);
const dateOptions: Intl.DateTimeFormatOptions = { month: "long", day: "numeric", year: "numeric" }; const dateOptions: Intl.DateTimeFormatOptions = { month: "long", day: "numeric", year: "numeric" };
return ( return (
<> <>
<ArticleControl articleId={article.id} /> <ArticleControl articleId={article.id} />

View File

@@ -27,7 +27,7 @@ model Category {
title String @unique title String @unique
color String color String
svgId String svgId String
svg Svg @relation(fields: [svgId], references: [id]) svg Svg @relation(fields: [svgId], references: [id], onDelete: Cascade)
articles Article[] articles Article[]
dateCreated DateTime @default(now()) dateCreated DateTime @default(now())
dateUpdated DateTime @default(now()) dateUpdated DateTime @default(now())