This commit is contained in:
Janis
2023-01-22 17:04:42 +01:00
parent bead72cde7
commit 2c0207dc65
21 changed files with 338 additions and 93 deletions

View File

@@ -10,6 +10,20 @@ import remarkGemoji from "remark-gemoji";
import remarkStringify from "remark-stringify"; import remarkStringify from "remark-stringify";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useLocalStorage } from "usehooks-ts"; import { useLocalStorage } from "usehooks-ts";
import React from "react";
import Head from "./head";
import { formatTextToUrlName } from "../utils";
function flatten(text, child) {
return typeof child === "string" ? text + child : React.Children.toArray(child.props.children).reduce(flatten, text);
}
function HeadingRenderer({ children, level }) {
children = React.Children.toArray(children);
const text = children.reduce(flatten, "");
return React.createElement("h" + level, { id: formatTextToUrlName(text) }, children);
}
export default function Markdown({ value }: { value: any }) { export default function Markdown({ value }: { value: any }) {
return ( return (
<div> <div>
@@ -18,6 +32,12 @@ export default function Markdown({ value }: { value: any }) {
//@ts-ignore //@ts-ignore
remarkPlugins={[remarkGfm, remarkGemoji, remarkStringify]} remarkPlugins={[remarkGfm, remarkGemoji, remarkStringify]}
components={{ components={{
h1: HeadingRenderer,
h2: HeadingRenderer,
h3: HeadingRenderer,
h4: HeadingRenderer,
h5: HeadingRenderer,
h6: HeadingRenderer,
code({ node, inline, className, children, ...props }) { code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || ""); const match = /language-(\w+)/.exec(className || "");
return !inline ? ( return !inline ? (

View File

@@ -6,39 +6,68 @@ import { useState, useRef, useEffect } from "react";
import styles from "../../../../styles/modules/AdminArticlesCreate.module.scss"; import styles from "../../../../styles/modules/AdminArticlesCreate.module.scss";
import { PostArticle } from "../../../../types/postData"; import { PostArticle } from "../../../../types/postData";
import Markdown from "../../../Markdown"; import Markdown from "../../../Markdown";
import { Category } from "@prisma/client"; import { Article, Category, Prisma } from "@prisma/client";
import "../../../../styles/inputs.scss"; import "../../../../styles/inputs.scss";
import "../../../../styles/buttons.scss";
import Select, { GroupBase, OptionsOrGroups } from "react-select"; import Select, { GroupBase, OptionsOrGroups } from "react-select";
import { apiUrl } from "../../../global"; import { apiUrl } from "../../../global";
import urlJoin from "url-join"; import urlJoin from "url-join";
import { getUrlSafeString } from "../../../utils"; import { formatTextToUrlName } from "../../../../utils";
import { isValidText } from "../../../../validators";
import { useRouter } from "next/navigation";
import ContentTable from "../../../articles/[categoryName]/[articleName]/ContentTable";
import { IContentTableEntry } from "../../../../types/contentTable";
type ArticleWithCategory = Prisma.ArticleGetPayload<{ include: { category: true } }>;
export default function AdminArticlesCreate() { export default function AdminArticlesCreate() {
const [formData, setFormData] = useState<PostArticle>(null); const router = useRouter();
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [markdown, setMarkdown] = useState<string>("");
const [selectCategoriesOptions, setSelectCategoriesOptions] = useState<any>([]); const [selectCategoriesOptions, setSelectCategoriesOptions] = useState<any>([]);
const [introduction, setIntroduction] = useState<string>("");
const [markdown, setMarkdown] = useState<string>("");
const [contentTable, setContentTable] = useState<IContentTableEntry[]>([]);
const titleRef = useRef<HTMLInputElement>(null); const titleRef = useRef<HTMLInputElement>(null);
const categorySelectRef = useRef(null); const categorySelectRef = useRef(null);
const introductionRef = useRef<HTMLInputElement>(null); const introductionRef = useRef<HTMLInputElement>(null);
const markdownTextAreaRef = useRef<HTMLTextAreaElement>(null); const markdownTextAreaRef = useRef<HTMLTextAreaElement>(null);
const errorTextRef = useRef(null);
function changeContentTableEntryAnchor(index: number, newAnchor: string) {
setContentTable((prevArray) => {
let newArray = [...prevArray];
newArray[index].anchor = newAnchor;
return newArray;
});
}
function changeContentTableEntryTitle(index: number, newTitle: string) {
setContentTable((prevArray) => {
let newArray = [...prevArray];
newArray[index].anchor = newTitle;
return newArray;
});
}
function handleFormChange() { function handleFormChange() {
setMarkdown(markdownTextAreaRef.current.value); setMarkdown(markdownTextAreaRef.current.value);
setTitle(titleRef.current.value); setTitle(titleRef.current.value);
setFormData({ setIntroduction(introductionRef.current.value);
name: getUrlSafeString(titleRef.current.value), }
async function postData() {
const formData: PostArticle = {
title: titleRef.current.value, title: titleRef.current.value,
introduction: introductionRef.current.value, introduction: introductionRef.current.value,
markdown: markdown, markdown: markdown,
categoryId: Number(categorySelectRef?.current?.getValue()[0]?.value), categoryId: Number(categorySelectRef?.current?.getValue()[0]?.value),
}); contentTable: contentTable,
} };
async function postData() {
console.log(formData); console.log(formData);
await fetch("/api/articles/create", { const result = await fetch("/api/articles/create", {
method: "POST", method: "POST",
headers: { headers: {
Accept: "application/json", Accept: "application/json",
@@ -46,6 +75,14 @@ export default function AdminArticlesCreate() {
}, },
body: JSON.stringify(formData), body: JSON.stringify(formData),
}); });
const response = await result.json();
console.log(response);
errorTextRef.current.innerText = response.error ?? "";
if (response.success) {
const newArticle: ArticleWithCategory = response.data;
router.push(urlJoin(`/articles/`, newArticle.category.name, newArticle.name));
}
} }
useEffect(() => { useEffect(() => {
@@ -54,7 +91,6 @@ export default function AdminArticlesCreate() {
cache: "no-cache", cache: "no-cache",
next: { revalidate: 60 * 1 }, next: { revalidate: 60 * 1 },
}); });
console.log();
const categories = await result.json(); const categories = await result.json();
let newSelectCategoriesOptions = []; let newSelectCategoriesOptions = [];
@@ -72,17 +108,19 @@ export default function AdminArticlesCreate() {
return ( return (
<div className={styles.adminArticlesCreate}> <div className={styles.adminArticlesCreate}>
<h1>Create a new article</h1> <h1>Create a new article</h1>
<button <div className={styles.formControl}>
type="button" <p className="text-error" ref={errorTextRef}></p>
onClick={() => { <button
postData(); type="button"
}} onClick={() => {
> postData();
send }}
</button> >
<div className={styles.form}> Create Article
<div className={styles.contentTableEditor}>contenttable</div> </button>
</div>
<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>
@@ -90,7 +128,7 @@ export default function AdminArticlesCreate() {
<div className={styles.titleInputs}> <div className={styles.titleInputs}>
<input <input
onChange={handleFormChange} onChange={handleFormChange}
className={""} className={!isValidText(title) && title ? "error" : ""}
type="text" type="text"
name="title" name="title"
placeholder="title" placeholder="title"
@@ -102,7 +140,7 @@ export default function AdminArticlesCreate() {
className={""} className={""}
type="text" type="text"
name="name" name="name"
value={getUrlSafeString(title)} value={title ? formatTextToUrlName(title) : ""}
/> />
</div> </div>
</div> </div>
@@ -117,11 +155,10 @@ export default function AdminArticlesCreate() {
/> />
</div> </div>
<div className={styles.introduction}> <div className={styles.introduction}>
{" "}
<label htmlFor="title">Introduction</label> <label htmlFor="title">Introduction</label>
<input <input
onChange={handleFormChange} onChange={handleFormChange}
className={""} className={!isValidText(introduction) && introduction ? "error" : ""}
type="text" type="text"
name="introduction" name="introduction"
placeholder="Introduction" placeholder="Introduction"
@@ -135,6 +172,43 @@ export default function AdminArticlesCreate() {
<Markdown value={markdown} /> <Markdown value={markdown} />
</div> </div>
</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"}
/>
<input
onChange={(e) => {
changeContentTableEntryTitle(i, e.target.value);
}}
type="text"
placeholder={"Title"}
/>
</div>
);
})}
<button
onClick={() => {
setContentTable([...contentTable, { title: "Title", anchor: "Anchor" }]);
}}
>
Add
</button>
</div>
<Markdown value={markdown} />
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,14 +1,15 @@
import React from "react"; import React from "react";
import styles from "../../../../styles/modules/ArticleContentTable.module.scss"; import styles from "../../../../styles/modules/ArticleContentTable.module.scss";
import { Article, ContentTableEntry } from "@prisma/client"; import { Article } from "@prisma/client";
import { IContentTableEntry } from "../../../../types/contentTable";
export default function ContentTable({ contentTableEntries }: { contentTableEntries: ContentTableEntry[] }) { export default function ContentTable({ contentTableData }: { contentTableData: any }) {
return ( return (
<div className={styles.articleContentTable}> <div className={styles.articleContentTable}>
<div className={styles.stickyContainer}> <div className={styles.stickyContainer}>
<div className={styles.list}> <div className={styles.list}>
<h2>Contents</h2> <h2>Contents</h2>
{contentTableEntries?.map((e, i) => { {contentTableData?.map((e, i) => {
return ( return (
<a key={i} href={"#" + e.anchor}> <a key={i} href={"#" + e.anchor}>
{e.title} {e.title}
@@ -16,7 +17,7 @@ export default function ContentTable({ contentTableEntries }: { contentTableEntr
); );
})} })}
</div> </div>
{contentTableEntries?.length < 15 ? <div className={styles.adContainer}>Future advertisement</div> : ""} {contentTableData?.length < 15 ? <div className={styles.adContainer}>Future advertisement</div> : ""}
</div> </div>
</div> </div>
); );

View File

@@ -2,16 +2,15 @@ import { marked } from "marked";
import ContentTable from "./ContentTable"; import ContentTable from "./ContentTable";
import Sidebar from "./Sidebar"; import Sidebar from "./Sidebar";
import styles from "../../../../styles/modules/Article.module.scss"; import styles from "../../../../styles/modules/Article.module.scss";
import { Article, Category, ContentTableEntry } from "@prisma/client";
import Image from "next/image"; import Image from "next/image";
import urlJoin from "url-join"; import urlJoin from "url-join";
import { apiUrl } from "../../../global"; import { apiUrl } from "../../../global";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import Markdown from "../../../Markdown"; import Markdown from "../../../Markdown";
import { IContentTableEntry } from "../../../../types/contentTable";
type ArticleWithIncludes = Prisma.ArticleGetPayload<{ type ArticleWithIncludes = Prisma.ArticleGetPayload<{
include: { contentTableEntries: true; category: true; image: true }; include: { category: true; image: true };
}>; }>;
export async function GetArticle(articleName: string): Promise<any> { export async function GetArticle(articleName: string): Promise<any> {
@@ -35,7 +34,7 @@ export default async function ArticlePage({ params }: { params: { articleName: s
return ( return (
<div className={styles.article}> <div className={styles.article}>
<ContentTable contentTableEntries={article.contentTableEntries} /> <ContentTable contentTableData={article.contentTable ? article.contentTable : []} />
<div className={styles.tutorialContent}> <div className={styles.tutorialContent}>
<div className={styles.header}> <div className={styles.header}>
<p className={`${styles.dates} text-muted`}> <p className={`${styles.dates} text-muted`}>
@@ -78,7 +77,7 @@ export default async function ArticlePage({ params }: { params: { articleName: s
export async function generateStaticParams() { export async function generateStaticParams() {
const articles: ArticleWithIncludes[] = await ( const articles: ArticleWithIncludes[] = await (
await fetch(urlJoin(apiUrl, `articles/`), { await fetch(urlJoin(apiUrl, `articles/`), {
cache: "force-cache", cache: "no-cache",
next: { revalidate: 60 * 10 }, next: { revalidate: 60 * 10 },
}) })
).json(); ).json();

View File

@@ -1,3 +0,0 @@
export function getUrlSafeString(value: string): string {
return encodeURIComponent(value.toLowerCase().replace(/[^a-z0-9 _-]+/gi, "-"));
}

View File

@@ -1,26 +1,22 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import prisma from "../../../lib/prisma"; import prisma from "../../../lib/prisma";
import { Prisma, ContentTableEntry } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { ResponseError } from "../../../types/responseErrors"; import { ResponseError } from "../../../types/responseErrors";
import { getUrlSafeString } from "../../../app/utils"; import { formatTextToUrlName } from "../../../utils";
type ArticleWithIncludes = Prisma.ArticleGetPayload<{ include: { contentTableEntries: true, category: true, image: true } }> type ArticleWithIncludes = Prisma.ArticleGetPayload<{ include: { contentTableEntries: true, category: true, image: true } }>
function sortContentTableEntries(entries: ContentTableEntry[]): ContentTableEntry[] {
return entries.sort((a, b) => a.orderIndex - b.orderIndex);
}
export default async function handler(req: Request, res: Response) { export default async function handler(req: Request, res: Response) {
res.setHeader("Content-Type", "application/json"); res.setHeader("Content-Type", "application/json");
const articleName: string = getUrlSafeString(req.query.articleName.toString()) const articleName: string = formatTextToUrlName(req.query.articleName.toString())
console.log(articleName)
await prisma.article await prisma.article
.findUnique({ where: { name: articleName }, include: { category: true, contentTableEntries: true, image: true } }) .findUnique({ where: { name: articleName }, include: { category: true, image: true } })
.then((result: ArticleWithIncludes) => { .then((result: ArticleWithIncludes) => {
if (result !== null) { if (result !== null) {
result.contentTableEntries = sortContentTableEntries(result.contentTableEntries);
res.end(JSON.stringify(result)); res.end(JSON.stringify(result));
} else { } else {
const error: ResponseError = { const error: ResponseError = {

View File

@@ -3,10 +3,46 @@ import prisma from "../../../lib/prisma";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { Article, Category } from "@prisma/client"; import { Article, Category } from "@prisma/client";
import { ResponseError } from "../../../types/responseErrors"; import { ResponseError } from "../../../types/responseErrors";
import { PostArticle } from "../../../types/postData";
import { isValidText } from "../../../validators";
import { formatTextToUrlName } from "../../../utils";
import { json } from "stream/consumers";
export default async function handler(req: Request, res: Response) { export default async function handler(req: Request, res: Response) {
res.setHeader("Content-Type", "application/json"); res.setHeader("Content-Type", "application/json");
await prisma.article.create({ data: req.body }); const postData: any = req.body;
console.log(); console.log(postData);
if (!isValidText(postData.title)) {
res.send(JSON.stringify({ target: "title", error: "Not a valid title" }));
return;
}
if (!isValidText(postData.introduction)) {
res.send(JSON.stringify({ target: "introduction", error: "Not a valid introduction" }));
return;
}
if (!postData.categoryId) {
res.send(JSON.stringify({ target: "category", error: "Category is required" }));
return;
}
postData.name = formatTextToUrlName(postData.title);
prisma.article
.create({ data: postData, include: { category: true } })
.then(
(data) => {
res.send(JSON.stringify({ success: true, data: data }));
},
(errorReason) => {
if (errorReason.code === "P2002") {
res.send(JSON.stringify({ target: errorReason.meta.target[0], error: "Already exists" }));
}
}
)
.catch((err) => {
console.error(err);
res.sendStatus(500).end();
});
} }

View File

@@ -27,7 +27,7 @@ export default async function handler(req: Request, res: Response) {
await prisma.article await prisma.article
.findMany({ .findMany({
where: { category: categoryName.length > 0 ? category : undefined }, where: { category: categoryName.length > 0 ? category : undefined },
include: { category: true, contentTableEntries: true }, include: { category: true },
take: limit, take: limit,
orderBy: orderByObj orderBy: orderByObj
}) })

View File

@@ -0,0 +1,40 @@
/*
Warnings:
- You are about to drop the column `typeId` on the `Article` table. All the data in the column will be lost.
- You are about to drop the `ArticleType` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `ContentTableEntry` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `contentTable` to the `Article` table without a default value. This is not possible if the table is not empty.
- Made the column `categoryId` on table `Article` required. This step will fail if there are existing NULL values in that column.
*/
-- DropForeignKey
ALTER TABLE "Article" DROP CONSTRAINT "Article_categoryId_fkey";
-- DropForeignKey
ALTER TABLE "Article" DROP CONSTRAINT "Article_typeId_fkey";
-- DropForeignKey
ALTER TABLE "ContentTableEntry" DROP CONSTRAINT "ContentTableEntry_articleId_fkey";
-- AlterTable
ALTER TABLE "Article" DROP COLUMN "typeId",
ADD COLUMN "contentTable" JSONB NOT NULL,
ADD COLUMN "imageId" INTEGER,
ADD COLUMN "introduction" TEXT NOT NULL DEFAULT '',
ALTER COLUMN "categoryId" SET NOT NULL;
-- AlterTable
ALTER TABLE "Image" ADD COLUMN "alt" TEXT NOT NULL DEFAULT '';
-- DropTable
DROP TABLE "ArticleType";
-- DropTable
DROP TABLE "ContentTableEntry";
-- AddForeignKey
ALTER TABLE "Article" ADD CONSTRAINT "Article_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Article" ADD CONSTRAINT "Article_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Article" ALTER COLUMN "contentTable" DROP NOT NULL;

View File

@@ -8,29 +8,18 @@ datasource db {
} }
model Article { model Article {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @unique name String @unique
title String @unique title String @unique
introduction String @default("") introduction String @default("")
imageId Int? imageId Int?
image Image? @relation(fields: [imageId], references: [id]) image Image? @relation(fields: [imageId], references: [id])
markdown String markdown String
contentTableEntries ContentTableEntry[] contentTable Json?
categoryId Int categoryId Int
category Category @relation(fields: [categoryId], references: [id]) category Category @relation(fields: [categoryId], references: [id])
dateCreated DateTime @default(now()) dateCreated DateTime @default(now())
dateUpdated DateTime @default(now()) dateUpdated DateTime @default(now())
}
model ContentTableEntry {
id Int @id @default(autoincrement())
title String
anchor String
orderIndex Int
articleId Int
article Article @relation(fields: [articleId], references: [id])
dateCreated DateTime @default(now())
dateUpdated DateTime @default(now())
} }
model Category { model Category {

34
styles/buttons.scss Normal file
View File

@@ -0,0 +1,34 @@
button {
border: 2px solid rgba(95, 95, 95, 0.5);
background-color: transparent;
height: 30px;
padding: 5px 5px 5px 5px;
color: var(--color-font);
border-radius: 3px;
outline: none;
transition: all 50ms linear;
cursor: pointer;
&::placeholder {
font-weight: bold;
}
&:hover {
border-color: var(--color-accent);
}
&.error {
border-color: var(--color-error);
}
&.success {
border-color: var(--color-success);
}
&.warning {
border-color: var(--color-warning);
}
&.info {
border-color: var(--color-info);
}
}

View File

@@ -1,10 +1,22 @@
@import "../variables.scss"; @import "../variables.scss";
.adminArticlesCreate { .adminArticlesCreate {
& > h1 {
text-align: center;
}
.formControl {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 10px 30px;
}
.form { .form {
display: grid; display: flex;
flex-direction: column;
gap: 70px; gap: 70px;
grid-template-columns: $tutorial-content-table-width minmax(0px, 1fr);
margin: 0px auto; margin: 0px auto;
max-width: 1800px; max-width: 1800px;
padding: 0px 24px; padding: 0px 24px;
@@ -42,17 +54,35 @@
gap: 10px 10px; gap: 10px 10px;
textarea { textarea {
color: var(--font-color); color: var(--font-color);
border: 2px solid rgba(59, 59, 59, 0.434); border: 2px solid #3b3b3b80;
background-color: transparent; background-color: transparent;
min-height: 700px; min-height: 700px;
resize: none; resize: none;
display: block; display: block;
border-radius: 0px; border-radius: 0px;
outline: 0; outline: 0;
resize: vertical; resize: both;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
} }
& > div {
max-width: 1000px;
border: 2px solid #3b3b3b80;
}
}
}
.contentTable {
.contentTableEditor {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px 10px;
& > div {
border: 2px solid #3b3b3b80;
max-width: 1000px;
}
} }
} }
} }

View File

@@ -21,7 +21,7 @@
transition: all 50ms ease-in-out; transition: all 50ms ease-in-out;
svg { svg {
fill: #bdbdbd; fill: #bdbdbd3a;
transition: all 50ms linear; transition: all 50ms linear;
&:hover { &:hover {
fill: var(--color-accent); fill: var(--color-accent);

View File

@@ -70,6 +70,10 @@ a {
font-size: 0.8em; font-size: 0.8em;
} }
.text-error {
font-weight: bold;
color: var(--color-error);
}
label { label {
font-weight: bold; font-weight: bold;
font-size: 0.9em; font-size: 0.9em;

View File

@@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": false,
@@ -25,13 +21,6 @@
} }
] ]
}, },
"include": [ "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"next-env.d.ts", "exclude": ["node_modules"]
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
} }

5
types/contentTable.ts Normal file
View File

@@ -0,0 +1,5 @@
export interface IContentTableEntry {
anchor: string;
title: string;
}

View File

@@ -1,8 +1,11 @@
import { IContentTableEntry } from "./contentTable";
export interface PostArticle { export interface PostArticle {
name: string;
title: string; title: string;
name?: string;
markdown: string; markdown: string;
introduction: string; introduction: string;
categoryId: number; categoryId: number;
contentTable: IContentTableEntry[]
imageId?: number; imageId?: number;
} }

16
utils.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { isValidName } from "./validators";
export function formatTextToUrlName(text: string): string {
text = text.toLowerCase();
let name = text;
name = name.replace(/[^a-z0-9\-_\s]+/gi, ""); // Replace all invalid characters (except spaces)
name = name.replace(/\s/g, "-"); // Replace spaces to -
// double check to be sure
if (isValidName(name)) {
return name;
} else {
console.error("formatTitleToName function not working");
return null;
}
}

10
validators.ts Normal file
View File

@@ -0,0 +1,10 @@
export function isValidText(text: string): boolean {
const textRegex = /^[a-zA-Z\s\d\-_\(\)\[\]\{\}\!\@\#\$\%\^\&\*\+\=\,\.\?\/\\\|\'\":;]+$/;
return textRegex.test(text) && text.length > 0;
}
export function isValidName(name: string): boolean {
const nameRegex = /^[a-zA-Z0-9\-_]+$/;
return nameRegex.test(name);
}