From 4a94b6807e6b2d392fbda0653dbadb849bb873ff Mon Sep 17 00:00:00 2001 From: Ahmed Al-Taiar Date: Tue, 1 Oct 2024 20:45:43 -0400 Subject: [PATCH] Resume + Projects done --- .../migration.sql | 15 ++ api/db/schema.prisma | 10 + api/src/graphql/resume.sdl.ts | 19 ++ api/src/graphql/titles.sdl.ts | 25 +++ api/src/services/resume/resume.ts | 42 ++++ api/src/services/titles/titles.ts | 26 +++ web/public/no_resume.pdf | Bin 0 -> 10307 bytes web/src/Routes.tsx | 15 +- .../ContactCardCell/ContactCardCell.tsx | 16 +- .../Portrait/PortraitCell/PortraitCell.tsx | 18 +- .../Portrait/PortraitForm/PortraitForm.tsx | 10 +- .../Project/AdminProject/AdminProject.tsx | 160 ++++++++++++++ .../AdminProjectCell/AdminProjectCell.tsx | 49 ++++ .../components/Project/Project/Project.tsx | 209 ++++++------------ .../Project/ProjectCell/ProjectCell.tsx | 2 +- .../components/Project/Projects/Projects.tsx | 182 +++++++-------- .../ProjectsShowcase/ProjectsShowcase.tsx | 139 ++++++++---- .../AdminResumeCell/AdminResumeCell.tsx | 35 +++ web/src/components/Resume/Resume/Resume.tsx | 26 +++ .../Resume/ResumeCell/ResumeCell.tsx | 33 +++ .../Resume/ResumeForm/ResumeForm.tsx | 202 +++++++++++++++++ web/src/components/Tag/Tags/Tags.tsx | 49 ---- web/src/components/Uploader/Uploader.tsx | 14 +- web/src/layouts/NavbarLayout/NavbarLayout.tsx | 8 + .../AdminProjectPage/AdminProjectPage.tsx | 16 ++ .../AdminProjectsPage/AdminProjectsPage.tsx | 12 + .../pages/Project/ProjectPage/ProjectPage.tsx | 17 +- .../Project/ProjectsPage/ProjectsPage.tsx | 31 ++- web/src/pages/ProjectPage/ProjectPage.tsx | 27 --- web/src/pages/ProjectsPage/ProjectsPage.tsx | 29 --- .../AdminResumePage/AdminResumePage.tsx | 14 ++ .../pages/Resume/ResumePage/ResumePage.tsx | 15 ++ 32 files changed, 1034 insertions(+), 431 deletions(-) create mode 100644 api/db/migrations/20241001005227_title_and_resume/migration.sql create mode 100644 api/src/graphql/resume.sdl.ts create mode 100644 api/src/graphql/titles.sdl.ts create mode 100644 api/src/services/resume/resume.ts create mode 100644 api/src/services/titles/titles.ts create mode 100644 web/public/no_resume.pdf create mode 100644 web/src/components/Project/AdminProject/AdminProject.tsx create mode 100644 web/src/components/Project/AdminProjectCell/AdminProjectCell.tsx create mode 100644 web/src/components/Resume/AdminResumeCell/AdminResumeCell.tsx create mode 100644 web/src/components/Resume/Resume/Resume.tsx create mode 100644 web/src/components/Resume/ResumeCell/ResumeCell.tsx create mode 100644 web/src/components/Resume/ResumeForm/ResumeForm.tsx create mode 100644 web/src/pages/Project/AdminProjectPage/AdminProjectPage.tsx create mode 100644 web/src/pages/Project/AdminProjectsPage/AdminProjectsPage.tsx delete mode 100644 web/src/pages/ProjectPage/ProjectPage.tsx delete mode 100644 web/src/pages/ProjectsPage/ProjectsPage.tsx create mode 100644 web/src/pages/Resume/AdminResumePage/AdminResumePage.tsx create mode 100644 web/src/pages/Resume/ResumePage/ResumePage.tsx diff --git a/api/db/migrations/20241001005227_title_and_resume/migration.sql b/api/db/migrations/20241001005227_title_and_resume/migration.sql new file mode 100644 index 0000000..746885d --- /dev/null +++ b/api/db/migrations/20241001005227_title_and_resume/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "Resume" ( + "id" SERIAL NOT NULL, + "fileId" TEXT NOT NULL, + + CONSTRAINT "Resume_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Title" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + + CONSTRAINT "Title_pkey" PRIMARY KEY ("id") +); diff --git a/api/db/schema.prisma b/api/db/schema.prisma index 31a5f10..1cd8215 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -58,6 +58,16 @@ model Portrait { fileId String } +model Resume { + id Int @id @default(autoincrement()) + fileId String +} + +model Title { + id Int @id @default(autoincrement()) + title String +} + model Tag { id Int @id @default(autoincrement()) tag String diff --git a/api/src/graphql/resume.sdl.ts b/api/src/graphql/resume.sdl.ts new file mode 100644 index 0000000..84b0778 --- /dev/null +++ b/api/src/graphql/resume.sdl.ts @@ -0,0 +1,19 @@ +export const schema = gql` + type Resume { + id: Int! + fileId: URL! + } + + type Query { + resume: Resume @skipAuth + } + + input CreateResumeInput { + fileId: URL! + } + + type Mutation { + createResume(input: CreateResumeInput!): Resume! @requireAuth + deleteResume: Resume! @requireAuth + } +` diff --git a/api/src/graphql/titles.sdl.ts b/api/src/graphql/titles.sdl.ts new file mode 100644 index 0000000..f5e191d --- /dev/null +++ b/api/src/graphql/titles.sdl.ts @@ -0,0 +1,25 @@ +export const schema = gql` + type Title { + id: Int! + title: String! + } + + type Query { + titles: [Title!]! @skipAuth + title(id: Int!): Title @skipAuth + } + + input CreateTitleInput { + title: String! + } + + input UpdateTitleInput { + title: String + } + + type Mutation { + createTitle(input: CreateTitleInput!): Title! @requireAuth + updateTitle(id: Int!, input: UpdateTitleInput!): Title! @requireAuth + deleteTitle(id: Int!): Title! @requireAuth + } +` diff --git a/api/src/services/resume/resume.ts b/api/src/services/resume/resume.ts new file mode 100644 index 0000000..f7b099c --- /dev/null +++ b/api/src/services/resume/resume.ts @@ -0,0 +1,42 @@ +import type { QueryResolvers, MutationResolvers } from 'types/graphql' + +import { isProduction } from '@redwoodjs/api/logger' +import { ValidationError } from '@redwoodjs/graphql-server' + +import { db } from 'src/lib/db' + +const address = isProduction + ? process.env.ADDRESS_PROD + : process.env.ADDRESS_DEV + +export const resume: QueryResolvers['resume'] = async () => { + const resume = await db.resume.findFirst() + + if (resume) return resume + else + return { + id: -1, + fileId: `${address}/no_resume.pdf`, + } +} + +export const createResume: MutationResolvers['createResume'] = async ({ + input, +}) => { + if (await db.resume.findFirst()) + throw new ValidationError('Resume already exists') + else + return db.resume.create({ + data: input, + }) +} + +export const deleteResume: MutationResolvers['deleteResume'] = async () => { + const resume = await db.resume.findFirst() + + if (!resume) throw new ValidationError('Resume does not exist') + else + return db.resume.delete({ + where: { id: resume.id }, + }) +} diff --git a/api/src/services/titles/titles.ts b/api/src/services/titles/titles.ts new file mode 100644 index 0000000..a644711 --- /dev/null +++ b/api/src/services/titles/titles.ts @@ -0,0 +1,26 @@ +import type { QueryResolvers, MutationResolvers } from 'types/graphql' + +import { db } from 'src/lib/db' + +export const titles: QueryResolvers['titles'] = () => db.title.findMany() + +export const title: QueryResolvers['title'] = ({ id }) => + db.title.findUnique({ + where: { id }, + }) + +export const createTitle: MutationResolvers['createTitle'] = ({ input }) => + db.title.create({ + data: input, + }) + +export const updateTitle: MutationResolvers['updateTitle'] = ({ id, input }) => + db.title.update({ + data: input, + where: { id }, + }) + +export const deleteTitle: MutationResolvers['deleteTitle'] = ({ id }) => + db.title.delete({ + where: { id }, + }) diff --git a/web/public/no_resume.pdf b/web/public/no_resume.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e5631eb2b427843c8720305c49fb38b00b988915 GIT binary patch literal 10307 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 literal 0 HcmV?d00001 diff --git a/web/src/Routes.tsx b/web/src/Routes.tsx index 5ac5725..4595bcf 100644 --- a/web/src/Routes.tsx +++ b/web/src/Routes.tsx @@ -3,7 +3,7 @@ import { Router, Route, Set, PrivateSet } from '@redwoodjs/router' import { useAuth } from 'src/auth' import AccountbarLayout from 'src/layouts/AccountbarLayout' import NavbarLayout from 'src/layouts/NavbarLayout/NavbarLayout' -import ScaffoldLayout from 'src/layouts/ScaffoldLayout' +import ScaffoldLayout from 'src/layouts/ScaffoldLayout/ScaffoldLayout' const Routes = () => { return ( @@ -27,11 +27,15 @@ const Routes = () => { + + + + - - + + @@ -49,9 +53,10 @@ const Routes = () => { - - + + + diff --git a/web/src/components/ContactCard/ContactCardCell/ContactCardCell.tsx b/web/src/components/ContactCard/ContactCardCell/ContactCardCell.tsx index 27ae50f..ea7c588 100644 --- a/web/src/components/ContactCard/ContactCardCell/ContactCardCell.tsx +++ b/web/src/components/ContactCard/ContactCardCell/ContactCardCell.tsx @@ -11,16 +11,14 @@ import CellFailure from 'src/components/Cell/CellFailure/CellFailure' import CellLoading from 'src/components/Cell/CellLoading/CellLoading' import ContactCard from 'src/components/ContactCard/ContactCard/ContactCard' -export const QUERY: TypedDocumentNode< - FindPortrait, - FindPortraitVariables -> = gql` - query ContactCardPortrait { - portrait: portrait { - fileId +export const QUERY: TypedDocumentNode = + gql` + query ContactCardPortrait { + portrait: portrait { + fileId + } } - } -` + ` export const Loading = () => diff --git a/web/src/components/Portrait/PortraitCell/PortraitCell.tsx b/web/src/components/Portrait/PortraitCell/PortraitCell.tsx index b716852..ab35c60 100644 --- a/web/src/components/Portrait/PortraitCell/PortraitCell.tsx +++ b/web/src/components/Portrait/PortraitCell/PortraitCell.tsx @@ -11,17 +11,15 @@ import CellFailure from 'src/components/Cell/CellFailure/CellFailure' import CellLoading from 'src/components/Cell/CellLoading/CellLoading' import PortraitForm from 'src/components/Portrait/PortraitForm/PortraitForm' -export const QUERY: TypedDocumentNode< - FindPortrait, - FindPortraitVariables -> = gql` - query FindPortrait { - portrait: portrait { - id - fileId +export const QUERY: TypedDocumentNode = + gql` + query FindPortrait { + portrait { + id + fileId + } } - } -` + ` export const Loading = () => diff --git a/web/src/components/Portrait/PortraitForm/PortraitForm.tsx b/web/src/components/Portrait/PortraitForm/PortraitForm.tsx index 8e2210d..0b9387d 100644 --- a/web/src/components/Portrait/PortraitForm/PortraitForm.tsx +++ b/web/src/components/Portrait/PortraitForm/PortraitForm.tsx @@ -55,8 +55,8 @@ const CREATE_PORTRAIT_MUTATION: TypedDocumentNode< } ` -const PortraitForm = (props: PortraitFormProps) => { - const [fileId, _setFileId] = useState(props.portrait?.fileId) +const PortraitForm = ({ portrait }: PortraitFormProps) => { + const [fileId, _setFileId] = useState(portrait?.fileId) const fileIdRef = useRef(fileId) const setFileId = (fileId: string) => { @@ -104,12 +104,12 @@ const PortraitForm = (props: PortraitFormProps) => { ) } - if (props.portrait?.fileId) + if (portrait?.fileId) return (
{`${process.env.FIRST_NAME}
@@ -119,7 +119,7 @@ const PortraitForm = (props: PortraitFormProps) => { className="btn btn-error btn-sm uppercase" onClick={() => { if (confirm('Are you sure?')) { - deleteFile(props.portrait?.fileId) + deleteFile(portrait?.fileId) deletePortrait() setFileId(null) } diff --git a/web/src/components/Project/AdminProject/AdminProject.tsx b/web/src/components/Project/AdminProject/AdminProject.tsx new file mode 100644 index 0000000..8a2a372 --- /dev/null +++ b/web/src/components/Project/AdminProject/AdminProject.tsx @@ -0,0 +1,160 @@ +import type { + DeleteProjectMutation, + DeleteProjectMutationVariables, + AdminFindProjectById, +} from 'types/graphql' + +import { Link, routes, navigate } from '@redwoodjs/router' +import { useMutation } from '@redwoodjs/web' +import type { TypedDocumentNode } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import { calculateLuminance } from 'src/lib/color' +import { timeTag } from 'src/lib/formatters' +import { batchDelete } from 'src/lib/tus' + +const DELETE_PROJECT_MUTATION: TypedDocumentNode< + DeleteProjectMutation, + DeleteProjectMutationVariables +> = gql` + mutation DeleteProjectMutation($id: Int!) { + deleteProject(id: $id) { + id + } + } +` + +interface Props { + project: NonNullable +} + +const AdminProject = ({ project }: Props) => { + const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, { + onCompleted: () => { + toast.success('Project deleted') + navigate(routes.adminProjects()) + }, + onError: (error) => toast.error(error.message), + }) + + const onDeleteClick = (id: DeleteProjectMutationVariables['id']) => { + if (confirm('Are you sure you want to delete project ' + id + '?')) { + batchDelete(project.images) + deleteProject({ variables: { id } }) + } + } + + return ( +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Project {project.id}: {project.title} +  
ID{project.id}
Title{project.title}
Description{project.description}
Date{timeTag(project.date)}
Images +
+ {project.images.map((image, i) => ( + + {i + 1} + + ))} +
+
Tags +
+ {project.tags.map((tag, i) => ( +
0.5 + ? 'black' + : 'white', + }} + > + {tag.tag} +
+ ))} +
+
Links +
+ {project.links.map((link, i) => ( + + {link} + + ))} +
+
+
+ +
+
+ ) +} + +export default AdminProject diff --git a/web/src/components/Project/AdminProjectCell/AdminProjectCell.tsx b/web/src/components/Project/AdminProjectCell/AdminProjectCell.tsx new file mode 100644 index 0000000..76126cc --- /dev/null +++ b/web/src/components/Project/AdminProjectCell/AdminProjectCell.tsx @@ -0,0 +1,49 @@ +import type { + AdminFindProjectById, + AdminFindProjectByIdVariables, +} from 'types/graphql' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty' +import CellFailure from 'src/components/Cell/CellFailure/CellFailure' +import CellLoading from 'src/components/Cell/CellLoading/CellLoading' +import Project from 'src/components/Project/AdminProject/AdminProject' + +export const QUERY: TypedDocumentNode< + AdminFindProjectById, + AdminFindProjectByIdVariables +> = gql` + query AdminFindProjectById($id: Int!) { + project: project(id: $id) { + id + title + description + date + links + images + tags { + id + tag + color + } + } + } +` + +export const Loading = () => +export const Empty = () => +export const Failure = ({ + error, +}: CellFailureProps) => ( + +) +export const Success = ({ + project, +}: CellSuccessProps) => ( + +) diff --git a/web/src/components/Project/Project/Project.tsx b/web/src/components/Project/Project/Project.tsx index da96a4e..aba020e 100644 --- a/web/src/components/Project/Project/Project.tsx +++ b/web/src/components/Project/Project/Project.tsx @@ -1,157 +1,80 @@ -import type { - DeleteProjectMutation, - DeleteProjectMutationVariables, - FindProjectById, -} from 'types/graphql' - -import { Link, routes, navigate } from '@redwoodjs/router' -import { useMutation } from '@redwoodjs/web' -import type { TypedDocumentNode } from '@redwoodjs/web' -import { toast } from '@redwoodjs/web/toast' +import { mdiLinkVariant } from '@mdi/js' +import Icon from '@mdi/react' +import { format, isAfter, startOfToday } from 'date-fns' +import type { FindProjectById } from 'types/graphql' import { calculateLuminance } from 'src/lib/color' -import { timeTag } from 'src/lib/formatters' -import { batchDelete } from 'src/lib/tus' - -const DELETE_PROJECT_MUTATION: TypedDocumentNode< - DeleteProjectMutation, - DeleteProjectMutationVariables -> = gql` - mutation DeleteProjectMutation($id: Int!) { - deleteProject(id: $id) { - id - } - } -` interface Props { project: NonNullable } const Project = ({ project }: Props) => { - const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, { - onCompleted: () => { - toast.success('Project deleted') - navigate(routes.adminProjects()) - }, - onError: (error) => toast.error(error.message), - }) - - const onDeleteClick = (id: DeleteProjectMutationVariables['id']) => { - if (confirm('Are you sure you want to delete project ' + id + '?')) { - batchDelete(project.images) - deleteProject({ variables: { id } }) - } - } - return ( -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Project {project.id}: {project.title} -  
ID{project.id}
Title{project.title}
Description{project.description}
Date{timeTag(project.date)}
Images -
- {project.images.map((image, i) => ( - - {i + 1} - - ))} -
-
Tags -
- {project.tags.map((tag, i) => ( -
0.5 - ? 'black' - : 'white', - }} - > - {tag.tag} -
- ))} -
-
Links -
- {project.links.map((link, i) => ( - - {link} - - ))} -
-
+
+
+

{project.title}

+
+ {isAfter(new Date(project.date), startOfToday()) && ( +
+ planned +
+ )} +
+ {format(project.date, 'PPP')} +
-
+
+ {project.images.map((image, i) => ( + - Edit - - - + + + ))}
) diff --git a/web/src/components/Project/ProjectCell/ProjectCell.tsx b/web/src/components/Project/ProjectCell/ProjectCell.tsx index 3ad80db..1db99d7 100644 --- a/web/src/components/Project/ProjectCell/ProjectCell.tsx +++ b/web/src/components/Project/ProjectCell/ProjectCell.tsx @@ -9,7 +9,7 @@ import type { import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty' import CellFailure from 'src/components/Cell/CellFailure/CellFailure' import CellLoading from 'src/components/Cell/CellLoading/CellLoading' -import Project from 'src/components/Project/Project' +import Project from 'src/components/Project/Project/Project' export const QUERY: TypedDocumentNode< FindProjectById, diff --git a/web/src/components/Project/Projects/Projects.tsx b/web/src/components/Project/Projects/Projects.tsx index 190d727..1262a88 100644 --- a/web/src/components/Project/Projects/Projects.tsx +++ b/web/src/components/Project/Projects/Projects.tsx @@ -1,5 +1,6 @@ import { mdiDotsVertical } from '@mdi/js' import Icon from '@mdi/react' +import { isAfter } from 'date-fns' import type { DeleteProjectMutation, DeleteProjectMutationVariables, @@ -57,101 +58,106 @@ const ProjectsList = ({ projects }: FindProjects) => { - {projects.map((project) => { - const actionButtons = ( - <> - - Show - - - Edit - - - + {projects + .slice() + .sort((a, b) => + isAfter(new Date(b.date), new Date(a.date)) ? 1 : -1 ) + .map((project) => { + const actionButtons = ( + <> + + Show + + + Edit + + + + ) - return ( - - {truncate(project.title)} - {truncate(project.description)} - {timeTag(project.date)} - {`${project.images.length} ${project.images.length === 1 ? 'image' : 'images'}`} - -
- {project.tags.map((tag, i) => ( + return ( + + {truncate(project.title)} + {truncate(project.description)} + {timeTag(project.date)} + {`${project.images.length} ${project.images.length === 1 ? 'image' : 'images'}`} + +
+ {project.tags.map((tag, i) => ( +
0.5 + ? 'black' + : 'white', + }} + > + {tag.tag} +
+ ))} +
+ + +
+ {project.links.map((link, i) => ( + + {link} + + ))} +
+ + + +
0.5 - ? 'black' - : 'white', - }} + tabIndex={0} + role="button" + className="btn btn-square btn-ghost btn-sm lg:hidden" > - {tag.tag} +
- ))} -
- - -
- {project.links.map((link, i) => ( - - {link} - - ))} -
- - - -
-
- + +
-
- -
-
- - - ) - })} + + + ) + })}
diff --git a/web/src/components/Project/ProjectsShowcase/ProjectsShowcase.tsx b/web/src/components/Project/ProjectsShowcase/ProjectsShowcase.tsx index 22396be..81a07eb 100644 --- a/web/src/components/Project/ProjectsShowcase/ProjectsShowcase.tsx +++ b/web/src/components/Project/ProjectsShowcase/ProjectsShowcase.tsx @@ -1,3 +1,5 @@ +import { useLayoutEffect, useRef, useState } from 'react' + import { format, isAfter, startOfToday } from 'date-fns' import { FindProjects } from 'types/graphql' @@ -6,54 +8,101 @@ import { Link, routes } from '@redwoodjs/router' import AutoCarousel from 'src/components/AutoCarousel/AutoCarousel' import { calculateLuminance } from 'src/lib/color' -const ProjectsShowcase = ({ projects }: FindProjects) => ( -
- {projects - .slice() - .sort((a, b) => (isAfter(new Date(b.date), new Date(a.date)) ? 1 : -1)) - .map((project, i) => ( - -
- {project.images.length > 0 && ( - - )} -
-
-

{project.title}

-
-
{project.description}
-
-
- {isAfter(new Date(project.date), startOfToday()) && ( -
planned
- )} -
- {format(project.date, 'yyyy-MM-dd')} +const CARD_WIDTH = 384 + +const ProjectsShowcase = ({ projects }: FindProjects) => { + const ref = useRef(null) + const [columns, setColumns] = useState( + Math.max( + Math.floor(ref.current?.getBoundingClientRect().width / CARD_WIDTH), + 1 + ) + ) + + useLayoutEffect(() => { + const handleResize = () => + setColumns( + Math.max( + Math.floor(ref.current?.getBoundingClientRect().width / CARD_WIDTH), + 1 + ) + ) + + handleResize() + + window.addEventListener('resize', handleResize) + + return () => window.removeEventListener('resize', handleResize) + }, []) + + return ( +
+ {split( + projects + .slice() + .sort((a, b) => + isAfter(new Date(b.date), new Date(a.date)) ? 1 : -1 + ), + columns + ).map((projectChunk, i) => ( +
+ {projectChunk.map((project, j) => ( + +
+ {project.images.length > 0 && ( + + )} +
+
+

{project.title}

+
+
{project.description}
+
+
+ {isAfter(new Date(project.date), startOfToday()) && ( +
planned
+ )} +
+ {format(project.date, 'yyyy-MM-dd')} +
+
+
+ {project.tags.map((tag, i) => ( +
0.5 + ? 'black' + : 'white', + }} + > + {tag.tag} +
+ ))} +
-
- {project.tags.map((tag, i) => ( -
0.5 - ? 'black' - : 'white', - }} - > - {tag.tag} -
- ))} -
-
-
- + + ))} +
))} -
-) +
+ ) +} export default ProjectsShowcase + +function split(arr: T[], chunks: number): T[][] { + const result: T[][] = [] + const chunkSize = Math.ceil(arr.length / chunks) + + for (let i = 0; i < arr.length; i += chunkSize) { + result.push(arr.slice(i, i + chunkSize)) + } + + return result +} diff --git a/web/src/components/Resume/AdminResumeCell/AdminResumeCell.tsx b/web/src/components/Resume/AdminResumeCell/AdminResumeCell.tsx new file mode 100644 index 0000000..140a4e2 --- /dev/null +++ b/web/src/components/Resume/AdminResumeCell/AdminResumeCell.tsx @@ -0,0 +1,35 @@ +import type { AdminFindResume, AdminFindResumeVariables } from 'types/graphql' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty' +import CellFailure from 'src/components/Cell/CellFailure/CellFailure' +import CellLoading from 'src/components/Cell/CellLoading/CellLoading' +import ResumeForm from 'src/components/Resume/ResumeForm/ResumeForm' + +export const QUERY: TypedDocumentNode< + AdminFindResume, + AdminFindResumeVariables +> = gql` + query AdminFindResume { + resume { + id + fileId + } + } +` + +export const Loading = () => +export const Empty = () => +export const Failure = ({ + error, +}: CellFailureProps) => + +export const Success = ({ + resume, +}: CellSuccessProps) => + resume.id === -1 ? : diff --git a/web/src/components/Resume/Resume/Resume.tsx b/web/src/components/Resume/Resume/Resume.tsx new file mode 100644 index 0000000..eb4f6a4 --- /dev/null +++ b/web/src/components/Resume/Resume/Resume.tsx @@ -0,0 +1,26 @@ +import { useState } from 'react' + +import { Resume as ResumeType } from 'types/graphql' + +interface ResumeProps { + resume?: ResumeType +} + +const Resume = ({ resume }: ResumeProps) => { + const [fileId] = useState(resume?.fileId) + + return ( + + ) +} + +export default Resume diff --git a/web/src/components/Resume/ResumeCell/ResumeCell.tsx b/web/src/components/Resume/ResumeCell/ResumeCell.tsx new file mode 100644 index 0000000..6c6fac5 --- /dev/null +++ b/web/src/components/Resume/ResumeCell/ResumeCell.tsx @@ -0,0 +1,33 @@ +import type { FindResume, FindResumeVariables } from 'types/graphql' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty' +import CellFailure from 'src/components/Cell/CellFailure/CellFailure' +import CellLoading from 'src/components/Cell/CellLoading/CellLoading' +import Resume from 'src/components/Resume/Resume/Resume' + +export const QUERY: TypedDocumentNode = gql` + query FindResume { + resume { + id + fileId + } + } +` + +export const Loading = () => +export const Empty = () => +export const Failure = ({ error }: CellFailureProps) => ( + +) + +export const Success = ({ + resume, +}: CellSuccessProps) => ( + +) diff --git a/web/src/components/Resume/ResumeForm/ResumeForm.tsx b/web/src/components/Resume/ResumeForm/ResumeForm.tsx new file mode 100644 index 0000000..42239f4 --- /dev/null +++ b/web/src/components/Resume/ResumeForm/ResumeForm.tsx @@ -0,0 +1,202 @@ +import { useRef, useState } from 'react' + +import { Meta, UploadResult } from '@uppy/core' +import type { + CreateResumeMutation, + CreateResumeMutationVariables, + DeleteResumeMutation, + DeleteResumeMutationVariables, + FindResume, + FindResumeVariables, + Resume, +} from 'types/graphql' + +import { TypedDocumentNode, useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import Uploader from 'src/components/Uploader/Uploader' +import { deleteFile, handleBeforeUnload } from 'src/lib/tus' + +interface ResumeFormProps { + resume?: Resume +} + +export const QUERY: TypedDocumentNode = gql` + query ResumeForm { + resume { + id + fileId + } + } +` + +const DELETE_RESUME_MUTATION: TypedDocumentNode< + DeleteResumeMutation, + DeleteResumeMutationVariables +> = gql` + mutation DeleteResumeMutation { + deleteResume { + id + fileId + } + } +` + +const CREATE_RESUME_MUTATION: TypedDocumentNode< + CreateResumeMutation, + CreateResumeMutationVariables +> = gql` + mutation CreateResumeMutation($input: CreateResumeInput!) { + createResume(input: $input) { + id + fileId + } + } +` + +const ResumeForm = ({ resume }: ResumeFormProps) => { + const [fileId, _setFileId] = useState(resume?.fileId) + const fileIdRef = useRef(fileId) + + const setFileId = (fileId: string) => { + _setFileId(fileId) + fileIdRef.current = fileId + } + + const unloadAbortController = new AbortController() + + const [deleteResume, { loading: deleteLoading }] = useMutation( + DELETE_RESUME_MUTATION, + { + onCompleted: () => { + toast.success('Resume deleted') + }, + onError: (error) => { + toast.error(error.message) + }, + refetchQueries: [{ query: QUERY }], + awaitRefetchQueries: true, + } + ) + + const [createResume, { loading: createLoading }] = useMutation( + CREATE_RESUME_MUTATION, + { + onCompleted: () => toast.success('Resume saved'), + onError: (error) => toast.error(error.message), + refetchQueries: [{ query: QUERY }], + awaitRefetchQueries: true, + } + ) + + const onUploadComplete = ( + result: UploadResult> + ) => { + setFileId(result.successful[0]?.uploadURL) + window.addEventListener( + 'beforeunload', + (e) => handleBeforeUnload(e, [fileIdRef.current]), + { + once: true, + signal: unloadAbortController.signal, + } + ) + } + + if (resume?.fileId) + return ( +
+ +
+ +
+ + ) + else + return ( +
+ {!fileId ? ( + <> + + + ) : ( + + )} + {fileId && ( +
+ + +
+ )} + + ) +} + +export default ResumeForm diff --git a/web/src/components/Tag/Tags/Tags.tsx b/web/src/components/Tag/Tags/Tags.tsx index 6361c60..695dd6a 100644 --- a/web/src/components/Tag/Tags/Tags.tsx +++ b/web/src/components/Tag/Tags/Tags.tsx @@ -137,55 +137,6 @@ const TagsList = ({ tags }: FindTags) => { ) - // return ( - //
- // - // - // - // - // - // - // - // - // - // - // {tags.map((tag) => ( - // - // - // - // - // - // - // ))} - // - //
IdTagColor 
{truncate(tag.id)}{truncate(tag.tag)}{truncate(tag.color)} - // - //
- //
- // ) } export default TagsList diff --git a/web/src/components/Uploader/Uploader.tsx b/web/src/components/Uploader/Uploader.tsx index f3a50e3..001651b 100644 --- a/web/src/components/Uploader/Uploader.tsx +++ b/web/src/components/Uploader/Uploader.tsx @@ -11,6 +11,8 @@ import { isProduction } from '@redwoodjs/api/logger' import '@uppy/core/dist/style.min.css' import '@uppy/dashboard/dist/style.min.css' +type FileType = 'image' | 'pdf' + interface Props { onComplete?(result: UploadResult>): void width?: string | number @@ -19,6 +21,7 @@ interface Props { maxFiles?: number disabled?: boolean hidden?: boolean + type?: FileType } const apiDomain = isProduction @@ -33,16 +36,15 @@ const Uploader = ({ disabled = false, hidden = false, maxFiles = 1, + type = 'image', }: Props) => { const [uppy] = useState(() => { const instance = new Uppy({ restrictions: { - allowedFileTypes: [ - 'image/webp', - 'image/png', - 'image/jpg', - 'image/jpeg', - ], + allowedFileTypes: + type === 'image' + ? ['image/webp', 'image/png', 'image/jpg', 'image/jpeg'] + : type === 'pdf' && ['application/pdf'], maxNumberOfFiles: maxFiles, maxFileSize: 25 * 1024 * 1024, }, diff --git a/web/src/layouts/NavbarLayout/NavbarLayout.tsx b/web/src/layouts/NavbarLayout/NavbarLayout.tsx index c71569a..18313a8 100644 --- a/web/src/layouts/NavbarLayout/NavbarLayout.tsx +++ b/web/src/layouts/NavbarLayout/NavbarLayout.tsx @@ -24,6 +24,10 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => { name: 'Projects', path: routes.projects(), }, + { + name: 'Resume', + path: routes.resume(), + }, { name: 'Contact', path: routes.contact(), @@ -47,6 +51,10 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => { name: 'Portrait', path: routes.portrait(), }, + { + name: 'Resume', + path: routes.adminResume(), + }, ] const navbarButtons = () => diff --git a/web/src/pages/Project/AdminProjectPage/AdminProjectPage.tsx b/web/src/pages/Project/AdminProjectPage/AdminProjectPage.tsx new file mode 100644 index 0000000..4db9441 --- /dev/null +++ b/web/src/pages/Project/AdminProjectPage/AdminProjectPage.tsx @@ -0,0 +1,16 @@ +import { Metadata } from '@redwoodjs/web' + +import AdminProjectCell from 'src/components/Project/AdminProjectCell/AdminProjectCell' + +type ProjectPageProps = { + id: number +} + +const ProjectPage = ({ id }: ProjectPageProps) => ( + <> + + + +) + +export default ProjectPage diff --git a/web/src/pages/Project/AdminProjectsPage/AdminProjectsPage.tsx b/web/src/pages/Project/AdminProjectsPage/AdminProjectsPage.tsx new file mode 100644 index 0000000..ddc861a --- /dev/null +++ b/web/src/pages/Project/AdminProjectsPage/AdminProjectsPage.tsx @@ -0,0 +1,12 @@ +import { Metadata } from '@redwoodjs/web' + +import ProjectsCell from 'src/components/Project/ProjectsCell' + +const ProjectsPage = () => ( + <> + + + +) + +export default ProjectsPage diff --git a/web/src/pages/Project/ProjectPage/ProjectPage.tsx b/web/src/pages/Project/ProjectPage/ProjectPage.tsx index e5ebe22..8039527 100644 --- a/web/src/pages/Project/ProjectPage/ProjectPage.tsx +++ b/web/src/pages/Project/ProjectPage/ProjectPage.tsx @@ -2,15 +2,18 @@ import { Metadata } from '@redwoodjs/web' import ProjectCell from 'src/components/Project/ProjectCell' -type ProjectPageProps = { +interface ProjectPageProps { id: number } -const ProjectPage = ({ id }: ProjectPageProps) => ( - <> - - - -) +const ProjectPage = ({ id }: ProjectPageProps) => { + return ( + <> + + + + + ) +} export default ProjectPage diff --git a/web/src/pages/Project/ProjectsPage/ProjectsPage.tsx b/web/src/pages/Project/ProjectsPage/ProjectsPage.tsx index ddc861a..e622f7a 100644 --- a/web/src/pages/Project/ProjectsPage/ProjectsPage.tsx +++ b/web/src/pages/Project/ProjectsPage/ProjectsPage.tsx @@ -1,12 +1,29 @@ +import { isMobile, isBrowser } from 'react-device-detect' + import { Metadata } from '@redwoodjs/web' -import ProjectsCell from 'src/components/Project/ProjectsCell' +import ProjectsShowcaseCell from 'src/components/Project/ProjectsShowcaseCell' -const ProjectsPage = () => ( - <> - - - -) +const ProjectsPage = () => { + return ( + <> + + +
+
+
+

Projects

+

+ {isBrowser && !isMobile ? 'Click' : 'Tap'} on a project for + details +

+
+
+
+ + + + ) +} export default ProjectsPage diff --git a/web/src/pages/ProjectPage/ProjectPage.tsx b/web/src/pages/ProjectPage/ProjectPage.tsx deleted file mode 100644 index dd3bfb7..0000000 --- a/web/src/pages/ProjectPage/ProjectPage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Metadata } from '@redwoodjs/web' - -interface ProjectPageProps { - id: number -} - -// TODO: implement - -const ProjectPage = ({ id }: ProjectPageProps) => { - return ( - <> - - -

