diff --git a/apps/portal/src/components/global/ProductNavigation.tsx b/apps/portal/src/components/global/ProductNavigation.tsx index e34cadd7..caae6c08 100644 --- a/apps/portal/src/components/global/ProductNavigation.tsx +++ b/apps/portal/src/components/global/ProductNavigation.tsx @@ -25,11 +25,11 @@ export default function ProductNavigation({ items, title }: Props) { {items.map((item) => item.children != null && item.children.length > 0 ? ( - + {item.name} ( {child.name} @@ -63,7 +63,7 @@ export default function ProductNavigation({ items, title }: Props) { ) : ( {item.name} diff --git a/apps/portal/src/components/resumes/browse/BrowseListItem.tsx b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx similarity index 87% rename from apps/portal/src/components/resumes/browse/BrowseListItem.tsx rename to apps/portal/src/components/resumes/browse/ResumeListItem.tsx index 3c10a463..a76a503a 100644 --- a/apps/portal/src/components/resumes/browse/BrowseListItem.tsx +++ b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx @@ -35,8 +35,11 @@ export default function BrowseListItem({ href, resumeInfo }: Props) {
- Uploaded {formatDistanceToNow(resumeInfo.createdAt)} ago by{' '} - {resumeInfo.user} +
+ Uploaded {formatDistanceToNow(resumeInfo.createdAt)} ago by{' '} + {resumeInfo.user} +
+
{resumeInfo.location}
diff --git a/apps/portal/src/components/resumes/browse/ResumeListItems.tsx b/apps/portal/src/components/resumes/browse/ResumeListItems.tsx new file mode 100644 index 00000000..9e1203a5 --- /dev/null +++ b/apps/portal/src/components/resumes/browse/ResumeListItems.tsx @@ -0,0 +1,35 @@ +import { Spinner } from '@tih/ui'; + +import ResumseListItem from './ResumeListItem'; + +import type { Resume } from '~/types/resume'; + +type Props = Readonly<{ + isLoading: boolean; + resumes: Array; +}>; + +export default function ResumeListItems({ isLoading, resumes }: Props) { + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
    + {resumes.map((resumeObj: Resume) => ( +
  • + +
  • + ))} +
+
+ ); +} diff --git a/apps/portal/src/components/resumes/browse/constants.ts b/apps/portal/src/components/resumes/browse/constants.ts index 19f3035b..6996538a 100644 --- a/apps/portal/src/components/resumes/browse/constants.ts +++ b/apps/portal/src/components/resumes/browse/constants.ts @@ -54,7 +54,7 @@ export const EXPERIENCE = [ export const LOCATION = [ { checked: false, label: 'Singapore', value: 'Singapore' }, - { checked: false, label: 'United States', value: 'Usa' }, + { checked: false, label: 'United States', value: 'United States' }, { checked: false, label: 'India', value: 'India' }, ]; diff --git a/apps/portal/src/components/resumes/comments/CommentListItems.tsx b/apps/portal/src/components/resumes/comments/CommentListItems.tsx new file mode 100644 index 00000000..f043b78c --- /dev/null +++ b/apps/portal/src/components/resumes/comments/CommentListItems.tsx @@ -0,0 +1,35 @@ +import { useSession } from 'next-auth/react'; +import { Spinner } from '@tih/ui'; + +import Comment from './comment/Comment'; + +import type { ResumeComment } from '~/types/resume-comments'; + +type Props = Readonly<{ + comments: Array; + isLoading: boolean; +}>; + +export default function CommentListItems({ comments, isLoading }: Props) { + const { data: session } = useSession(); + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {comments.map((comment) => ( + + ))} +
+ ); +} diff --git a/apps/portal/src/components/resumes/comments/CommentsList.tsx b/apps/portal/src/components/resumes/comments/CommentsList.tsx index 7e15e4d8..d40b500a 100644 --- a/apps/portal/src/components/resumes/comments/CommentsList.tsx +++ b/apps/portal/src/components/resumes/comments/CommentsList.tsx @@ -1,10 +1,9 @@ -import { useSession } from 'next-auth/react'; import { useState } from 'react'; import { Tabs } from '@tih/ui'; import { trpc } from '~/utils/trpc'; -import Comment from './comment/Comment'; +import CommentListItems from './CommentListItems'; import CommentsListButton from './CommentsListButton'; import { COMMENTS_SECTIONS } from './constants'; @@ -18,13 +17,9 @@ export default function CommentsList({ setShowCommentsForm, }: CommentsListProps) { const [tab, setTab] = useState(COMMENTS_SECTIONS[0].value); - const { data: session } = useSession(); - // Fetch the most updated comments to render const commentsQuery = trpc.useQuery(['resumes.reviews.list', { resumeId }]); - // TODO: Add loading prompt - return (
@@ -34,20 +29,10 @@ export default function CommentsList({ value={tab} onChange={(value) => setTab(value)} /> - -
- {commentsQuery.data - ?.filter((c) => c.section === tab) - .map((comment) => { - return ( - - ); - })} -
+ c.section === tab) ?? []} + isLoading={commentsQuery.isFetching} + />
); } diff --git a/apps/portal/src/components/shared/CompaniesTypeahead.tsx b/apps/portal/src/components/shared/CompaniesTypeahead.tsx new file mode 100644 index 00000000..b25f9cd8 --- /dev/null +++ b/apps/portal/src/components/shared/CompaniesTypeahead.tsx @@ -0,0 +1,40 @@ +import { useState } from 'react'; +import type { TypeaheadOption } from '@tih/ui'; +import { Typeahead } from '@tih/ui'; + +import { trpc } from '~/utils/trpc'; + +type Props = Readonly<{ + disabled?: boolean; + onSelect: (option: TypeaheadOption) => void; +}>; + +export default function CompaniesTypeahead({ disabled, onSelect }: Props) { + const [query, setQuery] = useState(''); + const companies = trpc.useQuery([ + 'companies.list', + { + name: query, + }, + ]); + + const { data } = companies; + + return ( + ({ + id, + label: name, + value: id, + })) ?? [] + } + onQueryChange={setQuery} + onSelect={onSelect} + /> + ); +} diff --git a/apps/portal/src/components/shared/MonthYearPicker.tsx b/apps/portal/src/components/shared/MonthYearPicker.tsx new file mode 100644 index 00000000..bb6539f1 --- /dev/null +++ b/apps/portal/src/components/shared/MonthYearPicker.tsx @@ -0,0 +1,96 @@ +import { Select } from '@tih/ui'; + +type Month = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + +export type MonthYear = Readonly<{ + month: Month; + year: number; +}>; + +type Props = Readonly<{ + onChange: (value: MonthYear) => void; + value: MonthYear; +}>; + +const MONTH_OPTIONS = [ + { + label: 'January', + value: 1, + }, + { + label: 'February', + value: 2, + }, + { + label: 'March', + value: 3, + }, + { + label: 'April', + value: 4, + }, + { + label: 'May', + value: 5, + }, + { + label: 'June', + value: 6, + }, + { + label: 'July', + value: 7, + }, + { + label: 'August', + value: 8, + }, + { + label: 'September', + value: 9, + }, + { + label: 'October', + value: 10, + }, + { + label: 'November', + value: 11, + }, + { + label: 'December', + value: 12, + }, +]; + +const NUM_YEARS = 5; +const YEAR_OPTIONS = Array.from({ length: NUM_YEARS }, (_, i) => { + const year = new Date().getFullYear() - NUM_YEARS + i + 1; + return { + label: String(year), + value: year, + }; +}); + +export default function MonthYearPicker({ value, onChange }: Props) { + return ( +
+ + onChange({ month: value.month, year: Number(newYear) }) + } + /> +
+ ); +} diff --git a/apps/portal/src/pages/index.tsx b/apps/portal/src/pages/index.tsx index c5bf7af5..1a8a1b64 100644 --- a/apps/portal/src/pages/index.tsx +++ b/apps/portal/src/pages/index.tsx @@ -1,11 +1,32 @@ +import { useState } from 'react'; +import type { TypeaheadOption } from '@tih/ui'; +import { HorizontalDivider } from '@tih/ui'; + +import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; +import type { MonthYear } from '~/components/shared/MonthYearPicker'; +import MonthYearPicker from '~/components/shared/MonthYearPicker'; + export default function HomePage() { + const [selectedCompany, setSelectedCompany] = + useState(null); + const [monthYear, setMonthYear] = useState({ + month: new Date().getMonth() + 1, + year: new Date().getFullYear(), + }); + return (
-
+

Homepage

+ setSelectedCompany(option)} + /> +
{JSON.stringify(selectedCompany, null, 2)}
+ +
diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx index d56c1135..52d4f3b7 100644 --- a/apps/portal/src/pages/resumes/[resumeId].tsx +++ b/apps/portal/src/pages/resumes/[resumeId].tsx @@ -27,17 +27,17 @@ export default function ResumeReviewPage() { const { data: session } = useSession(); const router = useRouter(); const { resumeId } = router.query; - const utils = trpc.useContext(); + const trpcContext = trpc.useContext(); // Safe to assert resumeId type as string because query is only sent if so const detailsQuery = trpc.useQuery( - ['resumes.details.find', { resumeId: resumeId as string }], + ['resumes.resume.findOne', { resumeId: resumeId as string }], { - enabled: typeof resumeId === 'string' && session?.user?.id !== undefined, + enabled: typeof resumeId === 'string', }, ); - const starMutation = trpc.useMutation('resumes.details.update_star', { + const starMutation = trpc.useMutation('resumes.star.user.create_or_delete', { onSuccess() { - utils.invalidateQueries(); + trpcContext.invalidateQueries(['resumes.resume.findOne']); }, }); diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx index fd0a304f..fab32bfe 100644 --- a/apps/portal/src/pages/resumes/index.tsx +++ b/apps/portal/src/pages/resumes/index.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { useSession } from 'next-auth/react'; -import { Fragment, useEffect, useState } from 'react'; +import { Fragment, useState } from 'react'; import { Disclosure, Menu, Transition } from '@headlessui/react'; import { ChevronDownIcon, @@ -10,9 +10,8 @@ import { PlusIcon, } from '@heroicons/react/20/solid'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; -import { Spinner, Tabs, TextInput } from '@tih/ui'; +import { Tabs, TextInput } from '@tih/ui'; -import BrowseListItem from '~/components/resumes/browse/BrowseListItem'; import { BROWSE_TABS_VALUES, EXPERIENCE, @@ -22,6 +21,10 @@ import { TOP_HITS, } from '~/components/resumes/browse/constants'; import FilterPill from '~/components/resumes/browse/FilterPill'; +import ResumeListItems from '~/components/resumes/browse/ResumeListItems'; +import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle'; + +import { trpc } from '~/utils/trpc'; import type { Resume } from '~/types/resume'; @@ -42,54 +45,41 @@ const filters = [ options: LOCATION, }, ]; -import ResumeReviewsTitle from '~/components/resumes/ResumeReviewsTitle'; - -import { trpc } from '~/utils/trpc'; export default function ResumeHomePage() { - const { data } = useSession(); + const { data: sessionData } = useSession(); const router = useRouter(); const [tabsValue, setTabsValue] = useState(BROWSE_TABS_VALUES.ALL); const [searchValue, setSearchValue] = useState(''); const [resumes, setResumes] = useState>([]); - const allResumesQuery = trpc.useQuery(['resumes.resume.all'], { + const allResumesQuery = trpc.useQuery(['resumes.resume.findAll'], { enabled: tabsValue === BROWSE_TABS_VALUES.ALL, + onSuccess: (data) => { + setResumes(data); + }, }); - const starredResumesQuery = trpc.useQuery(['resumes.resume.browse.stars'], { - enabled: tabsValue === BROWSE_TABS_VALUES.STARRED, - }); - const myResumesQuery = trpc.useQuery(['resumes.resume.browse.my'], { - enabled: tabsValue === BROWSE_TABS_VALUES.MY, - }); - - useEffect(() => { - switch (tabsValue) { - case BROWSE_TABS_VALUES.ALL: { - setResumes(allResumesQuery.data ?? []); - break; - } - case BROWSE_TABS_VALUES.STARRED: { - setResumes(starredResumesQuery.data ?? []); - break; - } - case BROWSE_TABS_VALUES.MY: { - setResumes(myResumesQuery.data ?? []); - break; - } - default: { - setResumes([]); - } - } - }, [ - allResumesQuery.data, - starredResumesQuery.data, - myResumesQuery.data, - tabsValue, - ]); + const starredResumesQuery = trpc.useQuery( + ['resumes.resume.user.findUserStarred'], + { + enabled: tabsValue === BROWSE_TABS_VALUES.STARRED, + onSuccess: (data) => { + setResumes(data); + }, + }, + ); + const myResumesQuery = trpc.useQuery( + ['resumes.resume.user.findUserCreated'], + { + enabled: tabsValue === BROWSE_TABS_VALUES.MY, + onSuccess: (data) => { + setResumes(data); + }, + }, + ); const onClickNew = () => { - if (data?.user?.id) { + if (sessionData?.user?.id) { router.push('/resumes/submit'); } else { // TODO: Handle non-logged in user behaviour @@ -150,6 +140,7 @@ export default function ResumeHomePage() {
+ {/* TODO: Sort logic */} Sort
- {allResumesQuery.isLoading || - starredResumesQuery.isLoading || - myResumesQuery.isLoading ? ( -
- -
- ) : ( -
-
    - {resumes.map((resumeObj) => ( -
  • - -
  • - ))} -
-
- )} + diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx index e6adec0f..1ca06031 100644 --- a/apps/portal/src/pages/resumes/submit.tsx +++ b/apps/portal/src/pages/resumes/submit.tsx @@ -40,6 +40,7 @@ export default function SubmitResumeForm() { const router = useRouter(); const [resumeFile, setResumeFile] = useState(); + const [isLoading, setIsLoading] = useState(false); const [invalidFileUploadError, setInvalidFileUploadError] = useState< string | null >(null); @@ -50,12 +51,18 @@ export default function SubmitResumeForm() { setValue, reset, formState: { errors }, - } = useForm(); + } = useForm({ + defaultValues: { + isChecked: false, + }, + }); const onSubmit: SubmitHandler = async (data) => { if (resumeFile == null) { + console.error('Resume file is empty'); return; } + setIsLoading(true); const formData = new FormData(); formData.append('key', RESUME_STORAGE_KEY); @@ -68,15 +75,27 @@ export default function SubmitResumeForm() { }); const { url } = res.data; - await resumeCreateMutation.mutate({ - additionalInfo: data.additionalInfo, - experience: data.experience, - location: data.location, - role: data.role, - title: data.title, - url, - }); - router.push('/resumes'); + resumeCreateMutation.mutate( + { + additionalInfo: data.additionalInfo, + experience: data.experience, + location: data.location, + role: data.role, + title: data.title, + url, + }, + { + onError: (error) => { + console.error(error); + }, + onSettled: () => { + setIsLoading(false); + }, + onSuccess: () => { + router.push('/resumes'); + }, + }, + ); }; const onUploadFile = (event: React.ChangeEvent) => { @@ -255,6 +274,7 @@ export default function SubmitResumeForm() {