Skip to content

Commit

Permalink
Add the ability to view word definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
alexkiro committed Mar 31, 2024
1 parent 5b62eb8 commit fd96f4d
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 18 deletions.
24 changes: 24 additions & 0 deletions src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,30 @@ body {
background-color: var(--ocean);
}

.fg-white {
color: white;
}

.fg-peach {
color: var(--dark-peach);
}

.fg-dark-peach {
color: var(--peach);
}

.fg-ocean {
color: var(--dark-ocean);
}

.fg-dark-ocean {
color: var(--ocean);
}

.pointer {
cursor: pointer;
}

.btn {
display: flex;

Expand Down
209 changes: 191 additions & 18 deletions src/components/FoundWordList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,140 @@
:key="word"
:data-word="word"
:class="{ highlight: (highlightWords ?? []).indexOf(word) !== -1 }"
@click="selectedWord = word"
>
{{ word }}
</span>
</div>
</div>
</div>
<native-dialog :show="!!selectedWord" class="brutal-border" @close="selectedWord = ''">
<header>
<div>
<div>
<b>{{ selectedWord }}</b>
word definition
</div>
<a :href="selectedWordWikiLink" target="_blank" rel="noreferrer noopener">(from wiktionary)</a>
</div>
<div class="fg-dark-ocean pointer" @click="selectedWord = ''"><b>Close</b></div>
</header>

<loading-spinner v-if="loadingDefinitions" />
<ul v-else-if="definitions.length > 0">
<li v-for="group in definitions">

Check failure on line 32 in src/components/FoundWordList.vue

View workflow job for this annotation

GitHub Actions / check-code-style

Elements in iteration expect to have 'v-bind:key' directives
<b>{{ group.partOfSpeech }}</b>
<ol>
<template v-for="definition in group.definitions">
<li v-if="definition.definition">

Check failure on line 36 in src/components/FoundWordList.vue

View workflow job for this annotation

GitHub Actions / check-code-style

Elements in iteration expect to have 'v-bind:key' directives
{{ stripHtml(definition.definition) }}
<ul v-if="definition.examples">
<li v-for="example in definition.examples">

Check failure on line 39 in src/components/FoundWordList.vue

View workflow job for this annotation

GitHub Actions / check-code-style

Elements in iteration expect to have 'v-bind:key' directives
<i>{{ stripHtml(example) }}</i>
</li>
</ul>
</li>
</template>
</ol>
</li>
</ul>
<div v-else>Unable to find definitions</div>
</native-dialog>
</template>

<script setup lang="ts">
import { computed } from "vue";
<script lang="ts">
import { defineComponent } from "vue";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
import NativeDialog from "@/components/NativeDialog.vue";
type WikiDefinition = {
partOfSpeech: string;
language: string;
definitions: [
{
definition: string;
examples?: string[];
},
];
};
const props = defineProps({
foundWords: {
required: true,
type: Array<string>,
// maps lang codes to list of definitions
type WikiDefinitionResponse = Record<string, WikiDefinition[]>;
const invalidPartsOfSpeech = new Set(["proper noun", "phrase", "symbol"]);
export default defineComponent({
components: { NativeDialog, LoadingSpinner },
props: {
foundWords: {
required: true,
type: Array<string>,
},
highlightWords: {
required: false,
type: Array<string>,
default: null,
},
},
highlightWords: {
required: false,
type: Array<string>,
default: null,
data() {
return {
controller: null as AbortController | null,
selectedWord: "",
definitions: [] as WikiDefinition[],
loadingDefinitions: false,
};
},
computed: {
selectedWordWikiLink() {
return `https://en.wiktionary.org/wiki/${this.selectedWord.toLowerCase()}#English`;
},
wordsByLength(): Record<number, string[]> {
const result: Record<number, string[]> = {};
for (const word of this.foundWords) {
result[word.length] ??= [];
result[word.length].push(word);
}
return result;
},
},
watch: {
async selectedWord() {
if (!this.selectedWord) return;
this.controller?.abort();
this.controller = new AbortController();
try {
this.loadingDefinitions = true;
const result: WikiDefinition[] = [];
const url = new URL(`https://en.wiktionary.org/api/rest_v1/page/definition/${this.selectedWord.toLowerCase()}`);
const response = await fetch(url, {
signal: this.controller.signal,
});
const data = (await response.json()) as WikiDefinitionResponse;
for (const item of data.en || []) {
if (item.language.toLowerCase() !== "english") {
continue;
}
if (invalidPartsOfSpeech.has(item.partOfSpeech.toLowerCase())) {
continue;
}
result.push(item);
}
this.definitions = result;
} finally {
this.loadingDefinitions = false;
}
},
},
methods: {
stripHtml(html: string): string {
let doc = new DOMParser().parseFromString(html, "text/html");

Check failure on line 141 in src/components/FoundWordList.vue

View workflow job for this annotation

GitHub Actions / check-code-style

'doc' is never reassigned. Use 'const' instead
return doc.body.textContent || "";
},
},
});
const wordsByLength = computed(() => {
const result: Record<number, string[]> = {};
for (const word of props.foundWords) {
result[word.length] ??= [];
result[word.length].push(word);
}
return result;
});
</script>

Expand All @@ -45,6 +150,7 @@ const wordsByLength = computed(() => {
overflow-y: auto;
display: flex;
flex-direction: column;
padding: 0 1rem;
}
.found-words > *:first-child {
Expand All @@ -69,6 +175,7 @@ const wordsByLength = computed(() => {
}
.word-list span {
cursor: pointer;
font-size: 16px;
background-color: white;
border: 2px solid var(--border-color);
Expand All @@ -77,8 +184,74 @@ const wordsByLength = computed(() => {
box-shadow: 1px 2px #000;
}
.word-list span:hover {
background-color: var(--ocean);
color: white;
}
.word-list span.highlight {
background-color: var(--ocean);
color: white;
}
dialog[open] {
background-color: white;
min-height: 40%;
min-width: 90%;
max-height: 80%;
max-width: 90%;
margin: auto;
display: flex;
flex-direction: column;
overflow: auto;
}
dialog::backdrop {
background-color: rgba(10, 10, 10, 0.8);
}
header {
padding-bottom: 1rem;
margin-bottom: 0.5rem;
display: flex;
align-items: flex-start;
justify-content: space-between;
border-bottom: 2px solid var(--border-color);
}
a {
display: block;
margin-top: 0.5rem;
}
ul,
ol {
margin: 0.5rem 0;
font-size: 16px;
}
dialog > ul > li {
margin-bottom: 1.5rem;
}
ol > li {
list-style: decimal inside;
margin-top: 0.5rem;
}
ol ul > li {
margin-left: 0.5rem;
}
b {
font-weight: bold;
}
i {
font-style: italic;
color: rgba(47, 47, 47, 0.8);
}
</style>
44 changes: 44 additions & 0 deletions src/components/LoadingSpinner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<div class="loader-container">
<div class="loader"></div>
</div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({});
</script>

<style scoped>
.loader-container {
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
}
.loader {
width: 40px;
aspect-ratio: 1;
border-radius: 50%;
background: var(--dark-peach);
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
animation: l1 2s infinite cubic-bezier(0.3, 1, 0, 1);
}
@keyframes l1 {
33% {
border-radius: 0;
background: var(--dark-ocean);
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
66% {
border-radius: 0;
background: var(--candle);
clip-path: polygon(50% 0, 50% 0, 100% 100%, 0 100%);
}
}
</style>
25 changes: 25 additions & 0 deletions src/components/NativeDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template>
<dialog><slot /></dialog>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: {
show: {
type: Boolean,
default: false,
},
},
watch: {
show() {
if (this.show) {
(this.$el as HTMLDialogElement).showModal();
} else {
(this.$el as HTMLDialogElement).close();
}
},
},
});
</script>

0 comments on commit fd96f4d

Please sign in to comment.