This commit is contained in:
Philippe Torrel
2026-08-21 11:02:52 +02:00
parent a08de03d4d
commit e2da42e181
140 changed files with 16542 additions and 8 deletions

View File

@@ -0,0 +1,52 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { HeartFilled, HeartIcon } from './Icons';
interface FavoriteWordProps {
word: string;
}
const FavoriteWord: React.FC<FavoriteWordProps> = ({ word }) => {
const [isFavorite, setIsFavorite] = useState(false);
const toggleFavorite = () => {
const favorites = JSON.parse(localStorage.getItem('favoriteWords') || '[]');
if (favorites.includes(word)) {
const filteredFavorites = favorites.filter((item: string) => item !== word);
localStorage.setItem('favoriteWords', JSON.stringify(filteredFavorites));
} else {
favorites.push(word);
localStorage.setItem('favoriteWords', JSON.stringify(favorites));
}
setIsFavorite(!isFavorite);
};
// [x] Missing useEffect
useEffect(() => {
const favoriteWords = JSON.parse(localStorage.getItem('favoriteWords') || '[]');
setIsFavorite(favoriteWords.includes(word));
}, [word]);
return (
<div className="fave-word-wrapper">
<Link
to={`/?word=${word}`}
// to="/"
className="fave-word-link">
<p>{word}</p>
</Link>
<button className="fave-icon-btn" onClick={toggleFavorite}>
{isFavorite ? (
<HeartFilled className="heart-icon-filled" aria-hidden="true" />
) : (
<HeartIcon className="heart-icon-unfilled" aria-hidden="true" />
)}
</button>
</div>
);
};
export default FavoriteWord;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { SERIF_FONTS, SANS_SERIF_FONTS } from '../lib/constants';
interface HeaderProps {
font: string;
setFont: React.Dispatch<React.SetStateAction<string>>;
}
const Header: React.FC<HeaderProps> = ({ font, setFont }) => {
return (
<header className="header-container">
<Link to="/" className="header-logo-wrapper">
<img
src="/LexiFind.webp"
alt="LexiFind Logo"
className="header-logo-img"
/>
<span className="header-logo-text">LexiFind</span>
</Link>
<div>
<Link to="/favorites" className="header-link-text">
Favorites
</Link>
</div>
<select
value={font}
onChange={(e) => setFont(e.target.value)}
className="header-select"
style={{ fontFamily: font }}
>
<option value={SERIF_FONTS}>Serif</option>
<option value={SANS_SERIF_FONTS}>Sans Serif</option>
</select>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,46 @@
export function HeartIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
/>
</svg>
);
}
export function HeartFilled(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
{...props}
>
<path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" />
</svg>
);
}
export function PlayIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
{...props}
>
<path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393" />
</svg>
);
}

View File

@@ -0,0 +1,59 @@
import React from 'react';
interface SearchbarProps {
word: string;
setWord: React.Dispatch<React.SetStateAction<string>>;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
}
const Searchbar: React.FC<SearchbarProps> = ({
word,
setWord,
handleSubmit,
}) => {
return (
<div className="searchbar-wrapper">
<form onSubmit={handleSubmit}>
<label htmlFor="searchWord" className="screen-reader-text">
Search word
</label>
<div className="searchbar-container">
<input
type="text"
name="searchWord"
id="searchWord"
className="searchbar-input"
placeholder="Type a word to search"
value={word}
onChange={(e) => setWord(e.target.value)}
/>
<button
type="submit"
className="searchbar-magnifying-glass"
aria-label="Search"
>
<SearchIcon className="searchbar-icon" aria-hidden="true" />
</button>
</div>
</form>
</div>
);
};
function SearchIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
{...props}
>
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0" />
</svg>
);
}
export default Searchbar;

View File

