Skip to content

Commit

Permalink
Make Transcripts page Responsive. (#93)
Browse files Browse the repository at this point in the history
Hugely clean up some of the component names, provider structures, pass the MUI sx property through, fix up theme palette to be more standard, and handle some "empty entry" problems in the database causing errors.

Lastly, migrate to Firebase App Hosting instead of Webframeworks. Sadly this removes the ability to do PR instances for now. We mitigate by adding a staging branch and kickin' it oldskool.
  • Loading branch information
awong-dev committed Jan 17, 2025
1 parent 95ba633 commit 63686ce
Show file tree
Hide file tree
Showing 33 changed files with 1,151 additions and 645 deletions.
22 changes: 11 additions & 11 deletions .github/workflows/firebase-hosting-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ jobs:
# Only ci, no build. The action-hosting-deploy with the webframeworks does the build.
- run: npm ci

# Firebase webframeworks doesn't do npm install on the functions directory.
- run: npm --prefix functions ci

- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SPS_BY_THE_NUMBERS }}
channelId: live
projectId: sps-by-the-numbers
env:
FIREBASE_CLI_EXPERIMENTS: webframeworks
# # Firebase webframeworks doesn't do npm install on the functions directory.
# - run: npm --prefix functions ci
#
# - uses: FirebaseExtended/action-hosting-deploy@v0
# with:
# repoToken: ${{ secrets.GITHUB_TOKEN }}
# firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SPS_BY_THE_NUMBERS }}
# channelId: live
# projectId: sps-by-the-numbers
# env:
# FIREBASE_CLI_EXPERIMENTS: webframeworks
14 changes: 7 additions & 7 deletions .github/workflows/firebase-hosting-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ jobs:
- run: npm --prefix functions run build
- run: npm --prefix functions test

- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SPS_BY_THE_NUMBERS }}
projectId: sps-by-the-numbers
env:
FIREBASE_CLI_EXPERIMENTS: webframeworks
# - uses: FirebaseExtended/action-hosting-deploy@v0
# with:
# repoToken: ${{ secrets.GITHUB_TOKEN }}
# firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SPS_BY_THE_NUMBERS }}
# projectId: sps-by-the-numbers
# env:
# FIREBASE_CLI_EXPERIMENTS: webframeworks
39 changes: 26 additions & 13 deletions app/[category]/v/[videoId]/[lang]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as Constants from 'config/constants'
import SpeakerInfoProvider from 'components/SpeakerInfoProvider'
import ActionDialog from 'components/ActionDialog';
import ActionDialogProvider from 'components/ActionDialogProvider';
import AnnotationsProvider from 'components/AnnotationsProvider';
import AuthProvider from 'components/AuthProvider';
import Transcript from 'components/Transcript'
import VideoControlContextProvider from 'components/VideoControlProvider'
import { DiarizedTranscript } from "common/transcript"
Expand Down Expand Up @@ -91,17 +94,27 @@ export default async function Index(props: {params: Promise<VideoParams>}) {
]);

return (
<SpeakerInfoProvider initialSpeakerInfo={ speakerControlInfo.speakerInfo }>
<VideoControlContextProvider>
<Transcript
metadata={ metadata }
category={ params.category }
diarizedTranscript={ diarizedTranscript }
languageOrder={ languageOrder }
speakerInfo={ speakerControlInfo.speakerInfo }
initialExistingNames={ speakerControlInfo.existingNames }
initialExistingTags={ speakerControlInfo.existingTags } />
</VideoControlContextProvider>
</SpeakerInfoProvider>
<AuthProvider>
<AnnotationsProvider
category={params.category}
videoId={params.videoId}
initialSpeakerInfo={speakerControlInfo.speakerInfo}
initialExistingNames={speakerControlInfo.existingNames}
initialExistingTags={speakerControlInfo.existingTags}
>
<VideoControlContextProvider>
<ActionDialogProvider>
<ActionDialog />
<Transcript
metadata={ metadata }
category={ params.category }
diarizedTranscript={ diarizedTranscript }
languageOrder={ languageOrder }
speakerInfo={ speakerControlInfo.speakerInfo }
/>
</ActionDialogProvider>
</VideoControlContextProvider>
</AnnotationsProvider>
</AuthProvider>
);
}
31 changes: 16 additions & 15 deletions app/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,32 @@ const theme = extendTheme({
fontFamily: roboto.style.fontFamily,
},
colorSchemes: {
light: {
dark: {
palette: {
background: {
default: "#cdcdcd"
},
primary: {
main: '#0a43ad',
analogous: '#0a95ad',
info: '#efefef',
contrastText: '#ececec',
main: '#0a43ad'
},
secondary: {
main: '#0a43ad',
main: '#8e6d0e',
},
},
}
},
components: {
MuiPaper: {
styleOverrides: {
root: {
backgroundColor: '#efefef',
}
},
},
light: {
palette: {
background: {
default: "#333333"
},
primary: {
main: '#0a43ad'
},
secondary: {
main: '#8e6d0e',
},
},
}
}
});

Expand Down
5 changes: 5 additions & 0 deletions common/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type ApiResponse = {
ok: boolean;
message: string;
data: any;
};
35 changes: 35 additions & 0 deletions components/ActionDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client'

import ActionDialogConstants from 'components/ActionDialogConstants';
import Dialog from '@mui/material/Dialog';
import SpeakerEditDialogContent from 'components/SpeakerEditDialogContent';
import UploadChangesDialogContent from 'components/UploadChangesDialogContent';
import { useActionDialog } from 'components/ActionDialogProvider';

function makeContents(actionDialogMode, handleClose) {
if (actionDialogMode?.mode === ActionDialogConstants.speakerMode) {
return (
<SpeakerEditDialogContent
speakerNum={actionDialogMode.params.speakerNum}
onClose={handleClose}/>
);
} else if (actionDialogMode?.mode === ActionDialogConstants.uploadChangesMode) {
return (<UploadChangesDialogContent onClose={handleClose}/>);
}

return (<></>);
}

export default function ActionDialog() {
const {actionDialogMode, setActionDialogMode} = useActionDialog();

const handleClose = (value: string) => {
setActionDialogMode(undefined); // Dismisses Dialog.
};

return (
<Dialog onClose={handleClose} open={actionDialogMode !== undefined}>
{makeContents(actionDialogMode, handleClose)}
</Dialog>
);
}
6 changes: 6 additions & 0 deletions components/ActionDialogConstants.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const actionDialogModes = {
uploadChangesMode: "upload_changes",
speakerMode: "speakerMode",
};

export default actionDialogModes;
34 changes: 34 additions & 0 deletions components/ActionDialogContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import CloseIcon from '@mui/icons-material/Close';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';

type ActionDialogContentsParams = {
title: string;
children: React.ReactNode;
onClose: (value: string) => void;
};

export default function ActionDialogContents({title, children, onClose}) {
return (
<>
<DialogTitle>
<Stack
direction="row"
sx={{
justifyContent:"space-between",
alignItems:"center"}}>
{title}
<IconButton onClick={onClose}>
<CloseIcon fontSize="inherit"/>
</IconButton>
</Stack>
</DialogTitle>
<DialogContent>
{children}
</DialogContent>
</>
);
}

45 changes: 45 additions & 0 deletions components/ActionDialogProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client'

import { createContext, useContext, useState, useMemo } from 'react';

type ActionDialogMode = {
mode: string;
params?: string | number;
};

type ActionDialogModeType = {
actionDialogMode : ActionDialogMode | undefined;
setActionDialogMode: (x: ActionDialogMode | undefined) => void;
};

type DialogProviderParams = {
children: React.ReactNode;
};

// Pattern from https://stackoverflow.com/a/74174425
const ActionDialogModeContext = createContext<ActionDialogModeType | undefined>(undefined);

export function useActionDialog() {
const context = useContext(ActionDialogModeContext);
if (context === undefined) {
throw new Error('Missing <ActionDialogModeProvider>')
}

return context;
}

export default function ActionDialogModeProvider({children}: DialogProviderParams) {
const [actionDialogMode, setActionDialogMode] = useState<ActionDialogMode | undefined>(undefined);

const value = useMemo(() => ({ actionDialogMode, setActionDialogMode }), [actionDialogMode]);

return (
<ActionDialogModeContext.Provider value={value}>
{useMemo(() => (
<>
{children}
</>
), [children])}
</ActionDialogModeContext.Provider>
)
}
125 changes: 125 additions & 0 deletions components/AnnotationsProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'use client'

import { createContext, useContext, useState, useMemo } from 'react'
import { isEqual, cloneDeep } from 'lodash-es'

import type { CategoryId, VideoId } from 'common/params';
import type { ExistingNames, TagSet, SpeakerInfoData } from 'utilities/client/speaker'

type LastPublishedState = {
speakerInfo: SpeakerInfoData;
};

type AnnotationsContextParams = {
children: React.ReactNode;
category: CategoryId;
videoId: VideoId;
initialSpeakerInfo: SpeakerInfoData;
initialExistingNames: ExistingNames;
initialExistingTags: TagSet;
};

class AnnotationsContextState {
readonly category: CategoryId;
readonly videoId: VideoId;

readonly speakerInfo: SpeakerInfoData;
readonly setSpeakerInfo:(x: SpeakerInfoData) => void;

readonly existingNames: ExistingNames;
readonly setExistingNames: (x: ExistingNames) => void;

readonly existingTags: TagSet;
readonly setExistingTags: (x: TagSet) => void;

readonly lastPublishedState: LastPublishedState;
readonly setLastPublishedState: (x: LastPublishedState) => void;

constructor(
category: CategoryId,
videoId: VideoId,

speakerInfo: SpeakerInfoData,
setSpeakerInfo:(x: SpeakerInfoData) => void,

existingNames: ExistingNames,
setExistingNames: (x: ExistingNames) => void,

existingTags: TagSet,
setExistingTags: (x: TagSet) => void,

lastPublishedState: LastPublishedState,
setLastPublishedState: (x: LastPublishedState) => void) {
this.category = category;
this.videoId = videoId;

this.speakerInfo = speakerInfo;
this.setSpeakerInfo = setSpeakerInfo;

this.existingNames = existingNames;
this.setExistingNames = setExistingNames;

this.existingTags = existingTags;
this.setExistingTags = setExistingTags;

this.lastPublishedState = lastPublishedState;
this.setLastPublishedState = setLastPublishedState;
}

needsPublish() : boolean {
return !isEqual(this.lastPublishedState?.speakerInfo, this?.speakerInfo);
}
}

// Pattern from https://stackoverflow.com/a/74174425
const AnnotationsContext =
createContext<AnnotationsContextState | undefined>(undefined);

// Pattern from https://kentcdodds.com/blog/how-to-use-react-context-effectively
export function useAnnotations() {
const context = useContext(AnnotationsContext);
if (context === undefined) {
throw new Error('Missing <AnnotationsProvider>')
}

return context;
}

export default function AnnotationsProvider({
children, category, videoId, initialSpeakerInfo, initialExistingNames, initialExistingTags}: AnnotationsContextParams) {

const [speakerInfo, setSpeakerInfo] = useState<SpeakerInfoData>(initialSpeakerInfo)
const [existingNames, setExistingNames] = useState<ExistingNames>(initialExistingNames);
const [existingTags, setExistingTags] = useState<TagSet>(initialExistingTags);

const [lastPublishedState, setLastPublishedState] = useState<LastPublishedState>(
{
speakerInfo: cloneDeep(initialSpeakerInfo),
}
);

const value = useMemo(() => (new AnnotationsContextState(
category,
videoId,
speakerInfo,
setSpeakerInfo,
existingNames,
setExistingNames,
existingTags,
setExistingTags,
lastPublishedState,
setLastPublishedState,
)),
[speakerInfo, existingNames, existingTags, lastPublishedState, category, videoId]
);

return (
<AnnotationsContext.Provider value={value}>
{useMemo(() => (
<>
{children}
</>
), [children])}
</AnnotationsContext.Provider>
)
}
Loading

0 comments on commit 63686ce

Please sign in to comment.