- {projects
- .slice()
- .sort((a, b) => (isAfter(new Date(b.date), new Date(a.date)) ? 1 : -1))
- .map((project, i) => (
-
- {project.images.length > 0 && (
-
- )}
-
-
-
{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.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 (
- //
- //
- //
- //
- // Id |
- // Tag |
- // Color |
- // |
- //
- //
- //
- // {tags.map((tag) => (
- //
- // {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