This commit is contained in:
258
src/app/(features)/alphabet/AlphabetCard.tsx
Normal file
258
src/app/(features)/alphabet/AlphabetCard.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||
import IconClick from "@/components/ui/buttons/IconClick";
|
||||
import IMAGES from "@/config/images";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
interface AlphabetCardProps {
|
||||
alphabet: Letter[];
|
||||
alphabetType: SupportedAlphabets;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export default function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardProps) {
|
||||
const t = useTranslations("alphabet");
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [showIPA, setShowIPA] = useState(true);
|
||||
const [showLetter, setShowLetter] = useState(true);
|
||||
const [showRoman, setShowRoman] = useState(false);
|
||||
const [isRandomMode, setIsRandomMode] = useState(false);
|
||||
|
||||
// 只有日语假名显示罗马音按钮
|
||||
const hasRomanization = alphabetType === "japanese";
|
||||
|
||||
const currentLetter = alphabet[currentIndex];
|
||||
|
||||
|
||||
const goToNext = useCallback(() => {
|
||||
if (isRandomMode) {
|
||||
setCurrentIndex(Math.floor(Math.random() * alphabet.length));
|
||||
} else {
|
||||
setCurrentIndex((prev) => (prev === alphabet.length - 1 ? 0 : prev + 1));
|
||||
}
|
||||
}, [alphabet.length, isRandomMode]);
|
||||
|
||||
const goToPrevious = useCallback(() => {
|
||||
if (isRandomMode) {
|
||||
setCurrentIndex(Math.floor(Math.random() * alphabet.length));
|
||||
} else {
|
||||
setCurrentIndex((prev) => (prev === 0 ? alphabet.length - 1 : prev - 1));
|
||||
}
|
||||
}, [alphabet.length, isRandomMode]);
|
||||
|
||||
const goToRandom = useCallback(() => {
|
||||
setCurrentIndex(Math.floor(Math.random() * alphabet.length));
|
||||
}, [alphabet.length]);
|
||||
|
||||
// 键盘快捷键支持
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: globalThis.KeyboardEvent) => {
|
||||
if (e.key === "ArrowLeft") {
|
||||
goToPrevious();
|
||||
} else if (e.key === "ArrowRight") {
|
||||
goToNext();
|
||||
} else if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
goToRandom();
|
||||
} else if (e.key === "Escape") {
|
||||
onBack();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [goToPrevious, goToNext, goToRandom, onBack]);
|
||||
|
||||
// 触摸滑动支持
|
||||
const [touchStart, setTouchStart] = useState<number | null>(null);
|
||||
const [touchEnd, setTouchEnd] = useState<number | null>(null);
|
||||
|
||||
const minSwipeDistance = 50;
|
||||
|
||||
const onTouchStart = (e: React.TouchEvent) => {
|
||||
setTouchEnd(null);
|
||||
setTouchStart(e.targetTouches[0].clientX);
|
||||
};
|
||||
|
||||
const onTouchMove = (e: React.TouchEvent) => {
|
||||
setTouchEnd(e.targetTouches[0].clientX);
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!touchStart || !touchEnd) return;
|
||||
|
||||
const distance = touchStart - touchEnd;
|
||||
const isLeftSwipe = distance > minSwipeDistance;
|
||||
const isRightSwipe = distance < -minSwipeDistance;
|
||||
|
||||
if (isLeftSwipe) {
|
||||
goToNext();
|
||||
}
|
||||
if (isRightSwipe) {
|
||||
goToPrevious();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8">
|
||||
<div className="w-full max-w-2xl">
|
||||
{/* 返回按钮 */}
|
||||
<div className="flex justify-end mb-4">
|
||||
<IconClick
|
||||
size={32}
|
||||
alt="close"
|
||||
src={IMAGES.close}
|
||||
onClick={onBack}
|
||||
className="bg-white rounded-full shadow-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 主卡片 */}
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 md:p-12">
|
||||
{/* 进度指示器 */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<span className="text-sm text-gray-500">
|
||||
{currentIndex + 1} / {alphabet.length}
|
||||
</span>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => setShowLetter(!showLetter)}
|
||||
className={`px-3 py-1 rounded-full text-sm transition-colors ${
|
||||
showLetter
|
||||
? "bg-[#35786f] text-white"
|
||||
: "bg-gray-200 text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{t("letter")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowIPA(!showIPA)}
|
||||
className={`px-3 py-1 rounded-full text-sm transition-colors ${
|
||||
showIPA
|
||||
? "bg-[#35786f] text-white"
|
||||
: "bg-gray-200 text-gray-600"
|
||||
}`}
|
||||
>
|
||||
IPA
|
||||
</button>
|
||||
{hasRomanization && (
|
||||
<button
|
||||
onClick={() => setShowRoman(!showRoman)}
|
||||
className={`px-3 py-1 rounded-full text-sm transition-colors ${
|
||||
showRoman
|
||||
? "bg-[#35786f] text-white"
|
||||
: "bg-gray-200 text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{t("roman")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsRandomMode(!isRandomMode)}
|
||||
className={`px-3 py-1 rounded-full text-sm transition-colors ${
|
||||
isRandomMode
|
||||
? "bg-[#35786f] text-white"
|
||||
: "bg-gray-200 text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{t("random")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 字母显示区域 */}
|
||||
<div className="text-center mb-8">
|
||||
{showLetter ? (
|
||||
<div className="text-6xl md:text-8xl font-bold text-gray-800 mb-4">
|
||||
{currentLetter.letter}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-6xl md:text-8xl font-bold text-gray-300 mb-4 h-20 md:h-24 flex items-center justify-center">
|
||||
<span className="text-2xl md:text-3xl text-gray-400">?</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showIPA && (
|
||||
<div className="text-2xl md:text-3xl text-gray-600 mb-2">
|
||||
{currentLetter.letter_sound_ipa}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showRoman && hasRomanization && currentLetter.roman_letter && (
|
||||
<div className="text-lg md:text-xl text-gray-500">
|
||||
{currentLetter.roman_letter}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 导航控制 */}
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
onClick={goToPrevious}
|
||||
className="p-3 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
|
||||
aria-label="上一个字母"
|
||||
>
|
||||
<ChevronLeft size={24} />
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
{isRandomMode ? (
|
||||
<button
|
||||
onClick={goToRandom}
|
||||
className="px-4 py-2 rounded-full bg-[#35786f] text-white text-sm font-medium hover:bg-[#2d5f58] transition-colors"
|
||||
>
|
||||
{t("randomNext")}
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex gap-1 flex-wrap max-w-xs justify-center">
|
||||
{alphabet.slice(0, 20).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
index === currentIndex
|
||||
? "w-8 bg-[#35786f]"
|
||||
: "w-2 bg-gray-300"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
{alphabet.length > 20 && (
|
||||
<div className="text-xs text-gray-500 flex items-center">...</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={goToNext}
|
||||
className="p-3 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
|
||||
aria-label="下一个字母"
|
||||
>
|
||||
<ChevronRight size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作提示 */}
|
||||
<div className="text-center mt-6 text-white text-sm">
|
||||
<p>
|
||||
{isRandomMode
|
||||
? "使用左右箭头键或空格键随机切换字母,ESC键返回"
|
||||
: "使用左右箭头键或滑动切换字母,ESC键返回"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 触摸事件处理 */}
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
onTouchEnd={onTouchEnd}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import LightButton from "@/components/buttons/LightButton";
|
||||
import IconClick from "@/components/IconClick";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
import IconClick from "@/components/ui/buttons/IconClick";
|
||||
import IMAGES from "@/config/images";
|
||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||
import {
|
||||
Dispatch,
|
||||
KeyboardEvent,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
@@ -19,25 +20,26 @@ export default function MemoryCard({
|
||||
setChosenAlphabet: Dispatch<SetStateAction<SupportedAlphabets | null>>;
|
||||
}) {
|
||||
const t = useTranslations("alphabet");
|
||||
const [index, setIndex] = useState(
|
||||
Math.floor(Math.random() * alphabet.length),
|
||||
);
|
||||
const [index, setIndex] = useState(() => alphabet.length > 0 ? Math.floor(Math.random() * alphabet.length) : 0);
|
||||
const [more, setMore] = useState(false);
|
||||
const [ipaDisplay, setIPADisplay] = useState(true);
|
||||
const [letterDisplay, setLetterDisplay] = useState(true);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (alphabet.length > 0) {
|
||||
setIndex(Math.floor(Math.random() * alphabet.length));
|
||||
}
|
||||
}, [alphabet.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeydown = (e: globalThis.KeyboardEvent) => {
|
||||
if (e.key === " ") refresh();
|
||||
};
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
return () => document.removeEventListener("keydown", handleKeydown);
|
||||
});
|
||||
}, [refresh]);
|
||||
|
||||
const letter = alphabet[index];
|
||||
const refresh = () => {
|
||||
setIndex(Math.floor(Math.random() * alphabet.length));
|
||||
};
|
||||
const letter = alphabet[index] || { letter: "", letter_name_ipa: "", letter_sound_ipa: "" };
|
||||
return (
|
||||
<div
|
||||
className="w-full flex justify-center items-center"
|
||||
|
||||
@@ -1,48 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import LightButton from "@/components/buttons/LightButton";
|
||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||
import { useEffect, useState } from "react";
|
||||
import MemoryCard from "./MemoryCard";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||
import Container from "@/components/ui/Container";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
import AlphabetCard from "./AlphabetCard";
|
||||
|
||||
export default function Alphabet() {
|
||||
const t = useTranslations("alphabet");
|
||||
const [chosenAlphabet, setChosenAlphabet] =
|
||||
useState<SupportedAlphabets | null>(null);
|
||||
const [alphabetData, setAlphabetData] = useState<
|
||||
Record<SupportedAlphabets, Letter[] | null>
|
||||
>({
|
||||
japanese: null,
|
||||
english: null,
|
||||
esperanto: null,
|
||||
uyghur: null,
|
||||
});
|
||||
const [loadingState, setLoadingState] = useState<
|
||||
"idle" | "loading" | "success" | "error"
|
||||
>("idle");
|
||||
const [chosenAlphabet, setChosenAlphabet] = useState<SupportedAlphabets | null>(null);
|
||||
const [alphabetData, setAlphabetData] = useState<Letter[] | null>(null);
|
||||
const [loadingState, setLoadingState] = useState<"idle" | "loading" | "success" | "error">("idle");
|
||||
|
||||
useEffect(() => {
|
||||
if (chosenAlphabet && !alphabetData[chosenAlphabet]) {
|
||||
setLoadingState("loading");
|
||||
|
||||
fetch("/alphabets/" + chosenAlphabet + ".json")
|
||||
.then((res) => {
|
||||
const loadAlphabetData = async () => {
|
||||
if (chosenAlphabet && !alphabetData) {
|
||||
try {
|
||||
setLoadingState("loading");
|
||||
|
||||
const res = await fetch("/alphabets/" + chosenAlphabet + ".json");
|
||||
if (!res.ok) throw new Error("Network response was not ok");
|
||||
return res.json();
|
||||
})
|
||||
.then((obj) => {
|
||||
setAlphabetData((prev) => ({
|
||||
...prev,
|
||||
[chosenAlphabet]: obj as Letter[],
|
||||
}));
|
||||
|
||||
const obj = await res.json();
|
||||
setAlphabetData(obj as Letter[]);
|
||||
setLoadingState("success");
|
||||
})
|
||||
.catch(() => {
|
||||
} catch (error) {
|
||||
setLoadingState("error");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadAlphabetData();
|
||||
}, [chosenAlphabet, alphabetData]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,48 +39,106 @@ export default function Alphabet() {
|
||||
const timer = setTimeout(() => {
|
||||
setLoadingState("idle");
|
||||
setChosenAlphabet(null);
|
||||
setAlphabetData(null);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [loadingState]);
|
||||
|
||||
if (!chosenAlphabet)
|
||||
// 语言选择界面
|
||||
if (!chosenAlphabet) {
|
||||
return (
|
||||
<>
|
||||
<div className="border border-gray-200 m-4 mt-4 flex flex-col justify-center items-center p-4 rounded-2xl gap-2">
|
||||
<span className="text-2xl md:text-3xl">{t("chooseCharacters")}</span>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<LightButton onClick={() => setChosenAlphabet("japanese")}>
|
||||
{t("japanese")}
|
||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex flex-col items-center justify-center px-4">
|
||||
<Container className="p-8 max-w-2xl w-full text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-gray-800 mb-4">
|
||||
{t("chooseCharacters")}
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-8 text-lg">
|
||||
选择一种语言的字母表开始学习
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<LightButton
|
||||
onClick={() => setChosenAlphabet("japanese")}
|
||||
className="p-6 text-lg font-medium hover:scale-105 transition-transform"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-2xl mb-2">あいうえお</span>
|
||||
<span>{t("japanese")}</span>
|
||||
</div>
|
||||
</LightButton>
|
||||
<LightButton onClick={() => setChosenAlphabet("english")}>
|
||||
{t("english")}
|
||||
|
||||
<LightButton
|
||||
onClick={() => setChosenAlphabet("english")}
|
||||
className="p-6 text-lg font-medium hover:scale-105 transition-transform"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-2xl mb-2">ABC</span>
|
||||
<span>{t("english")}</span>
|
||||
</div>
|
||||
</LightButton>
|
||||
<LightButton onClick={() => setChosenAlphabet("uyghur")}>
|
||||
{t("uyghur")}
|
||||
|
||||
<LightButton
|
||||
onClick={() => setChosenAlphabet("uyghur")}
|
||||
className="p-6 text-lg font-medium hover:scale-105 transition-transform"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-2xl mb-2">ئۇيغۇر</span>
|
||||
<span>{t("uyghur")}</span>
|
||||
</div>
|
||||
</LightButton>
|
||||
<LightButton onClick={() => setChosenAlphabet("esperanto")}>
|
||||
{t("esperanto")}
|
||||
|
||||
<LightButton
|
||||
onClick={() => setChosenAlphabet("esperanto")}
|
||||
className="p-6 text-lg font-medium hover:scale-105 transition-transform"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-2xl mb-2">ABCĜĤ</span>
|
||||
<span>{t("esperanto")}</span>
|
||||
</div>
|
||||
</LightButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
if (loadingState === "loading") {
|
||||
return t("loading");
|
||||
}
|
||||
if (loadingState === "error") {
|
||||
return t("loadFailed");
|
||||
}
|
||||
if (loadingState === "success" && alphabetData[chosenAlphabet]) {
|
||||
return (
|
||||
<>
|
||||
<MemoryCard
|
||||
alphabet={alphabetData[chosenAlphabet]}
|
||||
setChosenAlphabet={setChosenAlphabet}
|
||||
></MemoryCard>
|
||||
</>
|
||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center">
|
||||
<Container className="p-8 text-center">
|
||||
<div className="text-2xl text-gray-600">{t("loading")}</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
if (loadingState === "error") {
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center">
|
||||
<Container className="p-8 text-center">
|
||||
<div className="text-2xl text-red-600">{t("loadFailed")}</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 字母卡片界面
|
||||
if (loadingState === "success" && alphabetData) {
|
||||
return (
|
||||
<AlphabetCard
|
||||
alphabet={alphabetData}
|
||||
alphabetType={chosenAlphabet}
|
||||
onBack={() => {
|
||||
setChosenAlphabet(null);
|
||||
setAlphabetData(null);
|
||||
setLoadingState("idle");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import Container from "@/components/cards/Container";
|
||||
import Container from "@/components/ui/Container";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Center } from "@/components/Center";
|
||||
import { Center } from "@/components/common/Center";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { Folder } from "../../../../generated/prisma/browser";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import LightButton from "@/components/buttons/LightButton";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||
import { getTTSAudioUrl } from "@/lib/browser/tts";
|
||||
import { VOICES } from "@/config/locales";
|
||||
|
||||
@@ -21,7 +21,7 @@ export default async function MemorizePage({
|
||||
|
||||
if (!session) {
|
||||
redirect(
|
||||
`/login?redirect=/memorize${(await searchParams).folder_id
|
||||
`/auth?redirect=/memorize${(await searchParams).folder_id
|
||||
? `?folder_id=${tParam}`
|
||||
: ""
|
||||
}`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LightButton from "@/components/buttons/LightButton";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
import { useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef, forwardRef, useEffect, useCallback } from "react";
|
||||
import SubtitleDisplay from "./SubtitleDisplay";
|
||||
import LightButton from "@/components/buttons/LightButton";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
TextSpeakerArraySchema,
|
||||
TextSpeakerItemSchema,
|
||||
} from "@/lib/interfaces";
|
||||
import IconClick from "@/components/IconClick";
|
||||
import IconClick from "@/components/ui/buttons/IconClick";
|
||||
import IMAGES from "@/config/images";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import LightButton from "@/components/buttons/LightButton";
|
||||
import IconClick from "@/components/IconClick";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
import IconClick from "@/components/ui/buttons/IconClick";
|
||||
import IMAGES from "@/config/images";
|
||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import LightButton from "@/components/buttons/LightButton";
|
||||
import Container from "@/components/cards/Container";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
import Container from "@/components/ui/Container";
|
||||
import { TranslationHistorySchema } from "@/lib/interfaces";
|
||||
import { Dispatch, useEffect, useState } from "react";
|
||||
import z from "zod";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Container from "@/components/cards/Container";
|
||||
import Container from "@/components/ui/Container";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Folder } from "../../../../generated/prisma/browser";
|
||||
import { getFoldersByUserId } from "@/lib/server/services/folderService";
|
||||
import LightButton from "@/components/buttons/LightButton";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
import { Folder as Fd } from "lucide-react";
|
||||
|
||||
interface FolderSelectorProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import LightButton from "@/components/buttons/LightButton";
|
||||
import IconClick from "@/components/IconClick";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
import IconClick from "@/components/ui/buttons/IconClick";
|
||||
import IMAGES from "@/config/images";
|
||||
import { VOICES } from "@/config/locales";
|
||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||
|
||||
Reference in New Issue
Block a user