Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ VITE_APP_ENV=
VITE_GA_MEASUREMENT_ID=
VITE_UMAMI_SRC=
VITE_UMAMI_WEBSITE_ID=

# Vite dev server proxy for NotifyComp token requests.
NOTIFY_COMP_TOKEN_ORIGIN=https://www.competitiongroups.com
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ yarn test
yarn storybook
yarn build-storybook
```

During `yarn dev`, NotifyComp token requests are proxied to
`https://www.competitiongroups.com`, so production WCA sign-in and NotifyComp
authorization work without running Netlify Functions locally. The deployed
function checks the WCA access token and competition permissions. Set
`NOTIFY_COMP_TOKEN_ORIGIN` in `.env.local` to use another deployed token issuer
or a local Netlify Dev server. Use a token issuer connected to the same WCA and
NotifyComp environments as the frontend, and restart Vite after changing it.
35 changes: 33 additions & 2 deletions src/components/NotifyCompRemoteBar/NotifyCompRemoteBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { RemoteActivitySummaryList } from '@/components/RemoteActivitySummaryLis
import { useCompetitionRemoteControl } from '@/hooks/useCompetitionRemoteControl';
import { useNotifyCompWebSocketStatus } from '@/hooks/useNotifyCompWebSocketStatus';
import { useNow } from '@/hooks/useNow';
import { parseActivityCodeFlexible } from '@/lib/activityCodes';
import { isOfficialEventId, shortEventNameById } from '@/lib/events';
import { RemoteActivityGroup } from '@/lib/notifyCompRemoteActivities';
import { canUseNotifyCompRemoteControls } from '@/lib/notifyCompWebSocketStatus';
import { useConfirm } from '@/providers/ConfirmProvider';
Expand All @@ -23,6 +25,25 @@ const iconButtonClassName =
const primaryButtonClassName =
'flex h-10 w-10 items-center justify-center rounded-full border border-blue-300 bg-blue-200 text-lg leading-none text-gray-900 shadow-sm hover-transition hover:bg-blue-300 disabled:cursor-not-allowed disabled:opacity-40 dark:border-blue-600 dark:bg-blue-700 dark:text-white dark:hover:bg-blue-600';

const shortGroupName = (group: RemoteActivityGroup) => {
const activityCode = group.scheduledActivities[0]?.activityCode;
if (!activityCode || !isOfficialEventId(activityCode.split('-')[0])) {
return group.name;
}

const { eventId, roundNumber, groupNumber, attemptNumber } =
parseActivityCodeFlexible(activityCode);

return [
shortEventNameById(eventId),
roundNumber && `R${roundNumber}`,
groupNumber && `G${groupNumber}`,
attemptNumber && `A${attemptNumber}`,
]
.filter(Boolean)
.join(' ');
};

export function NotifyCompRemoteBar({ competitionId }: NotifyCompRemoteBarProps) {
const confirm = useConfirm();
const remote = useCompetitionRemoteControl({ competitionId });
Expand All @@ -41,6 +62,10 @@ export function NotifyCompRemoteBar({ competitionId }: NotifyCompRemoteBarProps)
const hasActiveGroups = activeNames.length > 0;
const currentTitle = activeNames.length > 0 ? activeNames.join(', ') : 'No active activity';
const nextTitle = remote.nextGroup?.name || 'No next activity';
const currentShortTitle = hasActiveGroups
? remote.activeGroups.map(shortGroupName).join(', ')
: currentTitle;
const nextShortTitle = remote.nextGroup ? shortGroupName(remote.nextGroup) : nextTitle;
const elapsed = formatElapsedDuration(remote.activeGroups, now);
const nextOffset = formatNextActivityOffset(remote.nextGroup, now);
const progress = getRemoteBarProgress({
Expand Down Expand Up @@ -148,7 +173,10 @@ export function NotifyCompRemoteBar({ competitionId }: NotifyCompRemoteBarProps)
)}>
<div className="min-w-0 space-y-0.5 text-left">
<div className="text-xs font-medium uppercase leading-none text-muted">Current</div>
<div className="truncate text-sm font-medium text-default">{currentTitle}</div>
<div className="text-sm font-medium text-default" title={currentTitle}>
<span className="block break-words sm:hidden">{currentShortTitle}</span>
<span className="hidden truncate sm:block">{currentTitle}</span>
</div>
{hasActiveGroups && <div className="text-sm tabular-nums text-muted">{elapsed}</div>}
</div>

Expand Down Expand Up @@ -203,7 +231,10 @@ export function NotifyCompRemoteBar({ competitionId }: NotifyCompRemoteBarProps)

<div className="min-w-0 space-y-0.5 text-right">
<div className="text-xs font-medium uppercase leading-none text-muted">Next</div>
<div className="truncate text-sm font-medium text-default">{nextTitle}</div>
<div className="text-sm font-medium text-default" title={nextTitle}>
<span className="block break-words sm:hidden">{nextShortTitle}</span>
<span className="hidden truncate sm:block">{nextTitle}</span>
</div>
<div className="truncate text-sm text-muted">{nextOffset}</div>
</div>
<NotifyCompConnectionStatus compact className="col-span-3 -mt-1" />
Expand Down
13 changes: 12 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ViteYaml from '@modyfi/vite-plugin-yaml';
import react from '@vitejs/plugin-react';
import { execSync } from 'child_process';
import { defineConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
import viteTsconfigPaths from 'vite-tsconfig-paths';

Expand Down Expand Up @@ -51,6 +51,17 @@ export default defineConfig(({ mode }) => ({
},
}),
],
server: {
proxy: {
// Use the deployed token issuer when running without Netlify Dev.
'^/\\.netlify/functions/notify-comp-token(?:\\?|$)': {
target:
loadEnv(mode, process.cwd(), 'NOTIFY_COMP_').NOTIFY_COMP_TOKEN_ORIGIN ||
'https://www.competitiongroups.com',
changeOrigin: true,
},
},
},
build: {
rollupOptions: {
output: {
Expand Down
Loading