mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-08-02 19:46:40 +08:00
[offers] tweak offer profiles UI
This commit is contained in:
@ -1,5 +1,9 @@
|
|||||||
|
import {
|
||||||
|
ArrowTrendingUpIcon,
|
||||||
|
BuildingOfficeIcon,
|
||||||
|
MapPinIcon,
|
||||||
|
} from '@heroicons/react/20/solid';
|
||||||
import { JobType } from '@prisma/client';
|
import { JobType } from '@prisma/client';
|
||||||
import { HorizontalDivider } from '@tih/ui';
|
|
||||||
|
|
||||||
import type { JobTitleType } from '~/components/shared/JobTitles';
|
import type { JobTitleType } from '~/components/shared/JobTitles';
|
||||||
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
|
||||||
@ -10,12 +14,10 @@ import { formatDate } from '~/utils/offers/time';
|
|||||||
import type { UserProfileOffer } from '~/types/offers';
|
import type { UserProfileOffer } from '~/types/offers';
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
disableTopDivider?: boolean;
|
|
||||||
offer: UserProfileOffer;
|
offer: UserProfileOffer;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default function DashboardProfileCard({
|
export default function DashboardProfileCard({
|
||||||
disableTopDivider,
|
|
||||||
offer: {
|
offer: {
|
||||||
company,
|
company,
|
||||||
income,
|
income,
|
||||||
@ -27,29 +29,53 @@ export default function DashboardProfileCard({
|
|||||||
},
|
},
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="px-4 py-4 sm:px-6">
|
||||||
{!disableTopDivider && <HorizontalDivider />}
|
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
<div className="col-span-1 row-span-3">
|
<div className="col-span-1 row-span-3">
|
||||||
<p className="font-bold">
|
<h4 className="font-medium">
|
||||||
{getLabelForJobTitleType(title as JobTitleType)}
|
{getLabelForJobTitleType(title as JobTitleType)}
|
||||||
</p>
|
</h4>
|
||||||
<p>
|
<div className="mt-1 flex flex-col sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-4">
|
||||||
{location
|
{company?.name && (
|
||||||
? `Company: ${company.name}, ${location}`
|
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||||
: `Company: ${company.name}`}
|
<BuildingOfficeIcon
|
||||||
</p>
|
aria-hidden="true"
|
||||||
{level && <p>Level: {level}</p>}
|
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
/>
|
||||||
|
{company.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{location && (
|
||||||
|
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||||
|
<MapPinIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
/>
|
||||||
|
{location}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{level && (
|
||||||
|
<div className="mt-2 flex items-center text-sm text-gray-500">
|
||||||
|
<ArrowTrendingUpIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
/>
|
||||||
|
{level}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 row-span-3">
|
<div className="col-span-1 row-span-3">
|
||||||
<p className="text-end">{formatDate(monthYearReceived)}</p>
|
<p className="text-end text-lg font-medium leading-6 text-slate-900">
|
||||||
<p className="text-end text-xl">
|
|
||||||
{jobType === JobType.FULLTIME
|
{jobType === JobType.FULLTIME
|
||||||
? `${convertMoneyToString(income)} / year`
|
? `${convertMoneyToString(income)} / year`
|
||||||
: `${convertMoneyToString(income)} / month`}
|
: `${convertMoneyToString(income)} / month`}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-end text-sm text-slate-500">
|
||||||
|
{formatDate(monthYearReceived)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { ArrowRightIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
import { BookmarkSlashIcon } from '@heroicons/react/20/solid';
|
||||||
|
import { ArrowRightIcon } from '@heroicons/react/24/outline';
|
||||||
import { Button, useToast } from '@tih/ui';
|
import { Button, useToast } from '@tih/ui';
|
||||||
|
|
||||||
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
@ -43,30 +44,34 @@ export default function DashboardProfileCard({
|
|||||||
);
|
);
|
||||||
|
|
||||||
function handleRemoveProfile() {
|
function handleRemoveProfile() {
|
||||||
|
// TODO(offers): Confirm before removal.
|
||||||
removeSavedProfileMutation.mutate({ profileId: id });
|
removeSavedProfileMutation.mutate({ profileId: id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 bg-white px-4 pt-5 sm:px-4">
|
<div className="overflow-hidden bg-white sm:rounded-lg sm:shadow">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="-ml-4 -mt-2 flex flex-wrap items-center justify-between border-b border-gray-300 pb-4 sm:flex-nowrap">
|
<div className="border-b border-slate-200 bg-white px-4 py-5 sm:px-6">
|
||||||
<div className="flex items-center gap-x-5">
|
<div className="-ml-4 -mt-4 flex flex-wrap items-center justify-between sm:flex-nowrap">
|
||||||
<div>
|
<div className="ml-4 mt-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
<ProfilePhotoHolder size="sm" />
|
<ProfilePhotoHolder size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-10">
|
<div className="ml-4">
|
||||||
<p className="text-xl font-bold">{profileName}</p>
|
<h2 className="text-lg font-medium leading-6 text-slate-900">
|
||||||
|
{profileName}
|
||||||
<div className="flex flex-row">
|
</h2>
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
<span>Created at {formatDate(createdAt)}</span>
|
<span>Created at {formatDate(createdAt)}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="ml-4 mt-4 flex flex-shrink-0">
|
||||||
<div className="flex self-start">
|
|
||||||
<Button
|
<Button
|
||||||
disabled={removeSavedProfileMutation.isLoading}
|
disabled={removeSavedProfileMutation.isLoading}
|
||||||
icon={XMarkIcon}
|
icon={BookmarkSlashIcon}
|
||||||
isLabelHidden={true}
|
isLabelHidden={true}
|
||||||
label="Remove Profile"
|
label="Remove Profile"
|
||||||
size="md"
|
size="md"
|
||||||
@ -75,22 +80,16 @@ export default function DashboardProfileCard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Offers */}
|
|
||||||
<div>
|
|
||||||
{offers.map((offer: UserProfileOffer, index) =>
|
|
||||||
index === 0 ? (
|
|
||||||
<DashboardOfferCard
|
|
||||||
key={offer.id}
|
|
||||||
disableTopDivider={true}
|
|
||||||
offer={offer}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DashboardOfferCard key={offer.id} offer={offer} />
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end pt-1">
|
{/* List of Offers */}
|
||||||
|
<ul className="divide-y divide-slate-200" role="list">
|
||||||
|
{offers.map((offer: UserProfileOffer) => (
|
||||||
|
<li key={offer.id}>
|
||||||
|
<DashboardOfferCard offer={offer} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex justify-end border-t border-slate-200 px-4 py-5 sm:px-6">
|
||||||
<Button
|
<Button
|
||||||
disabled={removeSavedProfileMutation.isLoading}
|
disabled={removeSavedProfileMutation.isLoading}
|
||||||
icon={ArrowRightIcon}
|
icon={ArrowRightIcon}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { signIn, useSession } from 'next-auth/react';
|
import { signIn, useSession } from 'next-auth/react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { DocumentDuplicateIcon } from '@heroicons/react/20/solid';
|
import { DocumentDuplicateIcon } from '@heroicons/react/20/solid';
|
||||||
import { BookmarkSquareIcon, CheckIcon } from '@heroicons/react/24/outline';
|
import { BookmarkIcon as BookmarkOutlineIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { BookmarkIcon as BookmarkSolidIcon } from '@heroicons/react/24/solid';
|
||||||
import { Button, TextInput, useToast } from '@tih/ui';
|
import { Button, TextInput, useToast } from '@tih/ui';
|
||||||
|
|
||||||
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||||
@ -126,7 +127,7 @@ export default function OffersProfileSave({
|
|||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Button
|
<Button
|
||||||
disabled={isSavedQuery.isLoading || isSaved}
|
disabled={isSavedQuery.isLoading || isSaved}
|
||||||
icon={isSaved ? CheckIcon : BookmarkSquareIcon}
|
icon={isSaved ? BookmarkSolidIcon : BookmarkOutlineIcon}
|
||||||
isLoading={saveMutation.isLoading || isSavedQuery.isLoading}
|
isLoading={saveMutation.isLoading || isSavedQuery.isLoading}
|
||||||
label={isSaved ? 'Added to account' : 'Add to your account'}
|
label={isSaved ? 'Added to account' : 'Add to your account'}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -264,7 +264,7 @@ export default function OffersSubmissionForm({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={pageRef} className="w-full overflow-y-scroll">
|
<div ref={pageRef} className="w-full">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="block w-full max-w-screen-md overflow-hidden rounded-lg sm:shadow-lg md:my-10">
|
<div className="block w-full max-w-screen-md overflow-hidden rounded-lg sm:shadow-lg md:my-10">
|
||||||
<div className="flex justify-center bg-slate-100 px-4 py-4 sm:px-6 lg:px-8">
|
<div className="flex justify-center bg-slate-100 px-4 py-4 sm:px-6 lg:px-8">
|
||||||
|
@ -110,10 +110,10 @@ export default function ProfileComments({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="bh-white h-fit px-4 md:h-[calc(100vh-4.5rem)] md:overflow-y-auto">
|
<div className="bh-white h-fit px-4 lg:h-[calc(100vh-4.5rem)] lg:overflow-y-auto">
|
||||||
<div className="bg-white pt-4 md:sticky md:top-0">
|
<div className="bg-white pt-4 lg:sticky lg:top-0">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<div className="grid w-fit space-y-2 md:grid-cols-1 lg:grid-cols-2 lg:space-y-0 lg:space-x-4">
|
<div className="grid w-fit space-y-2 lg:grid-cols-1 lg:grid-cols-2 lg:space-y-0 lg:space-x-4">
|
||||||
<div className="col-span-1 flex justify-end">
|
<div className="col-span-1 flex justify-end">
|
||||||
{isEditable && (
|
{isEditable && (
|
||||||
<Tooltip tooltipContent="Copy this link to edit your profile later">
|
<Tooltip tooltipContent="Copy this link to edit your profile later">
|
||||||
@ -210,7 +210,7 @@ export default function ProfileComments({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full">
|
<div className="w-full">
|
||||||
{replies?.map((reply: Reply) => (
|
{replies?.map((reply: Reply) => (
|
||||||
<ExpandableCommentCard
|
<ExpandableCommentCard
|
||||||
key={reply.id}
|
key={reply.id}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
type ProfilePhotoHolderProps = {
|
type ProfilePhotoHolderProps = Readonly<{
|
||||||
size?: 'lg' | 'sm';
|
size?: 'lg' | 'sm';
|
||||||
};
|
}>;
|
||||||
|
|
||||||
export default function ProfilePhotoHolder({
|
export default function ProfilePhotoHolder({
|
||||||
size = 'lg',
|
size = 'lg',
|
||||||
|
@ -4,20 +4,21 @@ import React from 'react';
|
|||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
variant?: 'narrow' | 'normal';
|
variant?: 'md' | 'sm' | 'xs';
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default function Container({
|
export default function Container({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
variant = 'normal',
|
variant = 'md',
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mx-auto px-4 sm:px-6 lg:px-8',
|
'mx-auto px-4 sm:px-6 lg:px-8',
|
||||||
variant === 'normal' && 'max-w-7xl',
|
variant === 'md' && 'max-w-7xl',
|
||||||
variant === 'narrow' && 'max-w-6xl',
|
variant === 'sm' && 'max-w-5xl',
|
||||||
|
variant === 'xs' && 'max-w-3xl',
|
||||||
className,
|
className,
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -4,6 +4,7 @@ import { useState } from 'react';
|
|||||||
import { Button, Spinner } from '@tih/ui';
|
import { Button, Spinner } from '@tih/ui';
|
||||||
|
|
||||||
import DashboardProfileCard from '~/components/offers/dashboard/DashboardProfileCard';
|
import DashboardProfileCard from '~/components/offers/dashboard/DashboardProfileCard';
|
||||||
|
import Container from '~/components/shared/Container';
|
||||||
|
|
||||||
import { trpc } from '~/utils/trpc';
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
@ -30,19 +31,21 @@ export default function ProfilesDashboard() {
|
|||||||
|
|
||||||
if (status === 'loading' || userProfilesQuery.isLoading) {
|
if (status === 'loading' || userProfilesQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen">
|
<div className="flex w-full">
|
||||||
<div className="m-auto mx-auto w-full justify-center">
|
<div className="m-auto mx-auto w-full justify-center">
|
||||||
<Spinner className="m-10" display="block" size="lg" />
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'unauthenticated') {
|
if (status === 'unauthenticated') {
|
||||||
signIn();
|
signIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userProfiles.length === 0) {
|
if (userProfiles.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen">
|
<div className="flex w-full">
|
||||||
<div className="m-auto mx-auto w-full justify-center text-xl">
|
<div className="m-auto mx-auto w-full justify-center text-xl">
|
||||||
<div className="mb-8 flex w-full flex-row justify-center">
|
<div className="mb-8 flex w-full flex-row justify-center">
|
||||||
<h2>You have not saved any offer profiles yet.</h2>
|
<h2>You have not saved any offer profiles yet.</h2>
|
||||||
@ -59,10 +62,11 @@ export default function ProfilesDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Container variant="xs">
|
||||||
{userProfilesQuery.isLoading && (
|
{userProfilesQuery.isLoading && (
|
||||||
<div className="flex h-screen w-screen">
|
<div className="flex h-screen">
|
||||||
<div className="m-auto mx-auto w-full justify-center">
|
<div className="m-auto mx-auto w-full justify-center">
|
||||||
<Spinner className="m-10" display="block" size="lg" />
|
<Spinner className="m-10" display="block" size="lg" />
|
||||||
</div>
|
</div>
|
||||||
@ -70,19 +74,17 @@ export default function ProfilesDashboard() {
|
|||||||
)}
|
)}
|
||||||
{!userProfilesQuery.isLoading && (
|
{!userProfilesQuery.isLoading && (
|
||||||
<div className="overflow-y-auto py-8">
|
<div className="overflow-y-auto py-8">
|
||||||
<h1 className="mx-auto mb-4 w-3/4 text-start text-4xl font-bold text-slate-900">
|
<h1 className="mx-auto mb-4 text-start text-4xl font-bold text-slate-900">
|
||||||
Your dashboard
|
Your dashboard
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mx-auto w-3/4 text-start text-xl text-slate-900">
|
<p className="mt-4 text-xl leading-8 text-slate-500">
|
||||||
Save your offer profiles to dashboard to easily access and edit them
|
Save your offer profiles to your dashboard to easily access and edit
|
||||||
later.
|
them later.
|
||||||
</p>
|
</p>
|
||||||
<div className="justfy-center mt-8 flex w-screen">
|
<div className="mt-8 flex justify-center">
|
||||||
<ul className="mx-auto w-3/4 space-y-3" role="list">
|
<ul className="w-full space-y-4" role="list">
|
||||||
{userProfiles?.map((profile) => (
|
{userProfiles?.map((profile) => (
|
||||||
<li
|
<li key={profile.id}>
|
||||||
key={profile.id}
|
|
||||||
className="overflow-hidden bg-white px-4 py-4 shadow sm:rounded-md sm:px-6">
|
|
||||||
<DashboardProfileCard key={profile.id} profile={profile} />
|
<DashboardProfileCard key={profile.id} profile={profile} />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@ -90,6 +92,6 @@ export default function ProfilesDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -202,9 +202,9 @@ export default function OfferProfile() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!getProfileQuery.isLoading && !getProfileQuery.isError && (
|
{!getProfileQuery.isLoading && !getProfileQuery.isError && (
|
||||||
<div className="h-fuill flex grid w-full grid-cols-1 items-center justify-center divide-x overflow-y-auto md:grid-cols-3">
|
<div className="w-full divide-x lg:flex">
|
||||||
<div className="col-span-1 flex h-full flex-col divide-y md:col-span-2 md:overflow-y-auto">
|
<div className="divide-y lg:w-2/3">
|
||||||
<div className="h-fit md:sticky md:top-0">
|
<div className="h-fit">
|
||||||
<ProfileHeader
|
<ProfileHeader
|
||||||
background={background}
|
background={background}
|
||||||
handleDelete={handleDelete}
|
handleDelete={handleDelete}
|
||||||
@ -226,7 +226,9 @@ export default function OfferProfile() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 h-full bg-white">
|
<div
|
||||||
|
className="bg-white lg:fixed lg:right-0 lg:bottom-0 lg:w-1/3"
|
||||||
|
style={{ top: 64 }}>
|
||||||
<ProfileComments
|
<ProfileComments
|
||||||
isDisabled={deleteMutation.isLoading}
|
isDisabled={deleteMutation.isLoading}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
|
@ -78,7 +78,7 @@ export default function OffersSubmissionResult() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!getAnalysis.isLoading && (
|
{!getAnalysis.isLoading && (
|
||||||
<div ref={pageRef} className="w-full overflow-y-scroll">
|
<div ref={pageRef} className="w-full">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="block w-full max-w-screen-md overflow-hidden rounded-lg sm:shadow-lg md:my-10">
|
<div className="block w-full max-w-screen-md overflow-hidden rounded-lg sm:shadow-lg md:my-10">
|
||||||
<div className="flex justify-center bg-slate-100 px-4 py-4 sm:px-6 lg:px-8">
|
<div className="flex justify-center bg-slate-100 px-4 py-4 sm:px-6 lg:px-8">
|
||||||
|
Reference in New Issue
Block a user