ProjectPage

-

- Find me in ./web/src/pages/ProjectPage/ProjectPage.tsx -

-

My id is: {id}

- {/* - My default route is named `project`, link to me with: - `Project` - */} - - ) -} - -export default ProjectPage diff --git a/web/src/pages/ProjectsPage/ProjectsPage.tsx b/web/src/pages/ProjectsPage/ProjectsPage.tsx deleted file mode 100644 index 34306ce..0000000 --- a/web/src/pages/ProjectsPage/ProjectsPage.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { isMobile, isBrowser } from 'react-device-detect' - -import { Metadata } from '@redwoodjs/web' - -import ProjectsShowcaseCell from 'src/components/Project/ProjectsShowcaseCell' - -const ProjectsPage = () => { - return ( - <> - - -
-
-
-

Projects

-

- {isBrowser && !isMobile ? 'Click' : 'Tap'} on a project for - details -

-
-
-
- - - - ) -} - -export default ProjectsPage diff --git a/web/src/pages/Resume/AdminResumePage/AdminResumePage.tsx b/web/src/pages/Resume/AdminResumePage/AdminResumePage.tsx new file mode 100644 index 0000000..36c289b --- /dev/null +++ b/web/src/pages/Resume/AdminResumePage/AdminResumePage.tsx @@ -0,0 +1,14 @@ +import { Metadata } from '@redwoodjs/web' + +import AdminResumeCell from 'src/components/Resume/AdminResumeCell/AdminResumeCell' + +const ResumePage = () => { + return ( + <> + + + + ) +} + +export default ResumePage diff --git a/web/src/pages/Resume/ResumePage/ResumePage.tsx b/web/src/pages/Resume/ResumePage/ResumePage.tsx new file mode 100644 index 0000000..07cc6d6 --- /dev/null +++ b/web/src/pages/Resume/ResumePage/ResumePage.tsx @@ -0,0 +1,15 @@ +import { Metadata } from '@redwoodjs/web' + +import ResumeCell from 'src/components/Resume/ResumeCell' + +const ResumePage = () => { + return ( + <> + + + + + ) +} + +export default ResumePage