diff --git a/.env.example b/.env.example index d13ac74..4c3c29d 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,12 @@ +// LLM ZHIPU_API_KEY= -AUTH_SECRET= +ZHIPU_MODEL_NAME= +// Auth +AUTH_SECRET= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= - NEXTAUTH_URL= -DATABASE_URL= \ No newline at end of file + +// Database +DATABASE_URL= diff --git a/Dockerfile b/Dockerfile index 5e980d4..ffe3608 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ FROM node:23-alpine AS base # Install dependencies only when needed FROM base AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat +RUN apk add --no-cache libc6-compat openssl WORKDIR /app # Install dependencies based on the preferred package manager @@ -29,10 +29,17 @@ COPY . . # Uncomment the following line in case you want to disable telemetry during the build. ENV NEXT_TELEMETRY_DISABLED=1 +# RUN \ +# if [ -f yarn.lock ]; then yarn run build; \ +# elif [ -f package-lock.json ]; then npm run build; \ +# elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ +# else echo "Lockfile not found." && exit 1; \ +# fi + +RUN pnpx prisma generate + RUN \ - if [ -f yarn.lock ]; then yarn run build; \ - elif [ -f package-lock.json ]; then npm run build; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ + if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ else echo "Lockfile not found." && exit 1; \ fi diff --git a/package.json b/package.json index 63fff30..5d37634 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "next-intl": "^4.5.2", "react": "19.1.0", "react-dom": "19.1.0", + "sonner": "^2.0.7", "unstorage": "^1.17.2", "zod": "^3.25.76" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38206b0..9ad489b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: react-dom: specifier: 19.1.0 version: 19.1.0(react@19.1.0) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) unstorage: specifier: ^1.17.2 version: 1.17.2 @@ -4040,6 +4043,12 @@ packages: resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} engines: {node: '>=8.0.0'} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -9954,6 +9963,11 @@ snapshots: slugify@1.6.6: optional: true + sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + source-map-js@1.2.1: {} source-map-support@0.5.21: diff --git a/src/app/(features)/memorize/FolderSelector.tsx b/src/app/(features)/memorize/FolderSelector.tsx index 23ced58..ee93daf 100644 --- a/src/app/(features)/memorize/FolderSelector.tsx +++ b/src/app/(features)/memorize/FolderSelector.tsx @@ -36,7 +36,7 @@ const FolderSelector: React.FC = ({ folders }) => {
- {folder.name} + {folder.id}. {folder.name} ({folder.total_pairs})
diff --git a/src/app/(features)/memorize/page.tsx b/src/app/(features)/memorize/page.tsx index b70541b..5e5d124 100644 --- a/src/app/(features)/memorize/page.tsx +++ b/src/app/(features)/memorize/page.tsx @@ -3,7 +3,6 @@ import { redirect } from "next/navigation"; import { getServerSession } from "next-auth"; import { - getFoldersByOwner, getFoldersWithTotalPairsByOwner, getOwnerByFolderId, } from "@/lib/services/folderService"; diff --git a/src/app/(features)/translator/AddToFolder.tsx b/src/app/(features)/translator/AddToFolder.tsx new file mode 100644 index 0000000..be085e5 --- /dev/null +++ b/src/app/(features)/translator/AddToFolder.tsx @@ -0,0 +1,78 @@ +import LightButton from "@/components/buttons/LightButton"; +import Container from "@/components/cards/Container"; +import { TranslationHistorySchema } from "@/lib/interfaces"; +import { useSession } from "next-auth/react"; +import { Dispatch, useEffect, useState } from "react"; +import z from "zod"; +import { folder } from "../../../../generated/prisma/browser"; +import { getFoldersByOwner } from "@/lib/services/folderService"; +import { Folder } from "lucide-react"; +import { createTextPair } from "@/lib/services/textPairService"; +import { toast } from "sonner"; + +interface AddToFolderProps { + item: z.infer; + setShow: Dispatch>; +} + +const AddToFolder: React.FC = ({ item, setShow }) => { + const session = useSession(); + const [folders, setFolders] = useState([]); + + useEffect(() => { + const username = session.data!.user!.name as string; + getFoldersByOwner(username).then(setFolders); + }, [session.data]); + + if (session.status !== "authenticated") { + return ( +
+ +
You are not authenticated
; +
+
+ ); + } + return ( +
+ +

Choose a Folder to Add to

+
+ {(folders.length > 0 && + folders.map((folder) => ( + + ))) ||
No folders found
} +
+ setShow(false)}>Close +
+
+ ); +}; + +export default AddToFolder; diff --git a/src/app/(features)/translator/page.tsx b/src/app/(features)/translator/page.tsx index bb90ecd..069eee4 100644 --- a/src/app/(features)/translator/page.tsx +++ b/src/app/(features)/translator/page.tsx @@ -1,7 +1,6 @@ "use client"; import LightButton from "@/components/buttons/LightButton"; -import Container from "@/components/cards/Container"; import IconClick from "@/components/IconClick"; import IMAGES from "@/config/images"; import { VOICES } from "@/config/locales"; @@ -9,10 +8,12 @@ import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { TranslationHistorySchema } from "@/lib/interfaces"; import { tlsoPush, tlso } from "@/lib/localStorageOperators"; import { getTTSAudioUrl } from "@/lib/tts"; -import { letsFetch } from "@/lib/utils"; +import { letsFetch, shallowEqual } from "@/lib/utils"; +import { Plus, Trash } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRef, useState } from "react"; import z from "zod"; +import AddToFolder from "./AddToFolder"; export default function TranslatorPage() { const t = useTranslations("translator"); @@ -27,6 +28,10 @@ export default function TranslatorPage() { const [history, setHistory] = useState< z.infer[] >(tlso.get()); + const [showAddToFolder, setShowAddToFolder] = useState(false); + const [addToFolderItem, setAddToFolderItem] = useState | null>(null); const lastTTS = useRef({ text: "", @@ -67,8 +72,7 @@ export default function TranslatorPage() { const checkUpdateLocalStorage = (item: typeof newItem) => { if (item.text1 && item.text2 && item.locale1 && item.locale2) { - tlsoPush(item as z.infer); - setHistory(tlso.get()); + setHistory(tlsoPush(item as z.infer)); } }; const innerStates = { @@ -187,11 +191,11 @@ export default function TranslatorPage() {
{/* ICard2 Component */}
-
{tresult}
-
+
{tresult}
+
{ipaTexts[1]}
-
+
{history.length > 0 && ( - +

History

-
    +
    {history.map((item, index) => ( -
  • - {item.text1} - {item.text2} -
  • +
    +
    +
    +

    {item.text1}

    +

    {item.text2}

    +
    +
    + + +
    +
    +
    ))} -
- +
+ {showAddToFolder && ( + + )} +
)} ); diff --git a/src/app/(features)/word-board/TheBoard.tsx b/src/app/(features)/word-board/TheBoard.tsx deleted file mode 100644 index 1840363..0000000 --- a/src/app/(features)/word-board/TheBoard.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { - BOARD_WIDTH, - TEXT_WIDTH, - BOARD_HEIGHT, - TEXT_SIZE, -} from "@/config/word-board-config"; -import { Word } from "@/lib/interfaces"; -import { Dispatch, SetStateAction } from "react"; - -export default function TheBoard({ - words, - selectWord, -}: { - words: [ - { - word: string; - x: number; - y: number; - }, - ]; - setWords: Dispatch>; - selectWord: (word: string) => void; -}) { - function DraggableWord({ word }: { word: Word }) { - return ( - {word.word})) - onClick={() => { - selectWord(word.word); - }} - > - {word.word} - - ); - } - return ( -
- {words.map( - ( - v: { - word: string; - x: number; - y: number; - }, - i: number, - ) => { - return ; - }, - )} -
- ); -} diff --git a/src/app/(features)/word-board/page.tsx b/src/app/(features)/word-board/page.tsx deleted file mode 100644 index df40cda..0000000 --- a/src/app/(features)/word-board/page.tsx +++ /dev/null @@ -1,185 +0,0 @@ -"use client"; -import TheBoard from "@/app/(features)/word-board/TheBoard"; -import LightButton from "../../../components/buttons/LightButton"; -import { KeyboardEvent, useRef, useState } from "react"; -import { Word } from "@/lib/interfaces"; -import { - BOARD_WIDTH, - TEXT_WIDTH, - BOARD_HEIGHT, - TEXT_SIZE, -} from "@/config/word-board-config"; -import { inspect } from "@/lib/utils"; - -export default function WordBoardPage() { - 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) => { - const isOK = (w: Word) => { - if (words.length === 0) return true; - const tf = (ww: Word) => - ({ - word: ww.word, - x: Math.floor(ww.x * (BOARD_WIDTH - TEXT_WIDTH * ww.word.length)), - y: Math.floor(ww.y * (BOARD_HEIGHT - TEXT_SIZE)), - }) as Word; - const tfd_words = words.map(tf); - const tfd_w = tf(w); - for (const www of tfd_words) { - const p1 = { - x: (www.x + www.x + TEXT_WIDTH * www.word.length) / 2, - y: (www.y + www.y + TEXT_SIZE) / 2, - }; - const p2 = { - x: (tfd_w.x + tfd_w.x + TEXT_WIDTH * tfd_w.word.length) / 2, - y: (tfd_w.y + tfd_w.y + TEXT_SIZE) / 2, - }; - if ( - Math.abs(p1.x - p2.x) < - (TEXT_WIDTH * (www.word.length + tfd_w.word.length)) / 2 && - Math.abs(p1.y - p2.y) < TEXT_SIZE - ) { - return false; - } - } - return true; - }; - let new_word; - let count = 0; - do { - new_word = { - word: word, - x: Math.random(), - y: Math.random(), - }; - if (++count > 1000) return null; - } while (!isOK(new_word)); - return new_word as Word; - }; - const insertWord = () => { - if (!inputRef.current) return; - const word = inputRef.current.value.trim(); - if (word === "") return; - const new_word = generateNewWord(word); - if (!new_word) return; - setWords([...words, new_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]); - } - }; - const deleteAll = () => { - setWords([] as Array); - }; - const handleKeyDown = (e: KeyboardEvent) => { - // e.preventDefault(); - if (e.key === "Enter") { - insertWord(); - } - }; - const selectWord = (word: string) => { - if (!inputRef.current) return; - inputRef.current.value = word; - }; - const searchWord = () => { - if (!inputRef.current) return; - const word = inputRef.current.value.trim(); - if (word === "") return; - inspect(word)(); - inputRef.current.value = ""; - }; - // const readWordAloud = () => { - // playFromUrl('https://fanyi.baidu.com/gettts?lan=uk&text=disclose&spd=3') - // return; - // if (!inputRef.current) return; - // const word = inputRef.current.value.trim(); - // if (word === '') return; - // inspect(word)(); - // inputRef.current.value = ''; - // } - return ( - <> -
-
- -
- - 插入 - 删除 - 搜索 - 导入 - 导出 - 删光 - {/* */} -
- -
-
- - ); -} diff --git a/src/app/folders/FoldersClient.tsx b/src/app/folders/FoldersClient.tsx index 0c5e9ba..301af7d 100644 --- a/src/app/folders/FoldersClient.tsx +++ b/src/app/folders/FoldersClient.tsx @@ -1,11 +1,6 @@ "use client"; -import { - ChevronRight, - Folder, - FolderPlus, - Trash2, -} from "lucide-react"; +import { ChevronRight, Folder, FolderPlus, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; import { Center } from "@/components/Center"; import { useRouter } from "next/navigation"; @@ -13,11 +8,11 @@ import { folder } from "../../../generated/prisma/browser"; import { createFolder, deleteFolderById, - getFoldersByOwner, + getFoldersWithTotalPairsByOwner, } from "@/lib/services/folderService"; interface FolderProps { - folder: folder; + folder: folder & { total_pairs: number }; deleteCallback: () => void; openCallback: () => void; } @@ -34,14 +29,16 @@ const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => {
-

{folder.name}

+

+ {folder.id}. {folder.name} ({folder.total_pairs}) +

{/*

{} items

*/}
#{folder.id}
-
+