[offers][feat] add event tracking and save to profile in submisison page (#465)

* [offers][feat] add event tracking and save to profile in form

* [offers][refactor] refactor feature page

* [offers][fix] fix offer table border for action column
pull/466/head
Zhang Ziqing 2 years ago committed by GitHub
parent 521ade6cf0
commit e62c2ae50f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,7 +12,6 @@ const navigationAuthenticated: ProductNavigationItems = [
];
const config = {
// TODO: Change this to your own GA4 measurement ID.
googleAnalyticsMeasurementID: 'G-34XRGLEVCF',
logo: (
<img alt="Tech Offers Repo" className="h-8 w-auto" src="/offers-logo.svg" />

@ -2,6 +2,7 @@ import { useRouter } from 'next/router';
import { ArrowRightIcon, XMarkIcon } from '@heroicons/react/24/outline';
import { Button, useToast } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import DashboardOfferCard from '~/components/offers/dashboard/DashboardOfferCard';
import { formatDate } from '~/utils/offers/time';
@ -10,7 +11,6 @@ import { trpc } from '~/utils/trpc';
import ProfilePhotoHolder from '../profile/ProfilePhotoHolder';
import type { UserProfile, UserProfileOffer } from '~/types/offers';
type Props = Readonly<{
profile: UserProfile;
}>;
@ -22,6 +22,7 @@ export default function DashboardProfileCard({
const router = useRouter();
const trpcContext = trpc.useContext();
const PROFILE_URL = `/offers/profile/${id}?token=${token}`;
const { event: gaEvent } = useGoogleAnalytics();
const removeSavedProfileMutation = trpc.useMutation(
'offers.user.profile.removeFromUserProfile',
{
@ -97,7 +98,14 @@ export default function DashboardProfileCard({
label="Read full profile"
size="md"
variant="secondary"
onClick={() => router.push(PROFILE_URL)}
onClick={() => {
gaEvent({
action: 'offers.view_profile_from_dashboard',
category: 'engagement',
label: 'View profile from dashboard',
});
router.push(PROFILE_URL);
}}
/>
</div>
</div>

@ -1,9 +1,14 @@
// Import { useState } from 'react';
// import { setTimeout } from 'timers';
import { useState } from 'react';
import { DocumentDuplicateIcon } from '@heroicons/react/20/solid';
import { BookmarkSquareIcon, CheckIcon } from '@heroicons/react/24/outline';
import { Button, TextInput, useToast } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import { copyProfileLink, getProfileLink } from '~/utils/offers/link';
import { trpc } from '~/utils/trpc';
type OfferProfileSaveProps = Readonly<{
profileId: string;
@ -15,16 +20,39 @@ export default function OffersProfileSave({
token,
}: OfferProfileSaveProps) {
const { showToast } = useToast();
// Const [isSaving, setSaving] = useState(false);
// const [isSaved, setSaved] = useState(false);
const { event: gaEvent } = useGoogleAnalytics();
const [isSaved, setSaved] = useState(false);
const saveMutation = trpc.useMutation(
['offers.user.profile.addToUserProfile'],
{
onError: () => {
showToast({
title: `Failed to saved to dashboard!`,
variant: 'failure',
});
},
onSuccess: () => {
showToast({
title: `Saved to your repository!`,
variant: 'success',
});
},
},
);
// Const saveProfile = () => {
// setSaving(true);
// setTimeout(() => {
// setSaving(false);
// setSaved(true);
// }, 5);
// };
const handleSave = () => {
saveMutation.mutate({
profileId,
token: token as string,
});
setSaved(true);
gaEvent({
action: 'offers.profile_submission_save_to_profile',
category: 'engagement',
label: 'Save to profile in profile submission',
});
};
return (
<div className="flex w-full justify-center">
@ -57,24 +85,29 @@ export default function OffersProfileSave({
title: `Profile edit link copied to clipboard!`,
variant: 'success',
});
gaEvent({
action: 'offers.profile_submission_copy_edit_profile_link',
category: 'engagement',
label: 'Copy Edit Profile Link in Profile Submission',
});
}}
/>
</div>
{/* <p className="mb-5 text-slate-900">
<p className="mb-5 text-slate-900">
If you do not want to keep the edit link, you can opt to save this
profile under your user account. It will still only be editable by
you.
profile under your account's respository. It will still only be
editable by you.
</p>
<div className="mb-20">
<Button
disabled={isSaved}
icon={isSaved ? CheckIcon : BookmarkSquareIcon}
isLoading={isSaving}
isLoading={saveMutation.isLoading}
label={isSaved ? 'Saved to user profile' : 'Save to user profile'}
variant="primary"
onClick={saveProfile}
onClick={handleSave}
/>
</div> */}
</div>
</div>
</div>
);

@ -6,6 +6,7 @@ import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { JobType } from '@prisma/client';
import { Button } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import type { BreadcrumbStep } from '~/components/offers/Breadcrumb';
import { Breadcrumbs } from '~/components/offers/Breadcrumb';
import BackgroundForm from '~/components/offers/offersSubmission/submissionForm/BackgroundForm';
@ -101,6 +102,7 @@ export default function OffersSubmissionForm({
token: editToken,
});
const [isSubmitted, setIsSubmitted] = useState(false);
const { event: gaEvent } = useGoogleAnalytics();
const router = useRouter();
const pageRef = useRef<HTMLDivElement>(null);
@ -215,6 +217,11 @@ export default function OffersSubmissionForm({
} else {
createOrUpdateMutation.mutate({ background, offers });
}
gaEvent({
action: 'offers.submit_profile',
category: 'submission',
label: 'Submit profile',
});
};
useEffect(() => {
@ -278,7 +285,14 @@ export default function OffersSubmissionForm({
icon={ArrowRightIcon}
label="Next"
variant="secondary"
onClick={() => goToNextStep(step)}
onClick={() => {
goToNextStep(step);
gaEvent({
action: 'offers.profile_submission_navigate_next',
category: 'submission',
label: 'Navigate next',
});
}}
/>
</div>
)}
@ -288,7 +302,14 @@ export default function OffersSubmissionForm({
icon={ArrowLeftIcon}
label="Previous"
variant="secondary"
onClick={() => setStep(step - 1)}
onClick={() => {
setStep(step - 1);
gaEvent({
action: 'offers.profile_submission_navigation_back',
category: 'submission',
label: 'Navigate back',
});
}}
/>
<Button
disabled={isSubmitting || isSubmitSuccessful}

@ -9,6 +9,7 @@ import {
useToast,
} from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
import Tooltip from '~/components/offers/util/Tooltip';
@ -40,6 +41,7 @@ export default function ProfileComments({
const [currentReply, setCurrentReply] = useState<string>('');
const [replies, setReplies] = useState<Array<Reply>>();
const { showToast } = useToast();
const { event: gaEvent } = useGoogleAnalytics();
const commentsQuery = trpc.useQuery(
['offers.comments.getComments', { profileId }],
@ -121,6 +123,11 @@ export default function ProfileComments({
variant="secondary"
onClick={() => {
copyProfileLink(profileId, token);
gaEvent({
action: 'offers.copy_profile_edit_link',
category: 'engagement',
label: 'Copy Profile Edit Link',
});
showToast({
title: `Profile edit link copied to clipboard!`,
variant: 'success',
@ -140,6 +147,11 @@ export default function ProfileComments({
variant="secondary"
onClick={() => {
copyProfileLink(profileId);
gaEvent({
action: 'offers.copy_profile_public_link',
category: 'engagement',
label: 'Copy Profile Public Link',
});
showToast({
title: `Public profile link copied to clipboard!`,
variant: 'success',

@ -27,7 +27,7 @@ export default function OfferTableRow({
<td className="py-4 px-6">{formatDate(monthYearReceived)}</td>
<td
className={clsx(
'sticky right-0 bg-white py-4 px-6 drop-shadow md:drop-shadow-none',
'sticky right-0 py-4 px-6 drop-shadow md:drop-shadow-none',
)}>
<Link
className="text-primary-600 dark:text-primary-500 font-medium hover:underline"

@ -2,6 +2,7 @@ import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { DropdownMenu, Spinner } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import OffersTablePagination from '~/components/offers/table/OffersTablePagination';
import {
OfferTableFilterOptions,
@ -39,6 +40,7 @@ export default function OffersTable({
const [selectedFilter, setSelectedFilter] = useState(
OfferTableFilterOptions[0].value,
);
const { event: gaEvent } = useGoogleAnalytics();
useEffect(() => {
setPagination({
currentPage: 0,
@ -90,13 +92,18 @@ export default function OffersTable({
label={itemLabel}
onClick={() => {
setSelectedTab(value);
gaEvent({
action: `offers.table_filter_yoe_category_${value}`,
category: 'engagement',
label: 'Filter by YOE category',
});
}}
/>
))}
</DropdownMenu>
<div className="divide-x-slate-200 flex items-center space-x-4 divide-x">
<div className="justify-left flex items-center space-x-2">
<span>All offers in</span>
<span>View all offers in</span>
<CurrencySelector
handleCurrencyChange={(value: string) => setCurrency(value)}
selectedCurrency={currency}

@ -8,11 +8,11 @@ import {
UsersIcon,
} from '@heroicons/react/24/outline';
import offersAnalysis from '~/components/offers/landing/images/offers-analysis.png';
import offersBrowse from '~/components/offers/landing/images/offers-browse.png';
import offersProfile from '~/components/offers/landing/images/offers-profile.png';
import LeftTextCard from '~/components/offers/landing/LeftTextCard';
import RightTextCard from '~/components/offers/landing/RightTextCard';
import offersAnalysis from '~/components/offers/features/images/offers-analysis.png';
import offersBrowse from '~/components/offers/features/images/offers-browse.png';
import offersProfile from '~/components/offers/features/images/offers-profile.png';
import LeftTextCard from '~/components/offers/features/LeftTextCard';
import RightTextCard from '~/components/offers/features/RightTextCard';
import { HOME_URL } from '~/components/offers/types';
const features = [
@ -38,32 +38,32 @@ const features = [
const footerNavigation = {
social: [
{
href: '#',
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path
clipRule="evenodd"
d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"
fillRule="evenodd"
/>
</svg>
),
name: 'Facebook',
},
{
href: '#',
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path
clipRule="evenodd"
d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
fillRule="evenodd"
/>
</svg>
),
name: 'Instagram',
},
// {
// href: '#',
// icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
// <svg fill="currentColor" viewBox="0 0 24 24" {...props}>
// <path
// clipRule="evenodd"
// d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"
// fillRule="evenodd"
// />
// </svg>
// ),
// name: 'Facebook',
// },
// {
// href: '#',
// icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
// <svg fill="currentColor" viewBox="0 0 24 24" {...props}>
// <path
// clipRule="evenodd"
// d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
// fillRule="evenodd"
// />
// </svg>
// ),
// name: 'Instagram',
// },
{
href: 'https://github.com/yangshun/tech-interview-handbook',
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
@ -87,6 +87,11 @@ export default function LandingPage() {
{/* Hero section */}
<div className="relative h-full">
<div className="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8">
<img
alt="Tech Offers Repo"
className="mx-auto mb-8 w-auto"
src="/offers-logo.svg"
/>
<h1 className="text-center text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
<span>Choosing offers </span>
<span className="from-primary-600 -mb-1 mr-2 bg-gradient-to-r to-purple-500 bg-clip-text pb-1 pr-4 italic text-transparent">
@ -121,16 +126,16 @@ export default function LandingPage() {
/>
<div className="relative">
<LeftTextCard
description="An offer profile includes not only offers that a person received in their application cycle, but also background information such as education and work experience. Use offer profiles to help you better contextualize offers."
description="Filter relevant offers by job title, company, submission date, salary and more."
icon={
<InformationCircleIcon
<TableCellsIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Offer table page"
imageSrc={offersProfile}
title="Choosing an offer needs context"
imageSrc={offersBrowse}
title="Stay informed of recent offers"
/>
</div>
<div className="mt-36">
@ -149,16 +154,16 @@ export default function LandingPage() {
</div>
<div className="mt-36">
<LeftTextCard
description="Filter relevant offers by job title, company, submission date, salary and more."
description="An offer profile includes not only offers that a person received in their application cycle, but also background information such as education and work experience. Use offer profiles to help you better contextualize offers."
icon={
<TableCellsIcon
<InformationCircleIcon
aria-hidden="true"
className="h-6 w-6 text-white"
/>
}
imageAlt="Offer table page"
imageSrc={offersBrowse}
title="Stay informed of recent offers"
imageSrc={offersProfile}
title="Choosing an offer needs context"
/>
</div>
</div>

@ -2,6 +2,7 @@ import Link from 'next/link';
import { useState } from 'react';
import { Banner } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import OffersTable from '~/components/offers/table/OffersTable';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
@ -9,6 +10,7 @@ import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
export default function OffersHomePage() {
const [jobTitleFilter, setjobTitleFilter] = useState('software-engineer');
const [companyFilter, setCompanyFilter] = useState('');
const { event: gaEvent } = useGoogleAnalytics();
return (
<main className="flex-1 overflow-y-auto">
@ -40,6 +42,11 @@ export default function OffersHomePage() {
onSelect={(option) => {
if (option) {
setjobTitleFilter(option.value);
gaEvent({
action: `offers.table_filter_job_title_${option.value}`,
category: 'engagement',
label: 'Filter by job title',
});
}
}}
/>
@ -50,6 +57,11 @@ export default function OffersHomePage() {
onSelect={(option) => {
if (option) {
setCompanyFilter(option.value);
gaEvent({
action: 'offers.table_filter_company',
category: 'engagement',
label: 'Filter by company',
});
}
}}
/>

@ -70,7 +70,12 @@ export default function OffersSubmissionResult() {
return (
<>
{getAnalysis.isLoading && (
<Spinner className="m-10" display="block" size="lg" />
<div className="flex h-screen w-screen">
<div className="m-auto mx-auto w-screen justify-center">
<Spinner display="block" size="lg" />
<div className="text-center">Loading...</div>
</div>
</div>
)}
{!getAnalysis.isLoading && (
<div ref={pageRef} className="fixed h-full w-full overflow-y-scroll">
@ -98,6 +103,7 @@ export default function OffersSubmissionResult() {
{step === 1 && (
<div className="flex items-center justify-between">
<Button
addonPosition="start"
icon={ArrowLeftIcon}
label="Previous"
variant="secondary"

Loading…
Cancel
Save