From e080e6b22296dc6f970f9099f76c9e37def30012 Mon Sep 17 00:00:00 2001 From: Ahmed Al-Taiar Date: Sun, 8 Sep 2024 23:36:30 -0400 Subject: [PATCH] Projects CRUD (todo: project images) --- api/src/graphql/projects.sdl.ts | 6 +- api/src/services/projects/projects.ts | 36 ++-- .../components/ColorPicker/ColorPicker.tsx | 2 +- .../components/FormTextList/FormTextList.tsx | 68 ++++++++ .../EditProjectCell/EditProjectCell.tsx | 43 ++--- .../Project/NewProject/NewProject.tsx | 29 ++-- .../components/Project/Project/Project.tsx | 114 ++++++------ .../Project/ProjectCell/ProjectCell.tsx | 3 - .../Project/ProjectForm/ProjectForm.tsx | 162 +++++++++++------- .../components/Project/Projects/Projects.tsx | 127 ++++++++------ .../Project/ProjectsCell/ProjectsCell.tsx | 3 - .../Social/EditSocialCell/EditSocialCell.tsx | 24 +-- web/src/components/Social/Social/Social.tsx | 4 +- .../Social/SocialForm/SocialForm.tsx | 4 +- web/src/components/Tag/TagForm/TagForm.tsx | 4 +- .../ForgotPasswordPage/ForgotPasswordPage.tsx | 4 +- .../ResetPasswordPage/ResetPasswordPage.tsx | 4 +- 17 files changed, 386 insertions(+), 251 deletions(-) create mode 100644 web/src/components/FormTextList/FormTextList.tsx diff --git a/api/src/graphql/projects.sdl.ts b/api/src/graphql/projects.sdl.ts index 3def48a..054fe70 100644 --- a/api/src/graphql/projects.sdl.ts +++ b/api/src/graphql/projects.sdl.ts @@ -5,7 +5,7 @@ export const schema = gql` description: String! images: [ProjectImage]! date: DateTime! - links: [String]! + links: [URL]! tags: [Tag]! } @@ -18,14 +18,14 @@ export const schema = gql` title: String! description: String! date: DateTime! - links: [String]! + links: [URL]! } input UpdateProjectInput { title: String description: String date: DateTime - links: [String]! + links: [URL]! } type Mutation { diff --git a/api/src/services/projects/projects.ts b/api/src/services/projects/projects.ts index 4c3e13d..3ea570b 100644 --- a/api/src/services/projects/projects.ts +++ b/api/src/services/projects/projects.ts @@ -6,45 +6,35 @@ import type { import { db } from 'src/lib/db' -export const projects: QueryResolvers['projects'] = () => { - return db.project.findMany() -} +export const projects: QueryResolvers['projects'] = () => db.project.findMany() -export const project: QueryResolvers['project'] = ({ id }) => { - return db.project.findUnique({ +export const project: QueryResolvers['project'] = ({ id }) => + db.project.findUnique({ where: { id }, }) -} -export const createProject: MutationResolvers['createProject'] = ({ - input, -}) => { - return db.project.create({ +export const createProject: MutationResolvers['createProject'] = ({ input }) => + db.project.create({ data: input, }) -} export const updateProject: MutationResolvers['updateProject'] = ({ id, input, -}) => { - return db.project.update({ +}) => + db.project.update({ data: input, where: { id }, }) -} -export const deleteProject: MutationResolvers['deleteProject'] = ({ id }) => { - return db.project.delete({ +export const deleteProject: MutationResolvers['deleteProject'] = ({ id }) => + db.project.delete({ where: { id }, }) -} export const Project: ProjectRelationResolvers = { - images: (_obj, { root }) => { - return db.project.findUnique({ where: { id: root?.id } }).images() - }, - tags: (_obj, { root }) => { - return db.project.findUnique({ where: { id: root?.id } }).tags() - }, + images: (_obj, { root }) => + db.project.findUnique({ where: { id: root?.id } }).images(), + tags: (_obj, { root }) => + db.project.findUnique({ where: { id: root?.id } }).tags(), } diff --git a/web/src/components/ColorPicker/ColorPicker.tsx b/web/src/components/ColorPicker/ColorPicker.tsx index a23606c..debecab 100644 --- a/web/src/components/ColorPicker/ColorPicker.tsx +++ b/web/src/components/ColorPicker/ColorPicker.tsx @@ -22,7 +22,7 @@ const ColorPicker = ({ color, setColor }: ColorPickerProps) => { + + {list.map((item, i) => ( + + ))} + + ) +} + +export default FormTextList diff --git a/web/src/components/Project/EditProjectCell/EditProjectCell.tsx b/web/src/components/Project/EditProjectCell/EditProjectCell.tsx index da34199..e504fa2 100644 --- a/web/src/components/Project/EditProjectCell/EditProjectCell.tsx +++ b/web/src/components/Project/EditProjectCell/EditProjectCell.tsx @@ -45,7 +45,6 @@ const UPDATE_PROJECT_MUTATION: TypedDocumentNode< ` export const Loading = () => - export const Failure = ({ error }: CellFailureProps) => ( ) @@ -58,33 +57,37 @@ export const Success = ({ project }: CellSuccessProps) => { toast.success('Project updated') navigate(routes.projects()) }, - onError: (error) => { - toast.error(error.message) - }, + onError: (error) => toast.error(error.message), } ) const onSave = ( input: UpdateProjectInput, id: EditProjectById['project']['id'] - ) => { - updateProject({ variables: { id, input } }) - } + ) => updateProject({ variables: { id, input } }) return ( -
-
-

- Edit Project {project?.id} -

-
-
- +
+
+ + + + + + + + + + + +
Edit Project {project.id}
+ +
) diff --git a/web/src/components/Project/NewProject/NewProject.tsx b/web/src/components/Project/NewProject/NewProject.tsx index f685cc8..da57dc5 100644 --- a/web/src/components/Project/NewProject/NewProject.tsx +++ b/web/src/components/Project/NewProject/NewProject.tsx @@ -30,23 +30,30 @@ const NewProject = () => { toast.success('Project created') navigate(routes.projects()) }, - onError: (error) => { - toast.error(error.message) - }, + onError: (error) => toast.error(error.message), } ) - const onSave = (input: CreateProjectInput) => { + const onSave = (input: CreateProjectInput) => createProject({ variables: { input } }) - } return ( -
-
-

New Project

-
-
- +
+
+ + + + + + + + + + + +
New Project
+ +
) diff --git a/web/src/components/Project/Project/Project.tsx b/web/src/components/Project/Project/Project.tsx index 2c00bfe..1eb287f 100644 --- a/web/src/components/Project/Project/Project.tsx +++ b/web/src/components/Project/Project/Project.tsx @@ -32,66 +32,76 @@ const Project = ({ project }: Props) => { toast.success('Project deleted') navigate(routes.projects()) }, - onError: (error) => { - toast.error(error.message) - }, + onError: (error) => toast.error(error.message), }) const onDeleteClick = (id: DeleteProjectMutationVariables['id']) => { - if (confirm('Are you sure you want to delete project ' + id + '?')) { + if (confirm('Are you sure you want to delete project ' + id + '?')) deleteProject({ variables: { id } }) - } } return ( - <> -
-
-

- Project {project.id} Detail -

-
- - - - - - - - - - - - - - - - - - - - - - - -
Id{project.id}
Title{project.title}
Description{project.description}
Date{timeTag(project.date)}
Links{project.links}
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Project {project.id}: {project.title} +  
ID{project.id}
Title{project.title}
Description{project.description}
Date{timeTag(project.date)}
Links + {project.links.map((link, i) => ( +
+ {link} +
+ ))} +
+
+
- - +
) } diff --git a/web/src/components/Project/ProjectCell/ProjectCell.tsx b/web/src/components/Project/ProjectCell/ProjectCell.tsx index ea5ca00..0676c3d 100644 --- a/web/src/components/Project/ProjectCell/ProjectCell.tsx +++ b/web/src/components/Project/ProjectCell/ProjectCell.tsx @@ -27,13 +27,10 @@ export const QUERY: TypedDocumentNode< ` export const Loading = () => - export const Empty = () => - export const Failure = ({ error, }: CellFailureProps) => - export const Success = ({ project, }: CellSuccessProps) => ( diff --git a/web/src/components/Project/ProjectForm/ProjectForm.tsx b/web/src/components/Project/ProjectForm/ProjectForm.tsx index f39d768..9a04eb5 100644 --- a/web/src/components/Project/ProjectForm/ProjectForm.tsx +++ b/web/src/components/Project/ProjectForm/ProjectForm.tsx @@ -1,17 +1,26 @@ +import { useEffect, useMemo, useRef, useState } from 'react' + +import { mdiFormatTitle, mdiLinkVariant } from '@mdi/js' +import Icon from '@mdi/react' import type { EditProjectById, UpdateProjectInput } from 'types/graphql' import type { RWGqlError } from '@redwoodjs/forms' import { Form, - FormError, FieldError, Label, TextField, Submit, + TextAreaField, } from '@redwoodjs/forms' +import { toast } from '@redwoodjs/web/toast' + +import FormTextList from 'src/components/FormTextList/FormTextList' type FormProject = NonNullable +// TODO: add project images + interface ProjectFormProps { project?: EditProjectById['project'] onSave: (data: UpdateProjectInput, id?: FormProject['id']) => void @@ -20,81 +29,110 @@ interface ProjectFormProps { } const ProjectForm = (props: ProjectFormProps) => { + const [links, setLinks] = useState(props.project?.links || []) + const [linkErrors, setLinkErrors] = useState([]) + + const urlRegex = useMemo( + () => + /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i, + [] + ) + + useEffect(() => { + setLinkErrors(links.map((link) => link.length > 0 && !urlRegex.test(link))) + }, [links, urlRegex]) + const onSubmit = (data: FormProject) => { + const errorsExist = linkErrors.indexOf(true) !== -1 + const errorCount = linkErrors.filter((val) => val).length + const emptyCount = links.filter((val) => val.trim().length === 0).length + + if (errorsExist) return toast.error(`${errorCount} links invalid`) + if (emptyCount > 0) return toast.error(`${emptyCount} links empty`) + + data.links = links + data.date = new Date().toISOString() // TODO: change to date picker value props.onSave(data, props?.project?.id) } + const titleRef = useRef(null) + useEffect(() => titleRef.current?.focus(), []) + return ( -
- onSubmit={onSubmit} error={props.error}> - - + + onSubmit={onSubmit} + error={props.error} + className="space-y-2 w-80" + > + - - - - - - - + - - - - - - - - - -
- - Save - +
+
- -
+ + + + + + ) } diff --git a/web/src/components/Project/Projects/Projects.tsx b/web/src/components/Project/Projects/Projects.tsx index 11e544d..47d4d69 100644 --- a/web/src/components/Project/Projects/Projects.tsx +++ b/web/src/components/Project/Projects/Projects.tsx @@ -1,3 +1,5 @@ +import { mdiDotsVertical } from '@mdi/js' +import Icon from '@mdi/react' import type { DeleteProjectMutation, DeleteProjectMutationVariables, @@ -25,74 +27,97 @@ const DELETE_PROJECT_MUTATION: TypedDocumentNode< const ProjectsList = ({ projects }: FindProjects) => { const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, { - onCompleted: () => { - toast.success('Project deleted') - }, - onError: (error) => { - toast.error(error.message) - }, - // This refetches the query on the list page. Read more about other ways to - // update the cache over here: - // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates + onCompleted: () => toast.success('Project deleted'), + onError: (error) => toast.error(error.message), refetchQueries: [{ query: QUERY }], awaitRefetchQueries: true, }) const onDeleteClick = (id: DeleteProjectMutationVariables['id']) => { - if (confirm('Are you sure you want to delete project ' + id + '?')) { + if (confirm('Are you sure you want to delete project ' + id + '?')) deleteProject({ variables: { id } }) - } } return ( -
- - +
+
+ - - + - {projects.map((project) => ( - - - - - - - - - ))} + {projects.map((project) => { + const actionButtons = ( + <> + + Show + + + Edit + + + + ) + + return ( + + + + + + + + ) + })}
Id Title Description Date Links  
{truncate(project.id)}{truncate(project.title)}{truncate(project.description)}{timeTag(project.date)}{truncate(project.links)} - -
{truncate(project.title)}{truncate(project.description)}{timeTag(project.date)} + {project.links.map((link, i) => ( +
+ {link} +
+ ))} +
+ +
+
+ +
+
+ +
+
+
diff --git a/web/src/components/Project/ProjectsCell/ProjectsCell.tsx b/web/src/components/Project/ProjectsCell/ProjectsCell.tsx index 0546c2b..8f3c818 100644 --- a/web/src/components/Project/ProjectsCell/ProjectsCell.tsx +++ b/web/src/components/Project/ProjectsCell/ProjectsCell.tsx @@ -25,13 +25,10 @@ export const QUERY: TypedDocumentNode = ` export const Loading = () => - export const Empty = () => - export const Failure = ({ error }: CellFailureProps) => ( ) - export const Success = ({ projects, }: CellSuccessProps) => ( diff --git a/web/src/components/Social/EditSocialCell/EditSocialCell.tsx b/web/src/components/Social/EditSocialCell/EditSocialCell.tsx index ffc4392..8c36be8 100644 --- a/web/src/components/Social/EditSocialCell/EditSocialCell.tsx +++ b/web/src/components/Social/EditSocialCell/EditSocialCell.tsx @@ -44,9 +44,7 @@ const UPDATE_SOCIAL_MUTATION: TypedDocumentNode< ` export const Loading = () => - export const Empty = () => - export const Failure = ({ error }: CellFailureProps) => ( ) @@ -77,17 +75,21 @@ export const Success = ({ social }: CellSuccessProps) => {
- + + + - + + +
Edit Social {social.id}
Edit Social {social.id}
- -
+ +
diff --git a/web/src/components/Social/Social/Social.tsx b/web/src/components/Social/Social/Social.tsx index 09a91fc..f97965a 100644 --- a/web/src/components/Social/Social/Social.tsx +++ b/web/src/components/Social/Social/Social.tsx @@ -32,9 +32,7 @@ const Social = ({ social }: Props) => { toast.success('Social deleted') navigate(routes.socials()) }, - onError: (error) => { - toast.error(error.message) - }, + onError: (error) => toast.error(error.message), }) const onDeleteClick = ( diff --git a/web/src/components/Social/SocialForm/SocialForm.tsx b/web/src/components/Social/SocialForm/SocialForm.tsx index 150f2ed..b9029e4 100644 --- a/web/src/components/Social/SocialForm/SocialForm.tsx +++ b/web/src/components/Social/SocialForm/SocialForm.tsx @@ -86,8 +86,8 @@ const SocialForm = (props: SocialFormProps) => { > diff --git a/web/src/components/Tag/TagForm/TagForm.tsx b/web/src/components/Tag/TagForm/TagForm.tsx index 83ea707..cacf30a 100644 --- a/web/src/components/Tag/TagForm/TagForm.tsx +++ b/web/src/components/Tag/TagForm/TagForm.tsx @@ -45,8 +45,8 @@ const TagForm = (props: TagFormProps) => { > diff --git a/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx b/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx index 68a690e..076bd30 100644 --- a/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx +++ b/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx @@ -51,8 +51,8 @@ const ForgotPasswordPage = () => { > diff --git a/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx b/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx index f7131fc..4258eab 100644 --- a/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx +++ b/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx @@ -75,8 +75,8 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => { >