diff --git a/package-lock.json b/package-lock.json index 3b21296..ed9a330 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "preact": "^10.15.1", "react": "^18.2.0", "react-dom": "18.2.0", + "react-icons": "^4.12.0", "react-p5": "^1.3.35", "react-router-dom": "^6.14.2", "shuffle-array": "^1.0.1", @@ -7857,6 +7858,14 @@ "react": "^18.2.0" } }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 469cfd4..408768e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "preact": "^10.15.1", "react": "^18.2.0", "react-dom": "18.2.0", + "react-icons": "^4.12.0", "react-p5": "^1.3.35", "react-router-dom": "^6.14.2", "shuffle-array": "^1.0.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f3eac9a..b15642e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -76,6 +76,9 @@ model User { wordFlasherSessions WordFlasherSession[] LetterMatcherSessions LetterMatcherSession[] GreenDotSessions GreenDotSession[] + NumberGuesserSession NumberGuesserSession[] + BoxFlasherSession BoxFlasherSession[] + PairsSession PairsSession[] highlightColor Overlay @default(GREY) lastSchulte String @default(" ") lastSpeedTest String @default(" ") @@ -89,8 +92,8 @@ model User { lastCubeByThree String @default(" ") lastNumberGuesser String @default(" ") lastLetterMatcher String @default(" ") + lastWordPairs String @default(" ") lastGreenDot String @default(" ") - lastWordPair String @default(" ") numberGuesserFigures Int @default(4) font Font @default(sans) isUsingChecklist Boolean @default(true) @@ -100,8 +103,6 @@ model User { schulteAdvanceCount Int @default(0) language Language @default(english) tested Boolean @default(false) - NumberGuesserSession NumberGuesserSession[] - BoxFlasherSession BoxFlasherSession[] @@index([id]) } @@ -208,6 +209,17 @@ model GreenDotSession { @@index([id]) } +model PairsSession { + id String @unique + userId String + user User @relation(fields: [userId], references: [id]) + date DateTime @db.DateTime + time Int + errorCount Int + + @@index([id]) +} + model SatPassage { id String @unique passageText String diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index 08970e2..7c9e731 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -2,6 +2,10 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { useClerk } from "@clerk/clerk-react" import { motion } from 'framer-motion' +import { + IoArrowForwardCircleOutline, + IoArrowBackCircleOutline +} from "react-icons/io5" export default function Sidebar() { const [showing, setShowing] = useState(false) @@ -59,8 +63,8 @@ export default function Sidebar() { > { showing - ? '⮈' - : '⮊' + ? + : }
diff --git a/src/components/wordpairs.tsx b/src/components/wordpairs.tsx new file mode 100644 index 0000000..4e36930 --- /dev/null +++ b/src/components/wordpairs.tsx @@ -0,0 +1,191 @@ +import { useRouter, type SingletonRouter } from 'next/router' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { uuid } from 'uuidv4' +import { FontProvider } from '~/cva/fontProvider' +import { useStopWatch } from '~/hooks/useStopWatch' +import useUserStore from '~/stores/userStore' +import { api } from '~/utils/api' +import { formatDate, navigateToNextExercise } from '~/utils/helpers' +import type { Font, WordPair } from '~/utils/types' + +function useGetProps(total = 18, diffCount = 5) { + + const pairs = api.getExcerciseProps.getWordPairs.useQuery({ + count: diffCount, + language: "english", + }) + const words = api.getExcerciseProps.getRandomWords.useQuery({ + number: total - diffCount, + language: "english", + max: 7 + }) + + const result = useMemo(() => { + if (words.isSuccess && pairs.isSuccess) { + return { + words: words.data, + pairs: pairs.data + } + } + else return { + words: undefined, + pairs: undefined + } + }, [pairs, words]) + + return result +} + +type PairsProps = { + diffCount: number +} + + +export default function WordPairs({ diffCount }: PairsProps) { + + const total = 18 + const { words, pairs } = useGetProps(total, diffCount) + const user = api.user.getUnique.useQuery() + const { mutate: updateUser } = api.user.setUser.useMutation() + const userStore = useUserStore() + const { mutate: collectSessionData } = api.wordPairSession.setUnique.useMutation() + const stopWatch = useStopWatch() + const router = useRouter() + const foud = useRef(0) + const wrongs = useRef(0) + const font = useRef('sans') + const [grid, setGrid] = useState() + + + function generateGrid() { + if (!pairs || !words) return + const cells = new Array() + words.forEach((word) => { + cells.push(generateSame(word)) + }) + pairs.forEach((pair) => { + cells.push(generateDifferent(pair)) + }) + stopWatch.start() + return cells.sort(() => Math.random() - 0.5) + } + + + const handleCellClick = useCallback((answer: 'correct' | 'error') => { + console.log(answer) + if (answer === 'correct') { + foud.current += 1 + } + if (answer === 'error') { + wrongs.current += 1 + } + + if (foud.current === diffCount) tearDown() + console.log('pairsFound', foud.current) + }, []) + + + function tearDown() { + console.log('teardown') + if (!user) return + if (!userStore.user) return + stopWatch.end() + updateUser({ lastWordPairs: formatDate(new Date()) }) + userStore.setUser({ + ...userStore.user, + lastWordPairs: formatDate(new Date()) + }) + if (user.data && user.data.isStudySubject) { + collectSessionData({ + userId: user.data.id, + errorCount: wrongs.current, + time: stopWatch.getDuration() + }) + } + navigateToNextExercise(router as SingletonRouter, user.data ?? userStore.user) + } + + function generateSame(word: string) { + return + } + function generateDifferent(pair: WordPair) { + return + } + + useEffect(() => { + if (!user.data) return + font.current = user.data.font + }, [user]) + + useEffect(() => { + setGrid(() => generateGrid()) + }, [words, pairs]) + + return ( +
+ {grid} +
+ ) +} + +type CellProps = { + font?: Font + different: boolean + word1: string + word2: string + id?: string + callback: (answer: 'correct' | 'error') => void +} + +function Cell({ font, different, word1, word2, id, callback }: CellProps) { + const [highlighted, setHighlighted] = useState(false) + + function handleClick() { + if (different && !highlighted) { + callback('correct') + setHighlighted(() => true) + } + else if (!different && !highlighted) { + setHighlighted(() => true) + callback('error') + } + } + return ( + handleClick()} + id={id?.toString() ?? '0'} + className={[ + 'items-center grid grid-cols-1 rounded-lg text-white', + 'md:text-3xl md:p-2', + 'text-2xl p-1', + `${highlighted ? (different ? 'bg-white/10' : 'bg-red-500/40') : 'bg-white/20'}`, + 'cursor-pointer', + ].join(' ')} + > +
+ {word1} +
+
+ {word2} +
+
+ ) +} diff --git a/src/pages/admin/testexercise.tsx b/src/pages/admin/testexercise.tsx index 3cd5e3e..dcbec79 100644 --- a/src/pages/admin/testexercise.tsx +++ b/src/pages/admin/testexercise.tsx @@ -38,6 +38,7 @@ export default function Page() { +
diff --git a/src/pages/exercises/wordpairs.tsx b/src/pages/exercises/wordpairs.tsx new file mode 100644 index 0000000..e7ab181 --- /dev/null +++ b/src/pages/exercises/wordpairs.tsx @@ -0,0 +1,18 @@ +import dynamic from 'next/dynamic' +import Sidebar from '~/components/sidebar' + +const WordPairs = dynamic(() => import('~/components/wordpairs'), { ssr: false }) +export default function Page() { + return ( +
+ +
+
+ +
+
+
+ ) +} diff --git a/src/pages/instructions/evennumbers.tsx b/src/pages/instructions/evennumbers.tsx index 60be8a6..cb114b5 100644 --- a/src/pages/instructions/evennumbers.tsx +++ b/src/pages/instructions/evennumbers.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from 'react' -import type { NextPage } from 'next' import Head from 'next/head' import LoadingSpinner from '~/components/loadingspinner' import { useUserStore } from '~/stores/userStore' diff --git a/src/pages/instructions/wordpairs.tsx b/src/pages/instructions/wordpairs.tsx new file mode 100644 index 0000000..bf8f636 --- /dev/null +++ b/src/pages/instructions/wordpairs.tsx @@ -0,0 +1,128 @@ +import { useState, useEffect } from 'react' +import Head from 'next/head' +import LoadingSpinner from '~/components/loadingspinner' +import { useUserStore } from '~/stores/userStore' +import type { Font, User } from '~/utils/types' +import { useRouter, type SingletonRouter } from "next/router"; +import { FontProvider } from "~/cva/fontProvider"; +import Sidebar from '~/components/sidebar' +import { navigate } from '~/utils/helpers' + +const INSTRUCTION_DELAY = 5_000 + +function Paragraph1({ user }: { user: User | undefined }) { + if (!user) return + if (user.language === 'english') return ( +
+

+ + Each cell will contian two words. In five of the cells the words will be + slightly different and the remaining cells contain matching words. + Click on all of the cells that contain two different words. + +
+ There is no time limit, though your time will be recorded to track your + progression. so try to go as quickly as you can while remaining + accurate. This exercise is designed to help you improve your ability to + focus and your perception. Try to stay relaxed and focused while you are + doing this exercise. It is up to you how you want to approach this + exercise. But we recommend that you either search the table row by row + or column by column. +

+
+ ) + // all of the following are grabbed from google translate and may not be accurate + // if you speak any of these languages and can correct them, please do so. + // TODO get proper translations + if (user.language === 'german') return ( +
+

+ Jede Zelle enthält zwei Wörter. In fünf der Zellen stehen die Wörter + etwas anders und die restlichen Zellen enthalten übereinstimmende Wörter. + Klicken Sie auf alle Zellen, die zwei verschiedene Wörter enthalten. + Es gibt keine zeitliche Begrenzung, Ihre Zeit wird jedoch aufgezeichnet, um Ihre Zeit zu verfolgen + Fortschreiten. Versuchen Sie also, so schnell wie möglich zu gehen, während Sie bleiben + genau. Diese Übung soll Ihnen dabei helfen, Ihre Fähigkeiten zu verbessern + Fokus und Ihre Wahrnehmung. Versuchen Sie dabei entspannt und konzentriert zu bleiben + diese Übung machen. Es liegt an Ihnen, wie Sie dies angehen möchten + Übung. Wir empfehlen Ihnen jedoch, die Tabelle entweder zeilenweise zu durchsuchen + oder Spalte für Spalte. +

+
+ ) + if (user.language === 'spanish') return ( +
+

+ Cada celda contendrá dos palabras. En cinco de las celdas las palabras estarán + ligeramente diferentes y las celdas restantes contienen palabras coincidentes. + Haga clic en todas las celdas que contengan dos palabras diferentes. + No hay límite de tiempo, aunque su tiempo se registrará para realizar un seguimiento de su + progresión. así que intenta ir lo más rápido que puedas mientras permaneces + preciso. Este ejercicio está diseñado para ayudarle a mejorar su capacidad para + enfoque y tu percepción. Intenta mantenerte relajado y concentrado mientras estás + haciendo este ejercicio. Depende de usted cómo quiere abordar esto + ejercicio. Pero le recomendamos que busque en la tabla fila por fila + o columna por columna. +

+
+ ) + if (user.language === 'italian') return ( +
+

+ Ogni cella conterrà due parole. In cinque celle ci saranno le parole + leggermente diverso e le celle rimanenti contengono parole corrispondenti. + Fare clic su tutte le celle che contengono due parole diverse. + Non c'è limite di tempo, anche se il tuo tempo verrà registrato per tenere traccia del tuo + progressione. quindi prova ad andare il più velocemente possibile rimanendo + accurato. Questo esercizio è progettato per aiutarti a migliorare la tua capacità di + concentrazione e la tua percezione. Cerca di rimanere rilassato e concentrato mentre lo sei + facendo questo esercizio. Dipende da te come vuoi affrontare questo problema + esercizio. Ma ti consigliamo di cercare nella tabella riga per riga + o colonna per colonna. +

+
+ ) +} + +function StartButton() { + const [time, setTime] = useState(false) + const router = useRouter() + + + useEffect(() => { + setTimeout(() => setTime(true), INSTRUCTION_DELAY) + }, []) + + return time ? ( + + ) : ( + + ) +} + +export default function Page() { + const userStore = useUserStore() + const [font, setFont] = useState('sans') + useEffect(() => { + if (!userStore.user) return + setFont(userStore.user.font) + }) + + return ( + <> + Even Number Exercise Instructions + + +
+ + +
+
+ + ) +} diff --git a/src/pages/nav.tsx b/src/pages/nav.tsx index 4477a9d..12504e5 100644 --- a/src/pages/nav.tsx +++ b/src/pages/nav.tsx @@ -169,6 +169,11 @@ export default function Page() { exercise='greenDot' user={user as User} /> + ) } diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 0dbd279..f783d7e 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -7,6 +7,7 @@ import { schulteSessionRouter, letterMatcherRouter, greenDotRouter, + wordPairsRouter, } from '~/server/api/routers/collector' import { createSpeedTestRouter, @@ -32,6 +33,7 @@ export const appRouter = createTRPCRouter({ boxFlasherSession: boxFlasherRouter, letterMatcherSession: letterMatcherRouter, greenDotSession: greenDotRouter, + wordPairSession: wordPairsRouter, }) // export type definition of API diff --git a/src/server/api/routers/app.ts b/src/server/api/routers/app.ts index fd556f7..1d157b4 100644 --- a/src/server/api/routers/app.ts +++ b/src/server/api/routers/app.ts @@ -61,7 +61,7 @@ export const userRouter = createTRPCRouter({ lastCubeByTwo: input.lastCubeByTwo, lastNumberGuesser: input.lastNumberGuesser, lastLetterMatcher: input.lastLetterMatcher, - lastWordPair: input.lastWordPair, + lastWordPairs: input.lastWordPairs, lastGreenDot: input.lastGreenDot, numberGuesserFigures: input.numberGuesserFigures, schulteLevel: input.schulteLevel, @@ -204,8 +204,9 @@ export const excercisesPropsRouter = createTRPCRouter({ .output(z.array(wordPairData)) .query(async ({ input, ctx }) => { const result = await ctx.prisma.$queryRaw>( - Prisma.sql`SELECT * FROM WordPair ORDER BY RANDOM() LIMIT ${input.count}`, + Prisma.sql`SELECT * FROM WordPair ORDER BY RAND() LIMIT ${input.count}`, ) + if (result === null || result === undefined) throw new Error('No result') return result }), }) diff --git a/src/server/api/routers/collector.ts b/src/server/api/routers/collector.ts index b002a26..ae5ce8c 100644 --- a/src/server/api/routers/collector.ts +++ b/src/server/api/routers/collector.ts @@ -7,6 +7,7 @@ import { numberGuesserData, schulteData, letterMatcherData, + wordPairSessionData, } from '~/utils/validators' export const highlightSessionRouter = createTRPCRouter({ @@ -121,3 +122,19 @@ export const greenDotRouter = createTRPCRouter({ }) }) }) + +export const wordPairsRouter = createTRPCRouter({ + setUnique: publicProcedure + .input(wordPairSessionData) + .mutation(async ({ input, ctx }) => { + await ctx.prisma.pairsSession.create({ + data: { + id: uuid(), + userId: ctx.auth.userId as string, + time: input.time, + errorCount: input.errorCount, + date: new Date(), + } + }) + }) +}) diff --git a/src/stores/usePairsStore.ts b/src/stores/usePairsStore.ts new file mode 100644 index 0000000..9df6df2 --- /dev/null +++ b/src/stores/usePairsStore.ts @@ -0,0 +1,20 @@ +import { create } from 'zustand' + +export const usePairsStore = create<{ + correct: number + errors: number + incrementCorrect: () => void + incrementErrors: () => void +}>()( + (set) => ({ + correct: 0, + errors: 0, + incrementCorrect: () => set((state) => ({ correct: state.correct + 1 })), + incrementErrors: () => set((state) => ({ errors: state.errors + 1 })), + }), +) + +export default usePairsStore + +export type PairsStore = typeof usePairsStore + diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 4740883..ab920cb 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -99,8 +99,9 @@ export function isAlreadyDone(user: User, exercise: Exercise) { case 'cubeByTwo': return isToday(user.lastCubeByTwo) case 'cubeByThree': return isToday(user.lastCubeByThree) case 'letterMatcher': return isToday(user.lastLetterMatcher) + case 'wordPairs': return isToday(user.lastWordPairs) case 'greenDot': return isToday(user.lastGreenDot) - default: return null + default: return null } } @@ -176,6 +177,8 @@ export function getNextURL(next: Exercise | undefined | null): string { return '/instructions/lettermatcher' case 'greenDot': return '/instructions/greendot' + case 'wordPairs': + return '/instructions/wordpairs' default: return '/done' } diff --git a/src/utils/types.ts b/src/utils/types.ts index 1b5ff5d..78707e1 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,5 +1,5 @@ import type { z } from 'zod' -import type { userSchema, wordPairData } from '~/utils/validators' +import type { language, userSchema, wordPairData } from '~/utils/validators' import type { speedTestSchema } from '~/utils/validators' export const Overlay = [ @@ -45,14 +45,8 @@ const Answer = ['A', 'B', 'C', 'D'] as const export type Answer = (typeof Answer)[number] -const Language = [ - 'english', - 'spanish', - 'german', - 'italian', -] as const -export type Language = (typeof Language)[number] +export type Language = z.infer export const Exercise = [ 'fourByOne', @@ -67,7 +61,8 @@ export const Exercise = [ 'cubeByThree', 'numberGuesser', 'letterMatcher', - 'greenDot' + 'wordPairs', + 'greenDot', ] as const /** diff --git a/src/utils/validators.ts b/src/utils/validators.ts index 6664d69..a569547 100644 --- a/src/utils/validators.ts +++ b/src/utils/validators.ts @@ -2,6 +2,13 @@ import { z } from 'zod' //zod is a library for data validation and parsing //in this code base z represents the zod validation library and its members +export const language = z.union([ + z.literal('english'), + z.literal('spanish'), + z.literal('german'), + z.literal('italian'), +]) + export const userSchema = z.object({ id: z.string(), firstName: z.string(), @@ -38,13 +45,7 @@ export const userSchema = z.object({ z.literal('ibmPlexMono'), ]) .default('sans'), - language: z.union([ - z.literal('english'), - z.literal('spanish'), - z.literal('german'), - z.literal('italian'), - ]) - .default('english'), + language: language.default('english'), lastSchulte: z.string().default(' '), lastSpeedTest: z.string().default(' '), lastFourByOne: z.string().default(' '), @@ -58,7 +59,7 @@ export const userSchema = z.object({ lastCubeByTwo: z.string().default(' '), lastLetterMatcher: z.string().default(' '), lastGreenDot: z.string().default(' '), - lastWordPair: z.string().default(' '), + lastWordPairs: z.string().default(' '), numberGuesserFigures: z.number().default(0), schulteLevel: z.union([ z.literal('three'), @@ -86,12 +87,7 @@ export const speedTestSchema = z.object({ const randomWordInputs = z.object({ number: z.number(), max: z.number(), - language: z.union([ - z.literal('english'), - z.literal('spanish'), - z.literal('german'), - z.literal('italian'), - ]), + language: language, }) const exercise = z.union([ @@ -166,22 +162,18 @@ export const boxFlasherData = z.object({ export const wordPairData = z.object({ primaryWord: z.string(), secondaryWord: z.string(), - language: z.union([ - z.literal('english'), - z.literal('spanish'), - z.literal('german'), - z.literal('italian'), - ]), + language: language, +}) + +export const wordPairSessionData = z.object({ + userId: z.string(), + errorCount: z.number(), + time: z.number(), }) export const wordPairProps = z.object({ count: z.number(), - language: z.union([ - z.literal('english'), - z.literal('spanish'), - z.literal('german'), - z.literal('italian'), - ]), + language: language, }) export const schemas = {