Skip to content

Commit

Permalink
Merge pull request #26 from oliverbutler/seo
Browse files Browse the repository at this point in the history
🔍 Improve SEO and statically build sprint page
  • Loading branch information
oliverbutler authored Feb 17, 2022
2 parents 32b4486 + fc6308e commit 640e1b3
Show file tree
Hide file tree
Showing 22 changed files with 216 additions and 117 deletions.
13 changes: 13 additions & 0 deletions apps/xeo-scrum/components/Error/Error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Alert } from '@xeo/ui';

interface Props {
errorMessage: string | React.ReactNode;
}

export const Error: React.FunctionComponent<Props> = ({ errorMessage }) => {
return (
<div className="w-full h-screen flex justify-center items-center">
<Alert variation="danger">{errorMessage}</Alert>
</div>
);
};
3 changes: 2 additions & 1 deletion apps/xeo-scrum/components/RouteGuard/RouteGuard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
import { trackUserIdentification } from 'utils/analytics';
import { isSprintEmbedded } from 'pages/sprint/[sprintId]';

export const RouteGuard: React.FunctionComponent = ({ children }) => {
const router = useRouter();
Expand Down Expand Up @@ -43,7 +44,7 @@ export const RouteGuard: React.FunctionComponent = ({ children }) => {
return;
}

const isPublicPath = path === '/login' || path.endsWith('/embed');
const isPublicPath = path === '/login' || isSprintEmbedded(router);

if (session.status !== 'authenticated' && !isPublicPath) {
setAuthorized(false);
Expand Down
14 changes: 12 additions & 2 deletions apps/xeo-scrum/components/SprintInfo/SprintGraph/SprintGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import classNames from 'classnames';
import { useTheme } from 'next-themes';
import utc from 'dayjs/plugin/utc';
import { useViewport } from '../../../../../libs/ui/src/hooks/useViewport';

dayjs.extend(utc);

Expand All @@ -35,6 +36,8 @@ export const SprintGraph: React.FunctionComponent<Props> = ({
showPointsNotStarted,
smallGraph,
}) => {
const { width } = useViewport();

const CustomTooltip = ({
active,
payload,
Expand Down Expand Up @@ -90,7 +93,12 @@ export const SprintGraph: React.FunctionComponent<Props> = ({
return null;
};

const HEIGHT = smallGraph ? 200 : 400;
const sm = theme.extend.screens.sm;
const smWidth = Number(sm.substring(0, sm.length - 2));

const isSmallWindow = width <= smWidth;

const HEIGHT = smallGraph ? 200 : isSmallWindow ? 300 : 400;
const WIDTH = 1000;

const nextTheme = useTheme();
Expand Down Expand Up @@ -146,7 +154,9 @@ export const SprintGraph: React.FunctionComponent<Props> = ({

return (
<div
className={classNames('relative -ml-6 mt-4', { 'text-sm': smallGraph })}
className={classNames('relative -ml-6 mt-4', {
'text-sm': smallGraph || isSmallWindow,
})}
style={{ height: HEIGHT }}
>
<ResponsiveContainer
Expand Down
59 changes: 27 additions & 32 deletions apps/xeo-scrum/components/SprintInfo/SprintInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import {
ExternalLinkIcon,
RefreshIcon,
} from '@heroicons/react/outline';
import { Button, ButtonVariation, CentredLoader, Clickable } from '@xeo/ui';
import { fetcher } from 'components/Connections/Notion/NotionBacklog/NotionBacklog';
import { Button, ButtonVariation, Clickable } from '@xeo/ui';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { GetSprintHistoryRequest } from 'pages/api/sprint/[sprintId]/history';
import { useCallback, useEffect, useState } from 'react';
import useSWR, { useSWRConfig } from 'swr';
import { useSWRConfig } from 'swr';
import { SprintGraph } from './SprintGraph/SprintGraph';
import { SprintStats } from './SprintStats/SprintStats';
import axios from 'axios';
Expand All @@ -19,11 +18,12 @@ import { ScopedMutator } from 'swr/dist/types';
import { toast } from 'react-toastify';
import { DarkModeButton } from 'components/DarkModeButton/DarkModeButton';
import { UserAction, trackSprintAction } from 'utils/analytics';
import { NextSeo } from 'next-seo';

dayjs.extend(relativeTime);

interface Props {
sprintId: string;
sprintData: GetSprintHistoryRequest['response'];
publicMode: boolean;
}

Expand All @@ -48,59 +48,54 @@ const updateSprintHistory = async (
};

export const SprintInfo: React.FunctionComponent<Props> = ({
sprintId,
sprintData,
publicMode,
}) => {
useEffect(() => {
trackSprintAction({ action: UserAction.SPRINT_VIEW, sprintId: sprintId });
trackSprintAction({
action: UserAction.SPRINT_VIEW,
sprintId: sprintData.sprint.id,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const [isLoading, setIsLoading] = useState(false);

const { data, error } = useSWR<GetSprintHistoryRequest['responseBody']>(
`/api/sprint/${sprintId}/history`,
fetcher
);
const { mutate } = useSWRConfig();

const handleUpdateSprintHistory = useCallback(async () => {
if (data?.sprint) {
if (!dayjs(data.sprint.endDate).isBefore(dayjs(), 'minute')) {
if (sprintData?.sprint) {
if (!dayjs(sprintData.sprint.endDate).isBefore(dayjs(), 'minute')) {
setIsLoading(true);
await updateSprintHistory(sprintId, mutate);
await updateSprintHistory(sprintData.sprint.id, mutate);
setIsLoading(false);
} else {
toast.warn("You can't update a sprint that's in the past!");
}
}
}, [data, mutate, sprintId]);
}, [sprintData, mutate]);

useEffect(() => {
if (data && !dayjs(data.sprint.endDate).isBefore(dayjs(), 'minute')) {
if (
sprintData &&
!dayjs(sprintData.sprint.endDate).isBefore(dayjs(), 'minute')
) {
handleUpdateSprintHistory();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const [showPointsNotStarted, setShowPointsNotStarted] = useState(true);

if (!data || error) {
return (
<div>
<CentredLoader />
</div>
);
}

if (error || !data.sprint) {
return <div>Error fetching Sprint</div>;
}

const { sprint, sprintHistoryPlotData } = data;
const { sprint, sprintHistoryPlotData } = sprintData;

return (
<div className="w-full p-2 pt-10 sm:p-10">
<div className="w-full p-4 sm:p-10">
<NextSeo
title={`Sprint - ${sprint.name}`}
description={`View ${sprint.name}`}
/>

<div className="flex flex-row justify-between">
<div>
<h1 className="mb-0">{sprint.name}</h1>
Expand Down Expand Up @@ -129,18 +124,18 @@ export const SprintInfo: React.FunctionComponent<Props> = ({
)}
</div>
</div>
<p>{sprint.sprintGoal}</p>
<p className="hidden sm:block">{sprint.sprintGoal}</p>

<SprintStats
sprintHistoryPlotData={sprintHistoryPlotData}
sprintId={sprintId}
sprintId={sprintData.sprint.id}
/>
<div className="flex flex-row items-end justify-between">
<div className="flex w-full flex-row justify-end gap-2">
{!publicMode && (
<Clickable
onClick={() => {
navigator.clipboard.writeText(`${window.location}/embed`);
navigator.clipboard.writeText(`${window.location}?embed=1`);
toast.info('Embeddable link copied to Clipboard');
}}
>
Expand Down
16 changes: 7 additions & 9 deletions apps/xeo-scrum/components/SprintInfo/SprintStats/SprintStat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ export const SprintStat: React.FunctionComponent<Props> = ({
value,
}) => {
return (
<div className="w-1/2 p-2 md:w-1/2 lg:w-1/3 xl:w-1/4">
<div className="bg-dark-50 dark:bg-dark-800 dark:border-l-dark-600 flex flex-row border-l-4">
<div id="icon-container" className="mx-2 flex items-center">
{icon}
</div>
<div id="text-container" className="-mb-2 flex flex-col justify-center">
<h3 className="m-0 mt-2">{title}</h3>
{value}
</div>
<div className="bg-dark-50 dark:bg-dark-800 dark:border-l-dark-600 flex flex-row border-l-4 text-sm sm:text-base hover:scale-105 transition-all">
<div id="icon-container" className="mx-2 flex items-center">
{icon}
</div>
<div id="text-container" className="-mb-2 flex flex-col justify-center">
<h3 className="m-0 mt-2">{title}</h3>
{value}
</div>
</div>
);
Expand Down
14 changes: 7 additions & 7 deletions apps/xeo-scrum/components/SprintInfo/SprintStats/SprintStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const SprintStats: React.FunctionComponent<Props> = ({
stats;

return (
<div className="flex flex-row flex-wrap">
<div className="grid grid-cols-3 2xl:grid-cols-6 gap-2 sm:gap-4 my-2">
<SprintStat
icon={
<ChartBarIcon height={40} width={40} className="stroke-primary-300" />
Expand All @@ -44,14 +44,14 @@ export const SprintStats: React.FunctionComponent<Props> = ({
icon={
deltaPoints < 0 ? (
<ExclamationCircleIcon
height={40}
width={40}
height={35}
width={35}
className="stroke-red-400 dark:stroke-red-300"
/>
) : (
<CheckCircleIcon
height={40}
width={40}
height={35}
width={35}
className="stroke-primary-300"
/>
)
Expand All @@ -72,8 +72,8 @@ export const SprintStats: React.FunctionComponent<Props> = ({
<SprintStat
icon={
<ClipboardCheckIcon
height={40}
width={40}
height={35}
width={35}
className="stroke-primary-300"
/>
}
Expand Down
7 changes: 7 additions & 0 deletions apps/xeo-scrum/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ const nextConfig = {
images: {
domains: ['avatars.githubusercontent.com', 'lh3.googleusercontent.com'],
},
redirects: () => [
{
source: '/sprint/:id/embed',
destination: '/sprint/:id?embed=1',
permanent: true,
},
],
};

module.exports = withSentryConfig(
Expand Down
5 changes: 3 additions & 2 deletions apps/xeo-scrum/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useRouter } from 'next/router';
import { RouteGuard } from 'components/RouteGuard/RouteGuard';
import { useEffect } from 'react';
import { initGA } from 'utils/analytics';
import { isSprintEmbedded } from './sprint/[sprintId]';

declare global {
interface Window {
Expand All @@ -22,10 +23,10 @@ function CustomApp({
Component,
pageProps: { session, ...pageProps },
}: AppProps) {
const { pathname } = useRouter();
const router = useRouter();

const hideSidebar =
pathname.startsWith('/login') || pathname.endsWith('/embed');
isSprintEmbedded(router) || router.pathname.startsWith('/login');

useEffect(() => {
if (!window.GA_INITIALIZED) {
Expand Down
54 changes: 11 additions & 43 deletions apps/xeo-scrum/pages/api/sprint/[sprintId]/history.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,28 @@
import { Sprint } from '@prisma/client';
import { NextApiRequest, NextApiResponse } from 'next';
import { DataPlotType, getDataForSprintChart } from 'utils/sprint/chart';
import { prisma } from 'utils/db';
import { DataPlotType } from 'utils/sprint/chart';
import { withSentry } from '@sentry/nextjs';
import { apiError, APIGetRequest, apiResponse } from 'utils/api';
import { getSprintAndPlotDataForPage } from 'utils/sprint/sprint-history';

export type GetSprintHistoryRequest = {
method: 'GET';
responseBody: {
sprint: Sprint;
sprintHistoryPlotData: DataPlotType[];
};
};
export type GetSprintHistoryRequest = APIGetRequest<{
sprint: Sprint;
sprintHistoryPlotData: DataPlotType[];
}>;

/**
* ⚠️ Public Facing Route
*/
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const sprintId = req.query.sprintId as string;

const sprint = await prisma.sprint.findFirst({
where: {
id: sprintId,
},
include: {
backlog: {
include: {
notionStatusLinks: true,
},
},
sprintHistory: {
include: {
sprintStatusHistory: true,
},
},
},
});
const sprintAndPlotData = await getSprintAndPlotDataForPage(sprintId);

if (!sprint) {
return res.status(404).json({ message: 'Sprint not found' });
if (!sprintAndPlotData) {
return apiError(res, { message: 'Sprint not found' }, 404);
}

const sprintHistoryPlotData = getDataForSprintChart(
sprint,
sprint.sprintHistory,
sprint.backlog.notionStatusLinks
);

// Remove backlog and sprintHistory from the response to avoid sending unnecessary data
const { backlog, sprintHistory, ...restSprint } = sprint;

const returnValue: GetSprintHistoryRequest['responseBody'] = {
sprint: restSprint,
sprintHistoryPlotData,
};

return res.status(200).json(returnValue);
return apiResponse<GetSprintHistoryRequest>(res, sprintAndPlotData);
};

export default withSentry(handler);
Loading

1 comment on commit 640e1b3

@vercel
Copy link

@vercel vercel bot commented on 640e1b3 Feb 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.