diff --git a/src/components/Fieldset.tsx b/src/components/Fieldset.tsx index 959f701f..65a432ed 100644 --- a/src/components/Fieldset.tsx +++ b/src/components/Fieldset.tsx @@ -7,5 +7,6 @@ export const Fieldset = styled.fieldset` min-width: 0; legend { display: table; + width: 100%; } `; diff --git a/src/routing/AllRoutes.tsx b/src/routing/AllRoutes.tsx index a7241a47..d3a01c7b 100644 --- a/src/routing/AllRoutes.tsx +++ b/src/routing/AllRoutes.tsx @@ -1,14 +1,12 @@ import { Navigate, Route, Routes } from "react-router-dom"; import { App } from "../App"; -import { LessonArchive } from "../views/lessons/LessonArchive"; +import { LessonHistoryPage } from "../views/lessons/LessonHistoryPage"; import { ViewLesson } from "../views/lessons/ViewLesson"; import { PracticeLesson } from "../views/practice/PracticeLesson"; -import { MyTerms } from "../views/terms/MyTerms"; import { CoursesPage } from "../views/vocabulary/CoursesPage"; -import { MySets } from "../views/vocabulary/MySets"; import { ViewCollection } from "../views/vocabulary/ViewCollection"; import { ViewSet } from "../views/vocabulary/ViewSet"; -import { Settings } from "../views/settings/Settings"; +import { SettingsPage } from "../views/settings/SettingsPage"; import { SignInPage } from "../views/signin/SignInPage"; import { CreateAccountPage } from "../views/signin/CreateAccountPage"; import { ForgotPasswordPage } from "../views/signin/ForgotPasswordPage"; @@ -54,14 +52,14 @@ export function AllRoutes() { {/* }> */} {/* } /> */} - } /> + } /> } /> } /> }> - } /> + } /> diff --git a/src/views/lessons/LessonArchive.tsx b/src/views/lessons/LessonArchive.tsx deleted file mode 100644 index 7e5e913d..00000000 --- a/src/views/lessons/LessonArchive.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, { ReactElement } from "react"; -import { Duration } from "luxon"; -import { SmallLoader } from "../../components/Loader"; -import { SectionHeading } from "../../components/SectionHeading"; -import { StyledLink } from "../../components/StyledLink"; -import { StyledTable } from "../../components/StyledTable"; -import { VisuallyHidden } from "../../components/VisuallyHidden"; -import { useAuth } from "../../firebase/AuthProvider"; -import { - useAnalyticsPageName, - useFirebaseAllLessonMetadata, -} from "../../firebase/hooks"; -import { Lesson, nameForLesson } from "../../state/reducers/lessons"; -import { ViewLessonPath } from "../../routing/paths"; - -type FinishedLesson = Lesson & { completedAt: number }; - -export function LessonArchive(): ReactElement { - useAnalyticsPageName("Lesson archive"); - const { user } = useAuth(); - - const [firebaseResult, _] = useFirebaseAllLessonMetadata(user); - - if (!firebaseResult.ready) - return ( -
- -
- ); - - const lessons = firebaseResult.data ?? {}; - - const finishedLessons = Object.values(lessons) - .filter((l): l is FinishedLesson => Boolean(l.completedAt)) - // most recent first - .sort((a, b) => b.completedAt - a.completedAt); - return ( -
- Lessons archive -

- Here, you can review lessons you've already completed. This can help you - know how long lessons of different sizes take for you to complete. -

- {finishedLessons.length ? ( - - - - Name - Number of challenges - Duration - - Link to view lesson details - - - - - {finishedLessons.map((lesson, idx) => ( - - ))} - - - ) : ( -

- You have not completed any lessons. Head over to the{" "} - dashboard to start learning! -

- )} -
- ); -} - -function FinishedLessonRow({ lesson }: { lesson: FinishedLesson }) { - return ( - - {nameForLesson(lesson)} - {lesson.numChallenges || "Unknown number of challenges"} - - {lesson.startedAt - ? Duration.fromObject({ - milliseconds: lesson.completedAt - lesson.startedAt, - }) - .shiftTo("minutes", "seconds", "milliseconds") - .mapUnits((x, u) => (u === "milliseconds" ? 0 : x)) - .shiftTo("minutes", "seconds") - .toHuman() - : "--"} - - - Details - - - ); -} diff --git a/src/views/lessons/LessonHistoryPage.tsx b/src/views/lessons/LessonHistoryPage.tsx new file mode 100644 index 00000000..fae187bf --- /dev/null +++ b/src/views/lessons/LessonHistoryPage.tsx @@ -0,0 +1,91 @@ +import React, { ReactElement } from "react"; +import { Duration } from "luxon"; +import { SmallLoader } from "../../components/Loader"; +import { SectionHeading } from "../../components/SectionHeading"; +import { StyledLink } from "../../components/StyledLink"; +import { StyledTable } from "../../components/StyledTable"; +import { VisuallyHidden } from "../../components/VisuallyHidden"; +import { useAuth } from "../../firebase/AuthProvider"; +import { + useAnalyticsPageName, + useFirebaseAllLessonMetadata, +} from "../../firebase/hooks"; +import { Lesson, nameForLesson } from "../../state/reducers/lessons"; +import { ViewLessonPath } from "../../routing/paths"; +import { HanehldaView } from "../../components/HanehldaView"; +import { DefaultNav } from "../../components/HanehldaView/HanehldaNav"; +import { Button, ButtonLink } from "../../components/Button"; +import { Link } from "react-router-dom"; + +type FinishedLesson = Lesson & { completedAt: number }; + +export function LessonHistoryPage(): ReactElement { + useAnalyticsPageName("Lesson archive"); + const { user } = useAuth(); + + const [firebaseResult, _] = useFirebaseAllLessonMetadata(user); + + if (!firebaseResult.ready) + return ( +
+ +
+ ); + + const lessons = firebaseResult.data ?? {}; + + const finishedLessons = Object.values(lessons) + .filter((l): l is FinishedLesson => Boolean(l.completedAt)) + // most recent first + .sort((a, b) => b.completedAt - a.completedAt); + return ( + } collapseNav> +
+ Lessons archive +

+ Here, you can review lessons you've already completed. This can help + you know how long lessons of different sizes take for you to complete. +

+ {finishedLessons.length ? ( + + + + Date completed + + Link to view lesson details + + + + + {finishedLessons.map((lesson, idx) => ( + + ))} + + + ) : ( +

+ You have not completed any lessons. Head over to the{" "} + dashboard to start learning! +

+ )} +
+
+ ); +} + +function FinishedLessonRow({ lesson }: { lesson: FinishedLesson }) { + return ( + + {new Date(lesson.createdFor).toDateString()} + + + + + ); +} diff --git a/src/views/lessons/ViewLesson.tsx b/src/views/lessons/ViewLesson.tsx index 2a91f295..8a2dcc54 100644 --- a/src/views/lessons/ViewLesson.tsx +++ b/src/views/lessons/ViewLesson.tsx @@ -1,5 +1,5 @@ import React, { ReactElement } from "react"; -import { Navigate, useNavigate, useParams } from "react-router-dom"; +import { Link, Navigate, useNavigate, useParams } from "react-router-dom"; import { Card, cards, keyForCard } from "../../data/cards"; import { nameForLesson } from "../../state/reducers/lessons"; import { ReviewResult } from "../../state/reducers/leitnerBoxes"; @@ -9,7 +9,15 @@ import { SectionHeading } from "../../components/SectionHeading"; import { CardTable } from "../../components/CardTable"; import { StyledLink } from "../../components/StyledLink"; import { useAnalyticsPageName } from "../../firebase/hooks"; -import { LessonsPath } from "../../routing/paths"; +import { DashboardPath, LessonsPath } from "../../routing/paths"; +import { BlueEm } from "../settings/SettingsPage"; +import { HanehldaView } from "../../components/HanehldaView"; +import { DefaultNav } from "../../components/HanehldaView/HanehldaNav"; +import styled from "styled-components"; +import { LearnPage } from "../learn/LearnPage"; +import { Button } from "../../components/Button"; +import { theme } from "../../theme"; +import { Hr } from "../setup/common"; export function ViewLesson(): ReactElement { useAnalyticsPageName("View lesson"); @@ -32,6 +40,18 @@ const reviewResultNames: Record = { REPEAT_MISTAKE: "Multiple mistakes", }; +const ContentWrapper = styled.div` + padding: 5px 20px; +`; + +const ReviewedCardsWrapper = styled.div` + h3 { + text-align: left; + margin: 10px; + font-size: ${theme.fontSizes.md}; + } +`; + export function _ViewLesson(): ReactElement { const { lesson, reviewedTerms } = useLesson(); const reviewedCards = useCardsForTerms( @@ -54,25 +74,40 @@ export function _ViewLesson(): ReactElement { } ); return ( -
- Lesson debrief - {nameForLesson(lesson)} -

- Take a look at how you did! If you were confused by a term, go ahead and - listen to all the alternate pronouncations by hitting the "More" button. -

-

- Head back to the dashboard to keep - learning! -

- {Object.entries(reviewedCardsByStatus).map( - ([result, cards]) => - cards.length > 0 && ( -
-

{reviewResultNames[result as ReviewResult]}

- -
- ) - )} -
+ } collapseNav> +
+ +

Lesson debrief

+ + {nameForLesson(lesson)} + +

+ Take a look at how you did! If you were confused by a term, go ahead + and listen to all the alternate pronouncations by hitting the "More" + button. +

+ +
+
+ + {Object.entries(reviewedCardsByStatus).map( + ([result, cards]) => + cards.length > 0 && ( +
+

{reviewResultNames[result as ReviewResult]}

+ +
+ ) + )} +
+ + + +
+
); } diff --git a/src/views/settings/Settings.tsx b/src/views/settings/Settings.tsx deleted file mode 100644 index b605bbf3..00000000 --- a/src/views/settings/Settings.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { ChangeEvent, useId, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import styled from "styled-components"; -import { Button } from "../../components/Button"; -import { SectionHeading } from "../../components/SectionHeading"; -import { useAnalyticsPageName } from "../../firebase/hooks"; -import { lessonKey } from "../../state/reducers/lessons"; -import { - isPhoneticsPreference, - PREFERENCE_LITERATES, -} from "../../state/reducers/phoneticsPreference"; -import { useUserStateContext } from "../../providers/UserStateProvider"; -import { UserState } from "../../state/useUserState"; -import { useAuth } from "../../firebase/AuthProvider"; -import { signOut } from "firebase/auth"; -import { auth } from "../../firebase"; -import { HanehldaView } from "../../components/HanehldaView"; -import { DefaultNav } from "../../components/HanehldaView/HanehldaNav"; - -interface ExportedLessonData { - lessonId: string; - reviewedTerms: string | null; - timings: string | null; -} - -export function Settings() { - const { - config: { userEmail }, - } = useUserStateContext(); - const { user } = useAuth(); - useAnalyticsPageName("Settings"); - return ( - } collapseNav> -
- -
- User identity -

User id: {user.uid}

-

- We have your email on file as: {userEmail} -

- {!user.isAnonymous && ( - - )} -

- - Wrong address? Contact the maintainer at{" "} - - charliemcvicker@protonmail.com - - -

-
-
-
-

- - Settings below this point might not be much use to you unless a - maintainer of this website contacted you. - -

- -
-
- ); -} - -const PreferencesForm = styled.form` - display: grid; - grid-template-columns: auto 1fr; - grid-gap: 8px; -`; - -function Preferences() { - const { - setPhoneticsPreference, - config: { phoneticsPreference }, - } = useUserStateContext(); - const phoneticsPreferenceId = useId(); - - function onPhoneticsPreferenceChanged(event: ChangeEvent) { - event.preventDefault(); - const newPreference = event.target.value; - if (isPhoneticsPreference(newPreference)) { - setPhoneticsPreference(newPreference); - } - } - - return ( -
- Preferences - - - - -
- ); -} - -function ImportExportDataConsole() { - const userState = useUserStateContext(); - const [fileToLoad, setFileToLoad] = useState(null); - const navigate = useNavigate(); - - function downloadAllData() { - // this will need to be updated as data is added to user state. if you get - // a type error here, you probably added a new top level field to user - // state, and need to add that key here. - const fieldsToSave: Record = { - leitnerBoxes: null, - // lessons: null, - config: null, - }; - - const stateToSave = Object.keys(fieldsToSave).reduce( - (obj, key) => ({ ...obj, [key]: userState[key as keyof UserState] }), - {} - ); - - // const lessonData: ExportedLessonData[] = Object.keys(userState).map( - // (lessonId) => ({ - // lessonId, - // reviewedTerms: window.localStorage.getItem( - // lessonKey(lessonId) + "/reviewed-terms" - // ), - // timings: window.localStorage.getItem(lessonKey(lessonId) + "/timings"), - // }) - // ); - - const dataStr = - "data:text/json;charset=utf-8," + - encodeURIComponent(JSON.stringify({ ...stateToSave /** lessonData */ })); - const dlAnchorElem = document.createElement("a"); - - dlAnchorElem.setAttribute("href", dataStr); - dlAnchorElem.setAttribute("download", "cherokeeLanguageExercisesData.json"); - - dlAnchorElem.click(); - } - - function onLoadFileChanged(event: ChangeEvent) { - const files = event.target.files; - if (!files || files.length !== 1) { - setFileToLoad(null); - } else { - setFileToLoad(files[0]); - } - } - - function loadData() { - if (fileToLoad) - fileToLoad.text().then((data) => { - const { lessonData, ...state } = JSON.parse(data); - // load lesson data - (lessonData ?? []).forEach((exported: ExportedLessonData) => { - if (exported.reviewedTerms) { - window.localStorage.setItem( - lessonKey(exported.lessonId) + "/reviewed-terms", - exported.reviewedTerms - ); - } - - if (exported.timings) { - window.localStorage.setItem( - lessonKey(exported.lessonId) + "/timings", - exported.timings - ); - } - }); - - // load larger user state - // userState.loadState(state); - localStorage.setItem("user-state", JSON.stringify(state)); - - navigate("/"); - }); - } - return ( -
- Export data - -
-
- Import data -
- - - -
-
- ); -} diff --git a/src/views/settings/SettingsPage.tsx b/src/views/settings/SettingsPage.tsx new file mode 100644 index 00000000..0d567583 --- /dev/null +++ b/src/views/settings/SettingsPage.tsx @@ -0,0 +1,164 @@ +import { + ChangeEvent, + FormEvent, + ReactElement, + useId, + useMemo, + useState, +} from "react"; +import { Link, useNavigate } from "react-router-dom"; +import styled from "styled-components"; +import { Button } from "../../components/Button"; +import { useAnalyticsPageName } from "../../firebase/hooks"; +import { lessonKey } from "../../state/reducers/lessons"; +import { PhoneticsPreference } from "../../state/reducers/phoneticsPreference"; +import { useUserStateContext } from "../../providers/UserStateProvider"; +import { UserState } from "../../state/useUserState"; +import { useAuth } from "../../firebase/AuthProvider"; +import { signOut } from "firebase/auth"; +import { auth } from "../../firebase"; +import { HanehldaView } from "../../components/HanehldaView"; +import { DefaultNav } from "../../components/HanehldaView/HanehldaNav"; +import { RadioBar } from "../../components/RadioBar"; +import { cards } from "../../data/cards"; +import { getPhonetics } from "../../utils/phonetics"; +import { Hr } from "../setup/common"; +import { theme } from "../../theme"; +import { LessonsPath } from "../../routing/paths"; + +interface ExportedLessonData { + lessonId: string; + reviewedTerms: string | null; + timings: string | null; +} + +export function SettingsPage() { + useAnalyticsPageName("Settings"); + return ( + } collapseNav> +
+ +
+ +
+ +
+

+ + If you have any questions please contact a maintainer at{" "} + + charliemcvicker@protonmail.com + + +

+
+
+ ); +} + +const PreferencesForm = styled.form` + display: grid; + grid-template-columns: auto 1fr; + grid-gap: 8px; + text-align: center; +`; + +function Preferences() { + const { + setPhoneticsPreference, + config: { phoneticsPreference }, + } = useUserStateContext(); + + const showTone = useMemo( + () => phoneticsPreference === PhoneticsPreference.Detailed, + [phoneticsPreference] + ); + + function onShowToneChanged(newValue: string) { + const newPhoneticsPreference = + newValue === "yes" + ? PhoneticsPreference.Detailed + : PhoneticsPreference.Simple; + setPhoneticsPreference(newPhoneticsPreference); + } + + const demoCard = useMemo( + () => cards.find((c) => c.syllabary === "ᎠᏴᏓᏆᎶᏍᎩ")!, + [] + ); + + const phoneticsPreview = useMemo( + () => getPhonetics(demoCard, phoneticsPreference), + [phoneticsPreference] + ); + + return ( +
+

Preferences

+ +
+ +

+ Example: {phoneticsPreview} +

+ +

+ + Tone is an important aspect of Cherokee, but can be daunting when + you're just getting started. You can always change this setting + later. + +

+
+
+
+ ); +} + +export const BlueEm = styled.em` + font-style: normal; + color: ${theme.hanehldaColors.DARK_BLUE}; +`; + +function UserIdentity(): ReactElement { + const { + config: { userEmail }, + } = useUserStateContext(); + const { user } = useAuth(); + return ( +
+

User identity

+

+ User id: {user.uid} +

+

+ We have your email on file as: {userEmail} +

+ {!user.isAnonymous && ( + + )} +
+ ); +} + +function LessonArchiveLink(): ReactElement { + return ( +
+

Lesson history

+

You can browse previously completed lessons below.

+ +
+ ); +}