@@ -0,0 +1,51 @@
import React from 'react';
interface SourceFooterProps {
source: string;
}
const SourceFooter: React.FC<SourceFooterProps> = ({ source }) => {
return (
<>
<div className="footer-divider"></div>
<div className="footer-wrapper">
<h4 className="footer-title">Source</h4>
<div>
<a href={source} className="footer-link-wrapper">
{source}
<BoxArrowUpRightIcon
className="footer-link-icon"
aria-hidden="true"
/>
</a>
</div>
</div>
</>
);
};
function BoxArrowUpRightIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
{...props}
>
<path
fillRule="evenodd"
d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"
/>
<path
fillRule="evenodd"
d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"
/>
</svg>
);
}
export default SourceFooter;

View File

@@ -0,0 +1,66 @@
import React from 'react';
import { Meaning, WordData } from '../lib/datatypes';
interface WordDefinitionsProps {
wordData: WordData;
}
const WordDefinitions: React.FC<WordDefinitionsProps> = ({ wordData }) => {
return (
<div className="word-definitions-wrapper">
{wordData.meanings.map((meaning: any, index: number) => (
<WordDefinitionBlock key={index} data={meaning} />
))}
</div>
);
};
interface WordDefinitionBlockProps {
data: Meaning;
}
const WordDefinitionBlock: React.FC<WordDefinitionBlockProps> = ({ data }) => {
const { partOfSpeech, definitions, synonyms } = data;
return (
<div className="word-block-wrapper">
<div className="word-block-category-container">
<h2 className="word-block-category-name">{partOfSpeech}</h2>
<div className="word-block-category-divider"></div>
</div>
<div className="word-block-meaning-container">
<h3 className="word-block-meaning-title">Meaning</h3>
<ul className="word-block-meaning-list">
{definitions.map((definition: any, index: number) => (
<li key={index} className="word-block-meaning-list-item">
{definition.definition}{' '}
{definition.example && (
<span className="word-block-meaning-list-item-example">
"{definition.example}"
</span>
)}
</li>
))}
</ul>
</div>
{synonyms.length > 0 && (
<div className="word-block-synonyms-container">
<h3 className="word-block-synonyms-title">Synonyms</h3>
<ul className="word-block-synonyms-list">
{synonyms.map((synonym: string, index: number) => (
<li key={index} className="word-block-synonyms-list-item">
{synonym}
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default WordDefinitions;

View File

@@ -0,0 +1,74 @@
import React, { useState, useEffect, useRef } from 'react';
import { PlayIcon, HeartIcon, HeartFilled } from './Icons';
import { Phonetic } from '../lib/datatypes';
interface WordHeroProps {
data: {
word: string;
phonetics: Phonetic[];
};
}
const WordHero: React.FC<WordHeroProps> = ({ data }) => {
const { word, phonetics } = data;
const [isFavorite, setIsFavorite] = useState(false);
const americanEnglishPhonetic = phonetics.find((phonetic) => phonetic.audio?.includes('-us'));
const finalPhonetic = americanEnglishPhonetic || phonetics[0];
const audioRef = useRef<HTMLAudioElement | null>(null);
const playAudio = () => {
if (audioRef.current) {
audioRef.current.play();
}
};
const toggleFavorite = () => {
const favorites = JSON.parse(localStorage.getItem('favoriteWords') || '[]');
if (favorites.includes(word)) {
const filteredFavorites = favorites.filter((item: string) => item !== word);
localStorage.setItem('favoriteWords', JSON.stringify(filteredFavorites));
} else {
favorites.push(word);
localStorage.setItem('favoriteWords', JSON.stringify(favorites));
}
setIsFavorite(!isFavorite);
};
useEffect(() => {
const favoriteWords = JSON.parse(localStorage.getItem('favoriteWords') || '[]');
setIsFavorite(favoriteWords.includes(word));
}, [word]);
return (
<div className="wordhero-wrapper">
<div className="wordhero-word-container">
<button className="heart-icon-btn" onClick={toggleFavorite}>
{isFavorite ? (
<HeartFilled className="heart-icon-filled" aria-hidden="true" />
) : (
<HeartIcon className="heart-icon-unfilled" aria-hidden="true" />
)}
</button>
<h1 className="wordhero-word">{word}</h1>
{finalPhonetic?.text && <p className="wordhero-word-phonetic">{finalPhonetic.text}</p>}
</div>
{finalPhonetic?.audio && (
<div>
<button onClick={playAudio} className="wordhero-play-btn">
<PlayIcon className="wordhero-play-icon" aria-hidden="true" />
</button>
<audio ref={audioRef} src={finalPhonetic.audio}></audio>
</div>
)}
</div>
);
};
export default WordHero;

View File

@@ -0,0 +1,430 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
*,
html,
body {
margin: 0;
padding: 0;
color: #171717;
}
button {
border: none;
}
a {
text-decoration: none;
}
.screen-reader-text {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.heart-icon-btn {
background-color: transparent;
margin-right: auto;
cursor: pointer;
}
.heart-icon-unfilled {
width: 2rem; /* 32px */
height: 2rem; /* 32px */
stroke: #9333ea;
background-color: transparent;
}
.heart-icon-filled {
width: 2rem; /* 32px */
height: 2rem; /* 32px */
fill: #9333ea;
background-color: transparent;
}
/* ===== App Component ===== */
.app-wrapper {
margin: 0 auto;
max-width: 42rem; /* 672px */
padding: 3.5rem 1.5rem; /* 56px 24px */
}
/* ===== Header Component ===== */
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-logo-wrapper {
display: flex;
align-items: center;
}
.header-logo-img {
height: 2.5rem; /* 40px */
}
.header-logo-text {
font-family: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
font-size: 1.875rem; /* 30px */
line-height: 2.25rem; /* 36px */
font-weight: 600;
margin-left: 1rem; /* 16px */
color: #262626;
}
.header-select {
border-radius: 0.375rem; /* 6px */
border-width: 0px;
padding: 0.625rem 0.5rem; /* 10px 8px */
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
outline: 2px solid transparent;
outline-offset: 2px;
color: #262626;
}
.header-select:focus {
outline-color: #9333ea;
}
.header-link-text {
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
text-decoration: none;
color: #262626;
}
.header-link-text:hover {
color: #9333ea;
}
/* ===== Searchbar Component ===== */
.searchbar-wrapper {
margin-top: 4rem; /* 64px */
}
.searchbar-container {
position: relative;
margin-top: 0.5rem; /* 8px */
border-radius: 0.75rem; /* 12px */
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.searchbar-input {
display: block;
width: 100%;
border-radius: 0.75rem; /* 12px */
border-width: 0px;
padding: 1.25rem 0 1.25rem 1.5rem; /* 20px 0 20px 24px */
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
font-weight: 600;
outline-style: solid;
outline: 2px solid transparent;
outline-color: #f5f5f5;
color: #171717;
background-color: #f5f5f5;
}
.searchbar-input:focus {
outline-color: #9333ea;
}
.searchbar-input::placeholder {
color: #737373;
}
.searchbar-magnifying-glass {
position: absolute;
top: 0px;
bottom: 0px;
right: 0px;
display: flex;
align-items: center;
border-top-right-radius: 0.75rem; /* 12px */
border-bottom-right-radius: 0.75rem; /* 12px */
padding-left: 1rem; /* 16px */
padding-right: 1rem; /* 16px */
outline: 2px solid transparent;
outline-offset: 2px;
background-color: transparent;
}
.searchbar-icon {
width: 1.5rem; /* 24px */
height: 1.5rem; /* 24px */
fill: #6b21a8;
}
/* ===== WordHero Component ===== */
.wordhero-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4rem; /* 64px */
}
.wordhero-word-container {
display: flex;
flex-direction: column;
}
.wordhero-word {
font-size: 3.75rem; /* 60px */
line-height: 1;
font-weight: 600;
color: #262626;
}
.wordhero-word-phonetic {
font-size: 1.5rem; /* 24px */
line-height: 2rem; /* 32px */
margin-top: 0.75rem; /* 12px */
margin-bottom: 0.75rem; /* 12px */
color: #9333ea;
}
.wordhero-play-btn {
display: flex;
align-items: center;
border-radius: 9999px;
padding: 1.25rem; /* 20px */
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
color: white;
background-color: #e9d5ff;
}
.wordhero-play-btn:hover {
background-color: #d8b4fe;
}
.wordhero-play-btn:focus {
outline: 2px solid transparent;
outline-color: #9333ea;
}
.wordhero-play-icon {
height: 2.5rem; /* 40px */
width: 2.5rem; /* 40px */
fill: #9333ea;
}
/* ===== WordDefinitions Component ===== */
.word-definitions-wrapper {
display: flex;
flex-direction: column;
gap: 2rem; /* 32px */
margin-top: 4rem; /* 64px */
}
.word-block-wrapper {
display: flex;
flex-direction: column;
gap: 3.5rem; /* 56px */
}
.word-block-category-container {
display: flex;
align-items: center;
gap: 0.5rem; /* 8px */
}
.word-block-category-name {
padding-right: 0.75rem; /* 12px */
font-size: 1.5rem; /* 24px */
line-height: 2rem; /* 32px */
font-weight: 600;
color: #262626;
}
.word-block-category-divider {
height: 1px;
width: 100%;
background-color: #d4d4d4;
}
.word-block-meaning-container {
display: flex;
flex-direction: column;
row-gap: 1rem; /* 16px */
}
.word-block-meaning-title {
font-size: 1.25rem; /* 20px */
line-height: 1.75rem; /* 28px */
font-weight: 400;
color: #737373;
}
.word-block-meaning-list {
display: flex;
flex-direction: column;
padding-left: 1rem; /* 16px */
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
row-gap: 2rem; /* 32px */
color: #262626;
}
.word-block-meaning-list-item::marker {
padding-left: 2.5rem; /* 40px */
color: #9333ea;
}
.word-block-meaning-list-item-example {
display: block;
margin-top: 0.5rem; /* 8px */
color: #9333ea;
}
.word-block-synonyms-container {
display: flex;
align-items: baseline;
column-gap: 2rem; /* 32px */
}
.word-block-synonyms-title {
font-size: 1.25rem; /* 20px */
line-height: 1.75rem; /* 28px */
font-weight: 400;
color: #737373;
}
.word-block-synonyms-list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem; /* 8px */
}
.word-block-synonyms-list-item {
list-style: none;
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
font-weight: 600;
color: #9333ea;
}
/* ===== SourceFooter Component ===== */
.footer-divider {
margin-top: 2.5rem; /* 40px */
margin-bottom: 2.5rem; /* 40px */
height: 1px;
background-color: #e5e5e5;
}
.footer-wrapper {
display: flex;
align-items: baseline;
gap: 1rem; /* 16px */
font-size: 0.875rem; /* 14px */
line-height: 1.25rem; /* 20px */
}
.footer-title {
font-weight: 400;
color: #737373;
}
.footer-link-wrapper {
display: flex;
text-decoration: underline;
color: #262626;
}
.footer-link-icon {
margin-left: 0.5rem; /* 8px */
height: 1rem; /* 16px */
width: 1rem; /* 16px */
fill: #262626;
}
/* ===== Favorites Page ===== */
.favorites-list {
margin-top: 1rem; /* 16px */
border-top-width: 2px;
border-bottom-width: 0px;
border-color: rgb(229 229 229);
}
.favorites-text {
margin-top: 4rem; /* 64px */
font-size: 1.125rem; /* 18px */
line-height: 1.75rem; /* 28px */
color: #525252;
}
.favorites-item {
list-style: none;
padding: 0.5rem 0; /* Add some padding to each item */
}
.favorites-item.with-divider {
border-top: 1px solid #e5e5e5; /* Add divider */
}
/* ===== Favorite Word ===== */
.fave-word-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2.5rem 0; /* 40px 0 */
}
.fave-word-link {
background-color: #e9d5ff;
padding: 0.5rem 0.875rem; /* 8px 14px */
border-radius: 0.375rem; /* 6px */
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.fave-word-link:hover {
background-color: #d8b4fe;
}
.fave-word-link:focus {
outline: 2px solid #9333ea;
}
.fave-word-link p {
font-size: 1.25rem; /* 20px */
line-height: 1.75rem; /* 28px */
font-weight: 500;
color: #262626;
}
.fave-icon-btn {
background-color: transparent;
cursor: pointer;
}
/* ===== Breakpoints ===== */
@media (min-width: 640px) {
.searchbar-input {
line-height: 1.5rem; /* 24px */
}
}
@media (min-width: 768px) {
.word-block-meaning-list {
padding-left: 2.5rem; /* 40px */
}
}
@media (min-width: 1024px) {
.app-wrapper {
padding: 3.5rem 0.5rem; /* 56px 8px */
}
}

View File

@@ -0,0 +1,5 @@
export const SERIF_FONTS =
'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif';
export const SANS_SERIF_FONTS =
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';

View File

@@ -0,0 +1,22 @@
export interface Phonetic {
text?: string;
audio?: string;
}
export interface Definition {
definition: string;
example?: string;
}
export interface Meaning {
partOfSpeech: string;
definitions: Definition[];
synonyms: string[];
}
export interface WordData {
word: string;
phonetics: Phonetic[];
meanings: Meaning[];
sourceUrls: string[];
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import Root from './pages/root.tsx';
import Favorites from './pages/favorites.tsx';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
},
{
path: '/favorites',
element: <Favorites />,
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);

View File

@@ -0,0 +1,46 @@
import { useState, useEffect } from 'react';
import Header from '../components/Header';
import FavoriteWord from '../components/FavoriteWord';
import { SERIF_FONTS } from '../lib/constants';
function Favorites() {
const [font, setFont] = useState<string>(SERIF_FONTS);
const [favoriteWords, setFavoriteWords] = useState<string[]>([]);
useEffect(() => {
const loadedFavorites = JSON.parse(
localStorage.getItem('favoriteWords') || '[]'
);
setFavoriteWords(loadedFavorites);
}, []);
return (
<div
className="app-wrapper"
style={{
fontFamily: font,
}}
>
<Header font={font} setFont={setFont} />
{favoriteWords.length > 0 ? (
<ul className="favorites-list">
{favoriteWords.map((word, index) => (
<li
className={`favorites-item ${index > 0 ? 'with-divider' : ''}`}
key={index}
>
<FavoriteWord word={word} />
</li>
))}
</ul>
) : (
<p className="favorites-text">No favorite words added yet.</p>
)}
</div>
);
}
export default Favorites;

View File

@@ -0,0 +1,66 @@
import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { WordData } from '../lib/datatypes';
import Header from '../components/Header';
import Searchbar from '../components/Searchbar';
import WordHero from '../components/WordHero';
import WordDefinitions from '../components/WordDefinitions';
import SourceFooter from '../components/SourceFooter';
import { SERIF_FONTS } from '../lib/constants';
function Root() {
const [params] = useSearchParams();
const [font, setFont] = useState<string>(SERIF_FONTS);
const [word, setWord] = useState<string>(params.get('word') || 'course'); // /?word=WERT
const [wordData, setWordData] = useState<WordData | null>(null);
const fetchData = async () => {
try {
const url = `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`;
const response = await fetch(url);
const data = await response.json();
setWordData(data[0]);
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
console.error(error);
}
};
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
fetchData();
};
useEffect(() => {
fetchData();
}, []);
return (
<div
className="app-wrapper"
style={{
fontFamily: font,
}}>
<Header font={font} setFont={setFont} />
<Searchbar word={word} setWord={setWord} handleSubmit={handleSubmit} />
{wordData && (
<div>
<WordHero data={wordData} />
<WordDefinitions wordData={wordData} />
<SourceFooter source={wordData.sourceUrls[0]} />
</div>
)}
</div>
);
}
export default Root;

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />