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

pull/465/head
Zhang Ziqing 3 years ago
parent a2bf45ad56
commit f0b34f0732

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

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

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

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

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

@ -38,32 +38,32 @@ const features = [
const footerNavigation = { const footerNavigation = {
social: [ social: [
{ // {
href: '#', // href: '#',
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => ( // icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}> // <svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path // <path
clipRule="evenodd" // 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" // 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" // fillRule="evenodd"
/> // />
</svg> // </svg>
), // ),
name: 'Facebook', // name: 'Facebook',
}, // },
{ // {
href: '#', // href: '#',
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => ( // icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}> // <svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path // <path
clipRule="evenodd" // 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" // 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" // fillRule="evenodd"
/> // />
</svg> // </svg>
), // ),
name: 'Instagram', // name: 'Instagram',
}, // },
{ {
href: 'https://github.com/yangshun/tech-interview-handbook', href: 'https://github.com/yangshun/tech-interview-handbook',
icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => ( icon: (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (

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

@ -70,7 +70,12 @@ export default function OffersSubmissionResult() {
return ( return (
<> <>
{getAnalysis.isLoading && ( {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 && ( {!getAnalysis.isLoading && (
<div ref={pageRef} className="fixed h-full w-full overflow-y-scroll"> <div ref={pageRef} className="fixed h-full w-full overflow-y-scroll">
@ -98,6 +103,7 @@ export default function OffersSubmissionResult() {
{step === 1 && ( {step === 1 && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button <Button
addonPosition="start"
icon={ArrowLeftIcon} icon={ArrowLeftIcon}
label="Previous" label="Previous"
variant="secondary" variant="secondary"

Loading…
Cancel
Save