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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ it for you. Any text not translated yet shows in English.
To change the English wording or add a language, see
[docs/translation.md](docs/translation.md).

## Audio

The exercise clips and their sentences come from
[Mozilla Common Voice](https://commonvoice.mozilla.org/)'s Thai dataset:
sentences read aloud by volunteers and released into the public domain under
CC0. The clips aren't stored in this repository; they are loaded from a local
copy of the dataset (see [Running it locally](#running-it-locally)). You can
record or check Thai sentences for the dataset at
[commonvoice.mozilla.org/th](https://commonvoice.mozilla.org/th).

## License

AGPL-3.0-or-later — see [LICENSE](LICENSE). Running a modified copy of this
Expand Down
70 changes: 70 additions & 0 deletions src/frontend/e2e/footer.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { expect, test } from '@playwright/test'

import { LONGEST, mockBackend, sentenceOnScreen } from './fixtures'

/** The lowest point any part of the exercise reaches, and the footer's box. */
async function layout(page) {
return page.evaluate(() => ({
contentBottom: Math.max(
...[...document.querySelector('main').children].map((el) => el.getBoundingClientRect().bottom),
),
footer: document.querySelector('footer').getBoundingClientRect().toJSON(),
scrolls: document.documentElement.scrollHeight > window.innerHeight,
}))
}

test.describe('the footer', () => {
test('links to the source code', async ({ page }) => {
await mockBackend(page)
await page.goto('/')
await sentenceOnScreen(page)

const footer = page.getByRole('contentinfo')
await expect(footer.getByRole('link', { name: 'Source code on GitHub' })).toHaveAttribute(
'href',
'https://github.com/Akay7/TypeLearn',
)
})

// The laptop screen the "fits on one screen" requirement names: the footer
// must not be what pushes the keyboard below the fold.
test('fits under the longest exercise without scrolling the page', async ({ page }) => {
await mockBackend(page, [LONGEST])
await page.goto('/')
await sentenceOnScreen(page)
await page.waitForFunction(() => document.fonts.status === 'loaded')

const { contentBottom, footer, scrolls } = await layout(page)

expect(scrolls).toBe(false)
expect(footer.top).toBeGreaterThanOrEqual(contentBottom)
})

test.describe('on a screen shorter than the exercise', () => {
test.use({ viewport: { width: 1280, height: 560 } })

test('sits below the keyboard legend instead of over it', async ({ page }) => {
await mockBackend(page, [LONGEST])
await page.goto('/')
await sentenceOnScreen(page)
await page.waitForFunction(() => document.fonts.status === 'loaded')

const { contentBottom, footer, scrolls } = await layout(page)

expect(scrolls).toBe(true)
expect(footer.top).toBeGreaterThanOrEqual(contentBottom)
})
})

test('follows the interface language', async ({ page }) => {
await mockBackend(page)
await page.goto('/')
await sentenceOnScreen(page)

await page.locator('button[aria-haspopup="true"]').click()
await page.locator('#interface-language').selectOption('ru')

const footer = page.getByRole('contentinfo')
await expect(footer.getByRole('link', { name: 'Исходный код на GitHub' })).toBeVisible()
})
})
37 changes: 28 additions & 9 deletions src/frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
<script setup>
import { onMounted } from 'vue'
import { useI18n } from 'vue-i18n'

import SentenceView from './components/SentenceView.vue'
import SettingsMenu from './components/SettingsMenu.vue'
import { useExerciseStore } from './stores/exercise'

const REPOSITORY_URL = 'https://github.com/Akay7/TypeLearn'

const store = useExerciseStore()
const { t } = useI18n()

onMounted(() => store.load())
</script>

<template>
<main class="relative mx-auto flex min-h-screen max-w-4xl flex-col items-center justify-center gap-4 px-6 py-4">
<!-- Positioned rather than laid out beside the title: the title's row
already has to stay as short as "the exercise fits on one screen"
needs, and a 40px control sitting in that row would cost every
viewport that height whether or not the menu is ever opened. -->
<SettingsMenu class="absolute top-4 right-6" />
<h1 class="text-sm font-medium tracking-[0.3em] uppercase opacity-60">TypeLearn</h1>
<SentenceView />
</main>
<div class="relative">
<main class="relative mx-auto flex min-h-screen max-w-4xl flex-col items-center justify-center gap-4 px-6 py-4">
<!-- Positioned rather than laid out beside the title: the title's row
already has to stay as short as "the exercise fits on one screen"
needs, and a 40px control sitting in that row would cost every
viewport that height whether or not the menu is ever opened. -->
<SettingsMenu class="absolute top-4 right-6" />
<h1 class="text-sm font-medium tracking-[0.3em] uppercase opacity-60">TypeLearn</h1>
<SentenceView />
</main>

<!-- Positioned for the same reason as the settings button: a row under the
exercise would push the keyboard below the fold on a laptop screen.
Pinned to the bottom edge instead, one `leading-4` line tall — exactly
`main`'s `py-4` bottom padding — so when the exercise is taller than
the screen the footer sits in that padding rather than over the
keyboard legend. The link opens a new tab so an exercise in progress
is not lost. -->
<footer class="absolute inset-x-0 bottom-0 flex justify-center text-xs leading-4 opacity-60">
<a :href="REPOSITORY_URL" target="_blank" rel="noopener noreferrer" class="hover:text-(--text-h) hover:underline">
{{ t('footer.source') }}
</a>
</footer>
</div>
</template>
3 changes: 3 additions & 0 deletions src/frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,8 @@
"today": "Heute",
"last7Days": "Letzte 7 Tage",
"next": "Nächste Übung →"
},
"footer": {
"source": "Quellcode auf GitHub"
}
}
3 changes: 3 additions & 0 deletions src/frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,8 @@
"today": "Today",
"last7Days": "Last 7 days",
"next": "Next exercise →"
},
"footer": {
"source": "Source code on GitHub"
}
}
3 changes: 3 additions & 0 deletions src/frontend/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,8 @@
"today": "Aujourd'hui",
"last7Days": "7 derniers jours",
"next": "Exercice suivant →"
},
"footer": {
"source": "Code source sur GitHub"
}
}
3 changes: 3 additions & 0 deletions src/frontend/src/locales/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,8 @@
"today": "Ma",
"last7Days": "Elmúlt 7 nap",
"next": "Következő gyakorlat →"
},
"footer": {
"source": "Forráskód a GitHubon"
}
}
3 changes: 3 additions & 0 deletions src/frontend/src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,8 @@
"today": "Сегодня",
"last7Days": "Последние 7 дней",
"next": "Следующее упражнение →"
},
"footer": {
"source": "Исходный код на GitHub"
}
}
3 changes: 3 additions & 0 deletions src/frontend/src/locales/th.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,8 @@
"today": "วันนี้",
"last7Days": "7 วันที่ผ่านมา",
"next": "แบบฝึกหัดถัดไป →"
},
"footer": {
"source": "ซอร์สโค้ดบน GitHub"
}
}