diff --git a/deploy.sh b/deploy.sh
new file mode 100644
index 0000000..417f684
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,208 @@
+#!/bin/bash
+
+# Next.js 项目部署脚本
+set -e # 遇到任何错误立即退出
+
+# ===== 配置区域 =====
+SERVER_IP="43.156.84.214"
+SERVER_USER="ubuntu"
+PROJECT_NAME="learn-languages"
+LOCAL_PROJECT_DIR="/home/goddonebianu/Code/learn-languages"
+REMOTE_PROJECT_DIR="/home/$SERVER_USER/$PROJECT_NAME"
+BRANCH="main" # 要部署的分支
+NODE_ENV="production"
+PORT="3000" # 应用运行端口
+# ===================
+
+# 颜色输出函数
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+log_info() {
+ echo -e "${GREEN}[INFO]${NC} $1"
+}
+
+log_warn() {
+ echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# 检查本地更改
+check_local_changes() {
+ log_info "检查本地更改..."
+ if ! git diff --quiet; then
+ log_warn "发现未提交的更改,建议先提交更改"
+ read -p "继续部署?(y/N): " -n 1 -r
+ echo
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ exit 1
+ fi
+ fi
+}
+
+# 构建项目
+build_project() {
+ log_info "开始构建项目..."
+ cd "$LOCAL_PROJECT_DIR"
+
+ # 安装依赖
+ if ! npm ci --only=production; then
+ log_error "npm install 失败"
+ exit 1
+ fi
+
+ # 构建项目
+ if ! npm run build; then
+ log_error "构建失败"
+ exit 1
+ fi
+
+ log_info "项目构建成功"
+}
+
+# 创建部署包
+create_deployment_package() {
+ log_info "创建部署包..."
+ local temp_dir=$(mktemp -d)
+ local package_name="${PROJECT_NAME}-$(date +%Y%m%d-%H%M%S).tar.gz"
+
+ # 复制需要的文件
+ cp -r package.json package-lock.json next.config.js* .next public "$temp_dir/"
+
+ # 如果有环境文件也复制
+ if [ -f .env.production ]; then
+ cp .env.production "$temp_dir/"
+ fi
+
+ # 创建压缩包
+ cd "$temp_dir"
+ tar -czf "/tmp/$package_name" .
+
+ echo "$package_name"
+}
+
+# 部署到服务器
+deploy_to_server() {
+ local package_name=$1
+
+ log_info "开始部署到服务器..."
+
+ # 上传部署包
+ if ! scp "/tmp/$package_name" $SERVER_USER@$SERVER_IP:/tmp/; then
+ log_error "文件上传失败"
+ exit 1
+ fi
+
+ # 执行远程部署命令
+ ssh $SERVER_USER@$SERVER_IP << EOF
+ set -e
+
+ echo "在服务器上执行部署..."
+
+ # 创建备份目录
+ BACKUP_DIR="$REMOTE_PROJECT_DIR-backup-\$(date +%Y%m%d-%H%M%S)"
+ if [ -d "$REMOTE_PROJECT_DIR" ]; then
+ mkdir -p "\$BACKUP_DIR"
+ cp -r "$REMOTE_PROJECT_DIR"/* "\$BACKUP_DIR/" || true
+ fi
+
+ # 创建项目目录
+ mkdir -p "$REMOTE_PROJECT_DIR"
+
+ # 解压新版本
+ tar -xzf "/tmp/$package_name" -C "$REMOTE_PROJECT_DIR"
+
+ # 清理临时文件
+ rm -f "/tmp/$package_name"
+
+ # 安装生产依赖
+ cd "$REMOTE_PROJECT_DIR"
+ npm ci --only=production
+
+ echo "部署文件准备完成"
+EOF
+
+ log_info "文件部署完成"
+}
+
+# 重启服务
+restart_service() {
+ log_info "重启服务..."
+
+ ssh $SERVER_USER@$SERVER_IP << EOF
+ set -e
+
+ cd "$REMOTE_PROJECT_DIR"
+
+ # 使用 PM2 重启应用
+ if command -v pm2 &> /dev/null; then
+ # 如果应用已经在运行,重新加载
+ if pm2 list | grep -q "$PROJECT_NAME"; then
+ pm2 reload $PROJECT_NAME --update-env
+ else
+ # 第一次启动
+ export PORT=$PORT
+ pm2 start npm --name "$PROJECT_NAME" -- start
+ pm2 save
+ pm2 startup 2>/dev/null || true
+ fi
+ else
+ # 如果没有 PM2,直接启动(不推荐生产环境)
+ echo "警告: 未找到 PM2,直接启动进程"
+ export PORT=$PORT
+ npm start &
+ fi
+
+ echo "等待服务启动..."
+ sleep 5
+
+ # 检查服务是否正常运行
+ if curl -f http://localhost:$PORT > /dev/null 2>&1; then
+ echo "服务启动成功!"
+ else
+ echo "服务启动可能有问题,请检查日志"
+ exit 1
+ fi
+EOF
+
+ log_info "服务重启完成"
+}
+
+# 清理工作
+cleanup() {
+ log_info "清理临时文件..."
+ rm -f "/tmp/${PROJECT_NAME}-*.tar.gz" 2>/dev/null || true
+}
+
+# 主部署流程
+main() {
+ log_info "开始部署 $PROJECT_NAME 到 $SERVER_IP"
+
+ # 检查是否在项目目录
+ if [ ! -f "$LOCAL_PROJECT_DIR/package.json" ]; then
+ log_error "请在正确的 Next.js 项目目录中运行"
+ exit 1
+ fi
+
+ check_local_changes
+ build_project
+ local package_name=$(create_deployment_package)
+ deploy_to_server "$package_name"
+ restart_service
+ cleanup
+
+ log_info "${GREEN}部署成功完成!${NC}"
+ log_info "应用地址: http://$SERVER_IP:$PORT"
+}
+
+# 错误处理
+trap 'log_error "部署过程中断"; cleanup; exit 1' INT TERM
+trap 'cleanup' EXIT
+
+# 执行主函数
+main "$@"
\ No newline at end of file
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
deleted file mode 100644
index 718d6fe..0000000
Binary files a/src/app/favicon.ico and /dev/null differ
diff --git a/src/app/page.tsx b/src/app/page.tsx
index a932894..15fbff5 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,103 +1,16 @@
-import Image from "next/image";
+function Link(
+ {href, label}: {href: string, label: string}
+) {
+ return (
+ {label}
+ )
+}
export default function Home() {
return (
-
-
-
-
- -
- Get started by editing{" "}
-
- src/app/page.tsx
-
- .
-
- -
- Save and see your changes instantly.
-
-
-
-
-
-
+
+
+
);
}
diff --git a/src/app/srt-player/components/AppCard.tsx b/src/app/srt-player/components/AppCard.tsx
new file mode 100644
index 0000000..5e58f72
--- /dev/null
+++ b/src/app/srt-player/components/AppCard.tsx
@@ -0,0 +1,25 @@
+'use client'
+
+import { useRef, useState } from "react";
+import UploadArea from "./UploadArea";
+import VideoPanel from "./VideoPlayer/VideoPanel";
+
+export default function AppCard() {
+ const videoRef = useRef
(null);
+
+ const [videoUrl, setVideoUrl] = useState(null);
+ const [srtUrl, setSrtUrl] = useState(null);
+
+ return (
+
+ );
+}
diff --git a/src/app/srt-player/components/Button.tsx b/src/app/srt-player/components/Button.tsx
new file mode 100644
index 0000000..74f88fb
--- /dev/null
+++ b/src/app/srt-player/components/Button.tsx
@@ -0,0 +1,7 @@
+export default function Button({ label, onClick }: { label: string, onClick?: () => void }) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/srt-player/components/UploadArea.tsx b/src/app/srt-player/components/UploadArea.tsx
new file mode 100644
index 0000000..530f299
--- /dev/null
+++ b/src/app/srt-player/components/UploadArea.tsx
@@ -0,0 +1,75 @@
+import { useRef, useState } from "react";
+import Button from "./Button";
+
+export default function UploadArea(
+ {
+ setVideoUrl,
+ setSrtUrl
+ }: {
+ setVideoUrl: (url: string | null) => void;
+ setSrtUrl: (url: string | null) => void;
+ }
+) {
+ const inputRef = useRef(null);
+
+ const [videoFile, setVideoFile] = useState(null);
+ const [SrtFile, setSrtFile] = useState(null);
+
+ const uploadVideo = () => {
+ const input = inputRef.current;
+ if (input) {
+ input.setAttribute('accept', 'video/*');
+ input.click();
+ input.onchange = () => {
+ const file = input.files?.[0];
+ if (file) {
+ setVideoFile(file);
+ setVideoUrl(URL.createObjectURL(file));
+ }
+ };
+ }
+ }
+ const uploadSRT = () => {
+ const input = inputRef.current;
+ if (input) {
+ input.setAttribute('accept', '.srt');
+ input.click();
+ input.onchange = () => {
+ const file = input.files?.[0];
+ if (file) {
+ setSrtFile(file);
+ setSrtUrl(URL.createObjectURL(file));
+ }
+ };
+ }
+ }
+ return (
+
+
+
+
+ | File Name |
+ Type |
+ Size |
+ Actions |
+
+
+
+
+ | {videoFile?.name} |
+ Video |
+ {videoFile ? (videoFile.size / 1024 / 1024).toFixed(2) + 'MB' : null} |
+ |
+
+
+ | {SrtFile?.name} |
+ SRT |
+ {SrtFile ? (SrtFile.size / 1024 / 1024).toFixed(2) + 'MB' : null} |
+ |
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/srt-player/components/VideoPlayer/SubtitleDisplay.tsx b/src/app/srt-player/components/VideoPlayer/SubtitleDisplay.tsx
new file mode 100644
index 0000000..3714ee6
--- /dev/null
+++ b/src/app/srt-player/components/VideoPlayer/SubtitleDisplay.tsx
@@ -0,0 +1,28 @@
+export default function SubtitleDisplay({ subtitle }: { subtitle: string }) {
+ const words = subtitle.match(/\b[\w']+(?:-[\w']+)*\b/g) || [];
+ const goto = (url: string) => {
+ window.open(url, '_blank');
+ }
+ const inspect = (word: string) => {
+ return () => {
+ word = word.toLowerCase();
+ goto(`https://www.youdao.com/result?word=${word}&lang=en`);
+ }
+ }
+ let i = 0;
+ return (
+
+ {
+ words.map((v) => (
+
+ {v + ' '}
+
+ ))
+ }
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/srt-player/components/VideoPlayer/VideoPanel.tsx b/src/app/srt-player/components/VideoPlayer/VideoPanel.tsx
new file mode 100644
index 0000000..712b5b9
--- /dev/null
+++ b/src/app/srt-player/components/VideoPlayer/VideoPanel.tsx
@@ -0,0 +1,177 @@
+import { useState, useRef, forwardRef, useEffect, KeyboardEvent, useCallback } from "react";
+import Button from "../Button";
+import { getIndex, getNearistIndex, parseSrt } from "../../subtitle";
+import SubtitleDisplay from "./SubtitleDisplay";
+
+type VideoPanelProps = {
+ videoUrl: string | null;
+ srtUrl: string | null;
+};
+
+const VideoPanel = forwardRef((
+ { videoUrl, srtUrl }, videoRef
+) => {
+ videoRef = videoRef as React.RefObject;
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [srtLength, setSrtLength] = useState(0);
+ const [progress, setProgress] = useState(-1);
+ const [autoPause, setAutoPause] = useState(true);
+ const [spanText, setSpanText] = useState('');
+ const [subtitle, setSubtitle] = useState('');
+ const parsedSrtRef = useRef<{ start: number; end: number; text: string; }[] | null>(null);
+ const rafldRef = useRef(0);
+ const ready = useRef({
+ 'vid': false,
+ 'sub': false,
+ 'all': function () { return this.vid && this.sub }
+ });
+
+ const togglePlayPause = useCallback(() => {
+ if (!videoUrl) return;
+
+ const video = videoRef.current;
+ if (!video) return;
+ if (video.paused || video.currentTime === 0) {
+ video.play();
+ } else {
+ video.pause();
+ }
+ setIsPlaying(!video.paused);
+ }, [videoRef, videoUrl]);
+
+ useEffect(() => {
+ const cb = () => {
+ if (ready.current.all()) {
+ if (!parsedSrtRef.current) {
+ ;
+ } else if (isPlaying) {
+ // 这里负责显示当前时间的字幕与自动暂停
+ const srt = parsedSrtRef.current;
+ const ct = videoRef.current?.currentTime as number;
+ const index = getIndex(srt, ct);
+ if (index !== null) {
+ setSubtitle(srt[index].text)
+ if (autoPause && ct >= (srt[index].end - 0.05) && ct < srt[index].end) {
+ videoRef.current!.currentTime = srt[index].start;
+ togglePlayPause();
+ }
+ } else {
+ setSubtitle('');
+ }
+ } else {
+ ;
+ }
+ }
+ rafldRef.current = requestAnimationFrame(cb);
+ }
+ rafldRef.current = requestAnimationFrame(cb);
+ return () => {
+ cancelAnimationFrame(rafldRef.current);
+ }
+ }, [autoPause, isPlaying, togglePlayPause, videoRef]);
+
+ useEffect(() => {
+ if (videoUrl && videoRef.current) {
+ videoRef.current.src = videoUrl;
+ videoRef.current.load();
+ setIsPlaying(false);
+ ready.current['vid'] = true;
+ }
+ }, [videoRef, videoUrl]);
+ useEffect(() => {
+ if (srtUrl) {
+ fetch(srtUrl)
+ .then(response => response.text())
+ .then(data => {
+ parsedSrtRef.current = parseSrt(data);
+ setSrtLength(parsedSrtRef.current.length);
+ ready.current['sub'] = true;
+ });
+ }
+ }, [srtUrl]);
+
+ const timeUpdate = () => {
+ if (!parsedSrtRef.current || !videoRef.current) return;
+ const index = getIndex(parsedSrtRef.current, videoRef.current.currentTime);
+ if (!index) return;
+ setSpanText(`${index + 1}/${parsedSrtRef.current.length}`)
+ }
+
+ const handleSeek = (e: React.ChangeEvent) => {
+ if (videoRef.current && parsedSrtRef.current) {
+ const newProgress = parseInt(e.target.value);
+ videoRef.current.currentTime = parsedSrtRef.current[newProgress]?.start || 0;
+ setProgress(newProgress);
+ }
+ };
+
+ const handleAutoPauseToggle = () => {
+ setAutoPause(!autoPause);
+ };
+
+ const next = () => {
+ if (!parsedSrtRef.current || !videoRef.current) return;
+ const i = getNearistIndex(parsedSrtRef.current, videoRef.current.currentTime);
+ if (i != null && i + 1 < parsedSrtRef.current.length) {
+ videoRef.current.currentTime = parsedSrtRef.current[i + 1].start;
+ videoRef.current.play();
+ setIsPlaying(true);
+ }
+ }
+
+ const previous = () => {
+ if (!parsedSrtRef.current || !videoRef.current) return;
+ const i = getNearistIndex(parsedSrtRef.current, videoRef.current.currentTime);
+ if (i != null && i - 1 >= 0) {
+ videoRef.current.currentTime = parsedSrtRef.current[i - 1].start;
+ videoRef.current.play();
+ setIsPlaying(true);
+ }
+ }
+
+ const restart = () => {
+ if (!parsedSrtRef.current || !videoRef.current) return;
+ const i = getNearistIndex(parsedSrtRef.current, videoRef.current.currentTime);
+ if (i != null && i >= 0) {
+ videoRef.current.currentTime = parsedSrtRef.current[i].start;
+ videoRef.current.play();
+ setIsPlaying(true);
+ }
+ }
+
+ const handleKeyDownEvent = (e: KeyboardEvent) => {
+ if (e.key === 'n') {
+ next();
+ } else if (e.key === 'p') {
+ previous();
+ } else if (e.key === ' ') {
+ togglePlayPause();
+ } else if (e.key === 'r') {
+ restart();
+ } else if (e.key === 'a') {
+ ; handleAutoPauseToggle();
+ }
+ }
+
+ return (
+
+ );
+});
+
+VideoPanel.displayName = 'VideoPanel';
+
+export default VideoPanel;
\ No newline at end of file
diff --git a/src/app/srt-player/layout.tsx b/src/app/srt-player/layout.tsx
new file mode 100644
index 0000000..81acea9
--- /dev/null
+++ b/src/app/srt-player/layout.tsx
@@ -0,0 +1,34 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "../globals.css";
+
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+ title: "SRT Video Player",
+ description: "Practice spoken English",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/srt-player/page.tsx b/src/app/srt-player/page.tsx
new file mode 100644
index 0000000..34bca58
--- /dev/null
+++ b/src/app/srt-player/page.tsx
@@ -0,0 +1,9 @@
+import AppCard from "./components/AppCard";
+
+export default function Home() {
+ return (
+
+ );
+}
diff --git a/src/app/srt-player/subtitle.ts b/src/app/srt-player/subtitle.ts
new file mode 100644
index 0000000..00f3f25
--- /dev/null
+++ b/src/app/srt-player/subtitle.ts
@@ -0,0 +1,52 @@
+export function parseSrt(data: string) {
+ const lines = data.split(/\r?\n/);
+ const result = [];
+ const re = new RegExp('(\\d{2}:\\d{2}:\\d{2},\\d{3})\\s*-->\\s*(\\d{2}:\\d{2}:\\d{2},\\d{3})');
+ let i = 0;
+ while (i < lines.length) {
+ if (!lines[i].trim()) { i++; continue; }
+ i++;
+ if (i >= lines.length) break;
+ const timeMatch = lines[i].match(re);
+ if (!timeMatch) { i++; continue; }
+ const start = toSeconds(timeMatch[1]);
+ const end = toSeconds(timeMatch[2]);
+ i++;
+ let text = '';
+ while (i < lines.length && lines[i].trim()) {
+ text += lines[i] + '\n';
+ i++;
+ }
+ result.push({ start, end, text: text.trim() });
+ i++;
+ }
+ return result;
+}
+
+export function getNearistIndex(srt: { start: number; end: number; text: string; }[], ct: number) {
+ for (let i = 0; i < srt.length; i++) {
+ const s = srt[i];
+ const l = ct - s.start >= 0;
+ const r = ct - s.end >= 0;
+ if (!(l || r)) return i - 1;
+ if (l && (!r)) return i;
+ }
+}
+
+export function getIndex(srt: { start: number; end: number; text: string; }[], ct: number) {
+ for (let i = 0; i < srt.length; i++) {
+ if (ct >= srt[i].start && ct <= srt[i].end) {
+ return i;
+ }
+ }
+ return null;
+}
+
+export function getSubtitle(srt: { start: number; end: number; text: string; }[], currentTime: number) {
+ return srt.find(sub => currentTime >= sub.start && currentTime <= sub.end) || null;
+}
+
+function toSeconds(timeStr: string): number {
+ const [h, m, s] = timeStr.replace(',', '.').split(':');
+ return parseFloat((parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s)).toFixed(3));
+}
diff --git a/src/app/word-board/Button.tsx b/src/app/word-board/Button.tsx
new file mode 100644
index 0000000..74f88fb
--- /dev/null
+++ b/src/app/word-board/Button.tsx
@@ -0,0 +1,7 @@
+export default function Button({ label, onClick }: { label: string, onClick?: () => void }) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/word-board/WordBoard.tsx b/src/app/word-board/WordBoard.tsx
new file mode 100644
index 0000000..252558f
--- /dev/null
+++ b/src/app/word-board/WordBoard.tsx
@@ -0,0 +1,42 @@
+'use client';
+
+export default function WordBoard(
+ { words }: {
+ words: [
+ {
+ word: string,
+ x: number,
+ y: number
+ }
+ ]
+ }
+) {
+ const inspect = (word: string) => {
+ const goto = (url: string) => {
+ window.open(url, '_blank');
+ }
+ return () => {
+ word = word.toLowerCase();
+ goto(`https://www.youdao.com/result?word=${word}&lang=en`);
+ }
+ }
+ return (
+
+ {words.map(
+ (v: {
+ word: string,
+ x: number,
+ y: number
+ }, i: number) => {
+ return ({v.word})
+ })}
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/word-board/layout.tsx b/src/app/word-board/layout.tsx
new file mode 100644
index 0000000..76225f4
--- /dev/null
+++ b/src/app/word-board/layout.tsx
@@ -0,0 +1,34 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "../globals.css";
+
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+ title: "Word Board",
+ description: "Word board page",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/word-board/page.tsx b/src/app/word-board/page.tsx
new file mode 100644
index 0000000..c0773a8
--- /dev/null
+++ b/src/app/word-board/page.tsx
@@ -0,0 +1,94 @@
+'use client';
+import WordBoard from "@/app/word-board/WordBoard";
+import Button from "./Button";
+import { useRef, useState } from "react";
+
+interface Word {
+ word: string,
+ x: number,
+ y: number
+}
+
+export default function Home() {
+ const inputRef = useRef(null);
+ const inputFileRef = useRef(null);
+ const initialWords =
+ [
+ // 'apple',
+ // 'banana',
+ // 'cannon',
+ // 'desktop',
+ // 'kernel',
+ // 'system',
+ // 'programming',
+ // 'owe'
+ ] as Array;
+ const [words, setWords] = useState(
+ initialWords.map((v: string) => ({
+ 'word': v,
+ 'x': Math.random(),
+ 'y': Math.random()
+ }))
+ );
+ const generateNewWord = (word: string) => {
+ return {
+ word: word,
+ x: Math.random(),
+ y: Math.random()
+ } as Word;
+ }
+ const insertWord = () => {
+ if (!inputRef.current) return;
+ const word = inputRef.current.value.trim();
+ if (word === '') return;
+ setWords([...words, generateNewWord(word)]);
+ inputRef.current.value = '';
+ }
+ const deleteWord = () => {
+ if (!inputRef.current) return;
+ const word = inputRef.current.value.trim();
+ if (word === '') return;
+ setWords(words.filter((v) => v.word !== word));
+ inputRef.current.value = '';
+ };
+ const importWords = () => {
+ inputFileRef.current?.click();
+ }
+ const exportWords = () => {
+ const blob = new Blob([JSON.stringify(words)], {
+ type: 'application/json'
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${Date.now()}.json`;
+ a.style.display = 'none';
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+ const handleFileChange = () => {
+ const files = inputFileRef.current?.files;
+ if (files && files.length > 0) {
+ const reader = new FileReader();
+ reader.onload = () => {
+ if (reader.result && typeof reader.result === 'string')
+ setWords(JSON.parse(reader.result) as [Word]);
+ }
+ reader.readAsText(files[0]);
+ }
+ }
+ return (
+
+ );
+}