Skip to content

Commit

Permalink
Merge pull request #35 from complexdatacollective/feature/interviewer…
Browse files Browse the repository at this point in the history
…-port-1

Initial Interviewer port
  • Loading branch information
jthrilly committed Dec 12, 2023
2 parents 7b61cf4 + 03ba7cf commit 90b999f
Show file tree
Hide file tree
Showing 839 changed files with 49,257 additions and 445 deletions.
11 changes: 8 additions & 3 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const config = {
overrides: [
{
extends: [
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/stylistic-type-checked',
'plugin:@typescript-eslint/recommended-type-checked',
],
files: ['*.ts', '*.tsx'],
parserOptions: {
Expand All @@ -20,13 +21,17 @@ const config = {
},
plugins: ['@typescript-eslint', 'eslint-plugin-local-rules'],
extends: [
'next/core-web-vitals',
'eslint:recommended',
'plugin:@typescript-eslint/stylistic',
'plugin:@typescript-eslint/recommended',
'next/core-web-vitals',
'plugin:storybook/recommended',
'prettier',
],
ignorePatterns: ['node_modules', 'lib', '*.stories.*', '*.test.*'],
ignorePatterns: ['node_modules', '*.stories.*', '*.test.*'],
rules: {
"import/no-anonymous-default-export": "off",
"@typescript-eslint/consistent-type-definitions": ['error', 'type'],
'no-process-env': 'error',
'no-console': 'error',
'@typescript-eslint/consistent-type-imports': [
Expand Down
9 changes: 6 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"css.customData": ["./.vscode/css-data.json"],
"css.customData": [
"./.vscode/css-data.json"
],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"WillLuke.nextjs.addTypesOnSave": true,
"WillLuke.nextjs.hasPrompted": true
}
"WillLuke.nextjs.hasPrompted": true,
"jest.jestCommandLine": "pnpm run test"
}
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# fresco-setup-test
# Fresco

## Deploy your own instance of Fresco
The Fresco project aims to bring Network Canvas interviews to the web browser. It is a pilot project that does not
add new features to Network Canvas, but rather provides a new way to conduct interviews.

## Known Limitations

- Custom node label workers are not implemented.
- Videos and audio cannot autoplay on load due to browser limitations. Participants must click the play button to start media.

# Deployment instructions

**1. Set up required servies**
- Create a database with [PlanetScale](https://planetscale.com/docs/tutorials/planetscale-quick-start-guide)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,10 @@ export const ParticipantColumns =
},
cell: ({ row }) => (
<Link
target="_blank"
className="text-blue-500 underline hover:text-blue-300"
href={`/interview/${row.original.id}`}
href={`/interview/new?identifier=${row.original.id}`}
>
interview/{row.original.id}
Participant link
</Link>
),
enableSorting: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ import {
import useZodForm from '~/hooks/useZodForm';
import ActionError from '~/components/ActionError';
import { api } from '~/trpc/client';
import { participantIdentifierSchema } from '~/shared/schemas';
import { participantIdentifierSchema } from '~/shared/schemas/schemas';
import type { Participant } from '@prisma/client';
import { clientRevalidateTag } from '~/utils/clientRevalidate';

interface ParticipantModalProps {
type ParticipantModalProps = {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
editingParticipant?: string | null;
setEditingParticipant?: Dispatch<SetStateAction<string | null>>;
existingParticipants: Participant[];
}
};

function ParticipantModal({
open,
Expand Down Expand Up @@ -59,6 +60,7 @@ function ParticipantModal({
setIsLoading(true);
},
async onSuccess() {
void clientRevalidateTag('participant.get.all');
await utils.participant.get.invalidate();
},
onError(error) {
Expand All @@ -79,6 +81,7 @@ function ParticipantModal({
setError(error.message);
},
onSettled() {
void clientRevalidateTag('participant.get.all');
setIsLoading(false);
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import type { ProtocolWithInterviews } from '~/shared/types';
import { useEffect, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import { api } from '~/trpc/client';
import { clientRevalidateTag } from '~/utils/clientRevalidate';
import { useRouter } from 'next/navigation';

interface DeleteProtocolsDialogProps {
open: boolean;
Expand All @@ -26,6 +28,8 @@ export const DeleteProtocolsDialog = ({
setOpen,
protocolsToDelete,
}: DeleteProtocolsDialogProps) => {
const router = useRouter();

const [protocolsInfo, setProtocolsInfo] = useState<{
hasInterviews: boolean;
hasUnexportedInterviews: boolean;
Expand All @@ -50,10 +54,10 @@ export const DeleteProtocolsDialog = ({
},
});

const utils = api.useUtils();
const handleConfirm = async () => {
await deleteProtocols(protocolsToDelete.map((d) => d.hash));
await utils.protocol.get.all.refetch();
await clientRevalidateTag('protocols.get.all');
router.refresh();
setOpen(false);
};

Expand Down
4 changes: 4 additions & 0 deletions app/(interview)/interview/[interviewId]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default function Loading() {
// Or a custom loading skeleton component
return <p>Loading interview...</p>;
}
47 changes: 10 additions & 37 deletions app/(interview)/interview/[interviewId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,19 @@
import { InterviewProvider } from '~/providers/InterviewProvider';
import Stage from '~/app/(interview)/interview/_components/Stage';
import InterviewNavigation from '~/app/(interview)/interview/_components/InterviewNavigation';
import type { NcNetwork, Protocol } from '@codaco/shared-consts';
import Link from 'next/link';
import { api } from '~/trpc/server';
import { Button } from '~/components/ui/Button';
import InterviewShell from '../_components/InterviewShell';
import NoSSRWrapper from '~/utils/NoSSRWrapper';

export default function Page({ params }: { params: { interviewId: string } }) {
const { interviewId } = params;

export default async function Page({
params: { interviewId },
}: {
params: { interviewId: string };
}) {
// Fetch interview data from the database
if (!interviewId) {
return 'No interview id found';
}

const interview = await api.interview.get.byId.query({ id: interviewId });

if (!interview) {
return 'No interview found';
}

const initialNetwork = interview.network as NcNetwork;
const interviewProtocol = interview.protocol as unknown as Protocol;

return (
<InterviewProvider
interviewId={interviewId}
initialNetwork={initialNetwork}
protocol={interviewProtocol}
>
<div className="flex grow flex-col justify-between p-10">
<h1 className="text-3xl">Interview</h1>
<Link href="/">
<Button>Exit Interview</Button>
</Link>
<Stage />
<aside className="flex items-center justify-center">
<InterviewNavigation />
</aside>
</div>
</InterviewProvider>
<div className="flex h-[100vh] max-h-[100vh] flex-col bg-[var(--nc-background)] text-[var(--nc-text)]">
<NoSSRWrapper>
<InterviewShell interviewID={interviewId} />
</NoSSRWrapper>
</div>
);
}
117 changes: 117 additions & 0 deletions app/(interview)/interview/_components/InterviewShell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use client';

import { Provider, useSelector } from 'react-redux';
import DialogManager from '~/lib/interviewer/components/DialogManager';
import ProtocolScreen from '~/lib/interviewer/containers/ProtocolScreen';
import { store } from '~/lib/interviewer/store';
import UserBanner from './UserBanner';
import { useEffect, useState } from 'react';
import {
SET_SERVER_SESSION,
type SetServerSessionAction,
} from '~/lib/interviewer/ducks/modules/setServerSession';
import { getActiveSession } from '~/lib/interviewer/selectors/session';
import { api } from '~/trpc/client';
import { useQueryState } from 'next-usequerystate';
import usePrevious from '~/hooks/usePrevious';
import { isEqual } from 'lodash';

// The job of ServerSync is to listen to actions in the redux store, and to sync
// data with the server.
const ServerSync = ({ interviewId }: { interviewId: string }) => {
const [init, setInit] = useState(false);
// Current stage
const currentSession = useSelector(getActiveSession);
const prevCurrentSession = usePrevious(currentSession);
const { mutate: syncSessionWithServer } = api.interview.sync.useMutation();

useEffect(() => {
if (!init) {
setInit(true);
return;
}

if (
isEqual(currentSession, prevCurrentSession) ||
!currentSession ||
!prevCurrentSession
) {
return;
}

// check if current stage index is null (happens when hot reloading)
if (currentSession.currentStep === null) {
console.log('⚠️ Current stage index is null. Skipping sync.');
return;
}

console.log(`⬆️ Syncing session with server...`);
syncSessionWithServer({
id: interviewId,
network: currentSession.network,
currentStep: currentSession.currentStep,
});
}, [
currentSession,
prevCurrentSession,
interviewId,
syncSessionWithServer,
init,
]);

return null;
};

// The job of interview shell is to receive the server-side session and protocol
// and create a redux store with that data.
// Eventually it will handle syncing this data back.
const InterviewShell = ({ interviewID }: { interviewID: string }) => {
const [currentStage, setCurrentStage] = useQueryState('stage');

const { isLoading } = api.interview.get.byId.useQuery(
{ id: interviewID },
{
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
onSuccess: async (data) => {
if (!data) {
return;
}

const { protocol, ...serverSession } = data;

console.log(
'✅ Received server session. Setting current stage, and initializing redux store...',
);

if (!currentStage) {
await setCurrentStage(serverSession.currentStep.toString());
}

store.dispatch<SetServerSessionAction>({
type: SET_SERVER_SESSION,
payload: {
protocol,
session: serverSession,
},
});
},
},
);

if (isLoading) {
return 'Second loading stage...';
}

return (
<Provider store={store}>
<ServerSync interviewId={interviewID} />
<UserBanner />
<ProtocolScreen />
<DialogManager />
</Provider>
);
};

export default InterviewShell;
54 changes: 0 additions & 54 deletions app/(interview)/interview/_components/Stage.tsx

This file was deleted.

Loading

0 comments on commit 90b999f

Please sign in to comment.