...
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||||
import IconClick from "@/components/ui/buttons/IconClick";
|
import { IconClick } from "@/components/ui/buttons";
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import IconClick from "@/components/ui/buttons/IconClick";
|
import { IconClick } from "@/components/ui/buttons";
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||||
import Container from "@/components/ui/Container";
|
import Container from "@/components/ui/Container";
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import AlphabetCard from "./AlphabetCard";
|
import AlphabetCard from "./AlphabetCard";
|
||||||
|
|
||||||
export default function Alphabet() {
|
export default function Alphabet() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useRef, forwardRef, useEffect, useCallback } from "react";
|
import { useState, useRef, forwardRef, useEffect, useCallback } from "react";
|
||||||
import SubtitleDisplay from "./SubtitleDisplay";
|
import SubtitleDisplay from "./SubtitleDisplay";
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
|
import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import { PlayButtonProps } from "../../types/player";
|
import { PlayButtonProps } from "../../types/player";
|
||||||
|
|
||||||
export default function PlayButton({ isPlaying, onToggle, disabled, className }: PlayButtonProps) {
|
export default function PlayButton({ isPlaying, onToggle, disabled, className }: PlayButtonProps) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import { SpeedControlProps } from "../../types/player";
|
import { SpeedControlProps } from "../../types/player";
|
||||||
import { getPlaybackRateOptions, getPlaybackRateLabel } from "../../utils/timeUtils";
|
import { getPlaybackRateOptions, getPlaybackRateLabel } from "../../utils/timeUtils";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { ChevronLeft, ChevronRight, RotateCcw, Pause } from "lucide-react";
|
import { ChevronLeft, ChevronRight, RotateCcw, Pause } from "lucide-react";
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import { ControlBarProps } from "../../types/controls";
|
import { ControlBarProps } from "../../types/controls";
|
||||||
import PlayButton from "../atoms/PlayButton";
|
import PlayButton from "../atoms/PlayButton";
|
||||||
import SpeedControl from "../atoms/SpeedControl";
|
import SpeedControl from "../atoms/SpeedControl";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Video, FileText } from "lucide-react";
|
import { Video, FileText } from "lucide-react";
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import { FileUploadProps } from "../../types/controls";
|
import { FileUploadProps } from "../../types/controls";
|
||||||
import { useFileUpload } from "../../hooks/useFileUpload";
|
import { useFileUpload } from "../../hooks/useFileUpload";
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import SubtitleArea from "./components/compounds/SubtitleArea";
|
|||||||
import ControlBar from "./components/compounds/ControlBar";
|
import ControlBar from "./components/compounds/ControlBar";
|
||||||
import UploadZone from "./components/compounds/UploadZone";
|
import UploadZone from "./components/compounds/UploadZone";
|
||||||
import SeekBar from "./components/atoms/SeekBar";
|
import SeekBar from "./components/atoms/SeekBar";
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
|
|
||||||
export default function SrtPlayerPage() {
|
export default function SrtPlayerPage() {
|
||||||
const t = useTranslations("home");
|
const t = useTranslations("home");
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
TextSpeakerArraySchema,
|
TextSpeakerArraySchema,
|
||||||
TextSpeakerItemSchema,
|
TextSpeakerItemSchema,
|
||||||
} from "@/lib/interfaces";
|
} from "@/lib/interfaces";
|
||||||
import IconClick from "@/components/ui/buttons/IconClick";
|
import { IconClick } from "@/components/ui/buttons";
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import IconClick from "@/components/ui/buttons/IconClick";
|
import { IconClick } from "@/components/ui/buttons";
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import Container from "@/components/ui/Container";
|
import Container from "@/components/ui/Container";
|
||||||
import { TranslationHistorySchema } from "@/lib/interfaces";
|
import { TranslationHistorySchema } from "@/lib/interfaces";
|
||||||
import { Dispatch, useEffect, useState } from "react";
|
import { Dispatch, useEffect, useState } from "react";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Container from "@/components/ui/Container";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Folder } from "../../../../generated/prisma/browser";
|
import { Folder } from "../../../../generated/prisma/browser";
|
||||||
import { getFoldersByUserId } from "@/lib/server/services/folderService";
|
import { getFoldersByUserId } from "@/lib/server/services/folderService";
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import { Folder as Fd } from "lucide-react";
|
import { Folder as Fd } from "lucide-react";
|
||||||
|
|
||||||
interface FolderSelectorProps {
|
interface FolderSelectorProps {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import IconClick from "@/components/ui/buttons/IconClick";
|
import { IconClick } from "@/components/ui/buttons";
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import { VOICES } from "@/config/locales";
|
import { VOICES } from "@/config/locales";
|
||||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import { signInAction, signUpAction, SignUpState } from "@/lib/actions/auth";
|
import { signInAction, signUpAction, SignUpState } from "@/lib/actions/auth";
|
||||||
import Container from "@/components/ui/Container";
|
import Container from "@/components/ui/Container";
|
||||||
import Input from "@/components/ui/Input";
|
import Input from "@/components/ui/Input";
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
|
||||||
interface AuthFormProps {
|
interface AuthFormProps {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import Input from "@/components/ui/Input";
|
import Input from "@/components/ui/Input";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { LOCALES } from "@/config/locales";
|
||||||
|
|
||||||
interface AddTextPairModalProps {
|
interface AddTextPairModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -15,6 +16,53 @@ interface AddTextPairModalProps {
|
|||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const COMMON_LOCALES = [
|
||||||
|
{ label: "中文", value: "zh-CN" },
|
||||||
|
{ label: "英文", value: "en-US" },
|
||||||
|
{ label: "意大利语", value: "it-IT" },
|
||||||
|
{ label: "日语", value: "ja-JP" },
|
||||||
|
{ label: "其他", value: "other" },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface LocaleSelectorProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
|
||||||
|
const isCommonLocale = COMMON_LOCALES.some((l) => l.value === value && l.value !== "other");
|
||||||
|
const showFullList = value === "other" || !isCommonLocale;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value={isCommonLocale ? value : "other"}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f]"
|
||||||
|
>
|
||||||
|
{COMMON_LOCALES.map((locale) => (
|
||||||
|
<option key={locale.value} value={locale.value}>
|
||||||
|
{locale.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{showFullList && (
|
||||||
|
<select
|
||||||
|
value={value === "other" ? LOCALES[0] : value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f] mt-2"
|
||||||
|
>
|
||||||
|
{LOCALES.map((locale) => (
|
||||||
|
<option key={locale} value={locale}>
|
||||||
|
{locale}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function AddTextPairModal({
|
export default function AddTextPairModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -23,23 +71,23 @@ export default function AddTextPairModal({
|
|||||||
const t = useTranslations("folder_id");
|
const t = useTranslations("folder_id");
|
||||||
const input1Ref = useRef<HTMLInputElement>(null);
|
const input1Ref = useRef<HTMLInputElement>(null);
|
||||||
const input2Ref = useRef<HTMLInputElement>(null);
|
const input2Ref = useRef<HTMLInputElement>(null);
|
||||||
const input3Ref = useRef<HTMLInputElement>(null);
|
const [locale1, setLocale1] = useState("en-US");
|
||||||
const input4Ref = useRef<HTMLInputElement>(null);
|
const [locale2, setLocale2] = useState("zh-CN");
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
if (
|
if (
|
||||||
!input1Ref.current?.value ||
|
!input1Ref.current?.value ||
|
||||||
!input2Ref.current?.value ||
|
!input2Ref.current?.value ||
|
||||||
!input3Ref.current?.value ||
|
!locale1 ||
|
||||||
!input4Ref.current?.value
|
!locale2
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const text1 = input1Ref.current.value;
|
const text1 = input1Ref.current.value;
|
||||||
const text2 = input2Ref.current.value;
|
const text2 = input2Ref.current.value;
|
||||||
const locale1 = input3Ref.current.value;
|
|
||||||
const locale2 = input4Ref.current.value;
|
|
||||||
if (
|
if (
|
||||||
typeof text1 === "string" &&
|
typeof text1 === "string" &&
|
||||||
typeof text2 === "string" &&
|
typeof text2 === "string" &&
|
||||||
@@ -55,6 +103,7 @@ export default function AddTextPairModal({
|
|||||||
input2Ref.current.value = "";
|
input2Ref.current.value = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black/50 backdrop-blur flex items-center justify-center z-50"
|
className="fixed inset-0 bg-black/50 backdrop-blur flex items-center justify-center z-50"
|
||||||
@@ -83,19 +132,11 @@ export default function AddTextPairModal({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{t("locale1")}
|
{t("locale1")}
|
||||||
<Input
|
<LocaleSelector value={locale1} onChange={setLocale1} />
|
||||||
ref={input3Ref}
|
|
||||||
className="w-full"
|
|
||||||
placeholder="en-US"
|
|
||||||
></Input>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{t("locale2")}
|
{t("locale2")}
|
||||||
<Input
|
<LocaleSelector value={locale2} onChange={setLocale2} />
|
||||||
ref={input4Ref}
|
|
||||||
className="w-full"
|
|
||||||
placeholder="zh-CN"
|
|
||||||
></Input>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LightButton onClick={handleAdd}>{t("add")}</LightButton>
|
<LightButton onClick={handleAdd}>{t("add")}</LightButton>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import AddTextPairModal from "./AddTextPairModal";
|
|||||||
import TextPairCard from "./TextPairCard";
|
import TextPairCard from "./TextPairCard";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import PageLayout from "@/components/ui/PageLayout";
|
import PageLayout from "@/components/ui/PageLayout";
|
||||||
import GreenButton from "@/components/ui/buttons/GreenButton";
|
import { GreenButton } from "@/components/ui/buttons";
|
||||||
import IconButton from "@/components/ui/buttons/IconButton";
|
import { IconButton } from "@/components/ui/buttons";
|
||||||
import CardList from "@/components/ui/CardList";
|
import CardList from "@/components/ui/CardList";
|
||||||
|
|
||||||
export interface TextPair {
|
export interface TextPair {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import Input from "@/components/ui/Input";
|
import Input from "@/components/ui/Input";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import LightButton from "@/components/ui/buttons/LightButton";
|
import { LightButton } from "@/components/ui/buttons";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import IconClick from "./ui/buttons/IconClick";
|
import { IconClick, GhostButton } from "./ui/buttons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import GhostButton from "./ui/buttons/GhostButton";
|
|
||||||
|
|
||||||
export default function LanguageSettings() {
|
export default function LanguageSettings() {
|
||||||
const [showLanguageMenu, setShowLanguageMenu] = useState(false);
|
const [showLanguageMenu, setShowLanguageMenu] = useState(false);
|
||||||
@@ -21,6 +20,7 @@ export default function LanguageSettings() {
|
|||||||
alt="language"
|
alt="language"
|
||||||
disableOnHoverBgChange={true}
|
disableOnHoverBgChange={true}
|
||||||
onClick={handleLanguageClick}
|
onClick={handleLanguageClick}
|
||||||
|
size={40}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{showLanguageMenu && (
|
{showLanguageMenu && (
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
import { Folder, Home } from "lucide-react";
|
import { Folder, Home, User } from "lucide-react";
|
||||||
import LanguageSettings from "../LanguageSettings";
|
import LanguageSettings from "../LanguageSettings";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import GhostButton from "../ui/buttons/GhostButton";
|
import { GhostButton } from "../ui/buttons";
|
||||||
|
|
||||||
export async function Navbar() {
|
export async function Navbar() {
|
||||||
const t = await getTranslations("navbar");
|
const t = await getTranslations("navbar");
|
||||||
@@ -14,46 +14,56 @@ export async function Navbar() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center w-full h-16 px-8 bg-[#35786f] text-white">
|
<div className="flex justify-between items-center w-full h-16 px-4 md:px-8 bg-[#35786f] text-white">
|
||||||
<GhostButton href="/" className="text-xl border-b hidden md:block">
|
<GhostButton href="/" className="text-lg md:text-xl border-b hidden! md:block!">
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</GhostButton>
|
</GhostButton>
|
||||||
<GhostButton className="block md:hidden" href={"/"}>
|
<GhostButton className="block! md:hidden!" href={"/"}>
|
||||||
<Home />
|
<Home size={20} />
|
||||||
</GhostButton>
|
</GhostButton>
|
||||||
<div className="flex text-xl gap-0.5 justify-center items-center flex-wrap">
|
<div className="flex text-base md:text-xl gap-0.5 justify-center items-center flex-wrap">
|
||||||
|
<LanguageSettings />
|
||||||
<GhostButton
|
<GhostButton
|
||||||
className="md:hidden block"
|
className="md:hidden! block! border-0 bg-transparent hover:bg-black/30 shadow-none p-2"
|
||||||
href="https://github.com/GoddoNebianU/learn-languages"
|
href="https://github.com/GoddoNebianU/learn-languages"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={IMAGES.github_mark_white}
|
src={IMAGES.github_mark_white}
|
||||||
alt="GitHub"
|
alt="GitHub"
|
||||||
width={24}
|
width={20}
|
||||||
height={24}
|
height={20}
|
||||||
/>
|
/>
|
||||||
</GhostButton>
|
</GhostButton>
|
||||||
<LanguageSettings />
|
<GhostButton href="/folders" className="md:block! hidden! border-0 bg-transparent hover:bg-black/30 shadow-none">
|
||||||
<GhostButton href="/folders" className="md:block hidden">
|
|
||||||
{t("folders")}
|
{t("folders")}
|
||||||
</GhostButton>
|
</GhostButton>
|
||||||
<GhostButton href="/folders" className="md:hidden block">
|
<GhostButton href="/folders" className="md:hidden! block! border-0 bg-transparent hover:bg-black/30 shadow-none p-2">
|
||||||
<Folder />
|
<Folder size={20} />
|
||||||
</GhostButton>
|
</GhostButton>
|
||||||
{
|
|
||||||
(() => {
|
|
||||||
return session &&
|
|
||||||
<GhostButton href="/profile">{t("profile")}</GhostButton>
|
|
||||||
|| <GhostButton href="/auth">{t("sign_in")}</GhostButton>;
|
|
||||||
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
<GhostButton
|
<GhostButton
|
||||||
className="hidden md:block"
|
className="hidden! md:block! border-0 bg-transparent hover:bg-black/30 shadow-none"
|
||||||
href="https://github.com/GoddoNebianU/learn-languages"
|
href="https://github.com/GoddoNebianU/learn-languages"
|
||||||
>
|
>
|
||||||
{t("sourceCode")}
|
{t("sourceCode")}
|
||||||
</GhostButton>
|
</GhostButton>
|
||||||
|
{
|
||||||
|
(() => {
|
||||||
|
return session &&
|
||||||
|
<>
|
||||||
|
<GhostButton href="/profile" className="hidden! md:block! text-sm md:text-base border-0 bg-transparent hover:bg-black/30 shadow-none px-2 py-1">{t("profile")}</GhostButton>
|
||||||
|
<GhostButton href="/profile" className="md:hidden! block! border-0 bg-transparent hover:bg-black/30 shadow-none p-2">
|
||||||
|
<User size={20} />
|
||||||
|
</GhostButton>
|
||||||
|
</>
|
||||||
|
|| <>
|
||||||
|
<GhostButton href="/auth" className="hidden! md:block! text-sm md:text-base border-0 bg-transparent hover:bg-black/30 shadow-none px-2 py-1">{t("sign_in")}</GhostButton>
|
||||||
|
<GhostButton href="/auth" className="md:hidden! block! border-0 bg-transparent hover:bg-black/30 shadow-none p-2">
|
||||||
|
<User size={20} />
|
||||||
|
</GhostButton>
|
||||||
|
</>;
|
||||||
|
|
||||||
|
})()
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
163
src/components/ui/Button.tsx
Normal file
163
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { COLORS } from "@/lib/theme/colors";
|
||||||
|
|
||||||
|
export type ButtonVariant = "primary" | "secondary" | "ghost" | "icon";
|
||||||
|
export type ButtonSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
export interface ButtonProps {
|
||||||
|
// Content
|
||||||
|
children?: React.ReactNode;
|
||||||
|
|
||||||
|
// Behavior
|
||||||
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
|
|
||||||
|
// Styling
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
className?: string;
|
||||||
|
selected?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
leftIcon?: React.ReactNode;
|
||||||
|
rightIcon?: React.ReactNode;
|
||||||
|
iconSrc?: string; // For Next.js Image icons
|
||||||
|
iconAlt?: string;
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
href?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Button({
|
||||||
|
variant = "secondary",
|
||||||
|
size = "md",
|
||||||
|
selected = false,
|
||||||
|
href,
|
||||||
|
iconSrc,
|
||||||
|
iconAlt,
|
||||||
|
leftIcon,
|
||||||
|
rightIcon,
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
style,
|
||||||
|
type = "button",
|
||||||
|
disabled = false,
|
||||||
|
...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
// Base classes
|
||||||
|
const baseClasses = "inline-flex items-center justify-center gap-2 rounded font-bold shadow hover:cursor-pointer transition-colors";
|
||||||
|
|
||||||
|
// Variant-specific classes
|
||||||
|
const variantStyles: Record<ButtonVariant, string> = {
|
||||||
|
primary: `
|
||||||
|
text-white
|
||||||
|
hover:opacity-90
|
||||||
|
`,
|
||||||
|
secondary: `
|
||||||
|
text-black
|
||||||
|
hover:bg-gray-100
|
||||||
|
`,
|
||||||
|
ghost: `
|
||||||
|
hover:bg-black/30
|
||||||
|
p-2
|
||||||
|
`,
|
||||||
|
icon: `
|
||||||
|
p-2 bg-gray-200 rounded-full
|
||||||
|
hover:bg-gray-300
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Size-specific classes
|
||||||
|
const sizeStyles: Record<ButtonSize, string> = {
|
||||||
|
sm: "px-3 py-1 text-sm",
|
||||||
|
md: "px-4 py-2",
|
||||||
|
lg: "px-6 py-3 text-lg"
|
||||||
|
};
|
||||||
|
|
||||||
|
const variantClass = variantStyles[variant];
|
||||||
|
const sizeClass = sizeStyles[size];
|
||||||
|
|
||||||
|
// Selected state for secondary variant
|
||||||
|
const selectedClass = variant === "secondary" && selected ? "bg-gray-100" : "";
|
||||||
|
|
||||||
|
// Background color for primary variant
|
||||||
|
const backgroundColor = variant === "primary" ? COLORS.primary : undefined;
|
||||||
|
|
||||||
|
// Combine all classes
|
||||||
|
const combinedClasses = `
|
||||||
|
${baseClasses}
|
||||||
|
${variantClass}
|
||||||
|
${sizeClass}
|
||||||
|
${selectedClass}
|
||||||
|
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||||
|
${className}
|
||||||
|
`.trim().replace(/\s+/g, " ");
|
||||||
|
|
||||||
|
// Icon rendering helper for SVG icons
|
||||||
|
const renderSvgIcon = (icon: React.ReactNode, position: "left" | "right") => {
|
||||||
|
if (!icon) return null;
|
||||||
|
return (
|
||||||
|
<span className={`flex items-center ${position === "left" ? "-ml-1 mr-2" : "-mr-1 ml-2"}`}>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Image icon rendering for Next.js Image
|
||||||
|
const renderImageIcon = () => {
|
||||||
|
if (!iconSrc) return null;
|
||||||
|
const sizeMap = { sm: 16, md: 20, lg: 24 };
|
||||||
|
const imgSize = sizeMap[size] || 20;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={iconSrc}
|
||||||
|
width={imgSize}
|
||||||
|
height={imgSize}
|
||||||
|
alt={iconAlt || "icon"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Content assembly
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
{renderImageIcon()}
|
||||||
|
{renderSvgIcon(leftIcon, "left")}
|
||||||
|
{children}
|
||||||
|
{renderSvgIcon(rightIcon, "right")}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If href is provided, render as Link
|
||||||
|
if (href) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={combinedClasses}
|
||||||
|
style={{ ...style, backgroundColor }}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise render as button
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
disabled={disabled}
|
||||||
|
className={combinedClasses}
|
||||||
|
style={{ ...style, backgroundColor }}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export type ButtonType = "button" | "submit" | "reset" | undefined;
|
|
||||||
|
|
||||||
export default function GhostButton({
|
|
||||||
onClick,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
type = "button",
|
|
||||||
href
|
|
||||||
}: {
|
|
||||||
onClick?: () => void;
|
|
||||||
className?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
type?: ButtonType;
|
|
||||||
href?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={`rounded hover:bg-black/30 p-2 ${className}`}
|
|
||||||
type={type}
|
|
||||||
>
|
|
||||||
{href ? <Link href={href}>{children}</Link> : children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import PlainButton, { ButtonType } from "./PlainButton";
|
|
||||||
import { COLORS } from "@/lib/theme/colors";
|
|
||||||
|
|
||||||
interface GreenButtonProps {
|
|
||||||
onClick?: () => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
type?: ButtonType;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GreenButton({
|
|
||||||
onClick,
|
|
||||||
children,
|
|
||||||
className = "",
|
|
||||||
type = "button",
|
|
||||||
disabled = false,
|
|
||||||
}: GreenButtonProps) {
|
|
||||||
return (
|
|
||||||
<PlainButton
|
|
||||||
onClick={onClick}
|
|
||||||
className={`px-4 py-2 rounded-full transition-colors text-sm font-medium text-white hover:bg-opacity-90 ${className}`}
|
|
||||||
style={{ backgroundColor: COLORS.primary }}
|
|
||||||
type={type}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</PlainButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import PlainButton, { ButtonType } from "./PlainButton";
|
|
||||||
|
|
||||||
interface IconButtonProps {
|
|
||||||
onClick?: () => void;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
type?: ButtonType;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function IconButton({
|
|
||||||
onClick,
|
|
||||||
icon,
|
|
||||||
className = "",
|
|
||||||
type = "button",
|
|
||||||
disabled = false,
|
|
||||||
}: IconButtonProps) {
|
|
||||||
return (
|
|
||||||
<PlainButton
|
|
||||||
onClick={onClick}
|
|
||||||
className={`p-2 bg-gray-200 rounded-full hover:bg-gray-300 transition-colors ${className}`}
|
|
||||||
type={type}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</PlainButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
interface IconClickProps {
|
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
className?: string;
|
|
||||||
size?: number;
|
|
||||||
disableOnHoverBgChange?: boolean;
|
|
||||||
}
|
|
||||||
export default function IconClick({
|
|
||||||
src,
|
|
||||||
alt,
|
|
||||||
onClick = () => {},
|
|
||||||
className = "",
|
|
||||||
size = 32,
|
|
||||||
disableOnHoverBgChange = false,
|
|
||||||
}: IconClickProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
onClick={onClick}
|
|
||||||
className={`${disableOnHoverBgChange ? "" : "hover:bg-gray-200"} hover:cursor-pointer rounded-3xl w-[${size}px] h-[${size}px] flex justify-center items-center ${className}`}
|
|
||||||
>
|
|
||||||
<Image src={src} width={size - 5} height={size - 5} alt={alt}></Image>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import PlainButton, { ButtonType } from "../buttons/PlainButton";
|
|
||||||
|
|
||||||
export default function LightButton({
|
|
||||||
onClick,
|
|
||||||
className,
|
|
||||||
selected,
|
|
||||||
children,
|
|
||||||
type = "button",
|
|
||||||
disabled
|
|
||||||
}: {
|
|
||||||
onClick?: (() => void) | undefined;
|
|
||||||
className?: string;
|
|
||||||
selected?: boolean;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
type?: ButtonType;
|
|
||||||
disabled?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<PlainButton
|
|
||||||
onClick={onClick}
|
|
||||||
className={`hover:bg-gray-100 text-black ${selected ? "bg-gray-100" : "bg-white"} ${className}`}
|
|
||||||
type={type}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</PlainButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
export type ButtonType = "button" | "submit" | "reset" | undefined;
|
|
||||||
|
|
||||||
export default function PlainButton({
|
|
||||||
onClick,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
type = "button",
|
|
||||||
disabled,
|
|
||||||
style
|
|
||||||
}: {
|
|
||||||
onClick?: () => void;
|
|
||||||
className?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
type?: ButtonType;
|
|
||||||
disabled?: boolean;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={`px-2 py-1 rounded shadow font-bold hover:cursor-pointer ${className}`}
|
|
||||||
type={type}
|
|
||||||
disabled={disabled}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
56
src/components/ui/buttons/index.tsx
Normal file
56
src/components/ui/buttons/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// 向后兼容的按钮组件包装器
|
||||||
|
// 这些组件将新 Button 组件包装,以保持向后兼容
|
||||||
|
|
||||||
|
import Button from "../Button";
|
||||||
|
|
||||||
|
// LightButton: 次要按钮,支持 selected 状态
|
||||||
|
export const LightButton = (props: any) => <Button variant="secondary" {...props} />;
|
||||||
|
|
||||||
|
// GreenButton: 主题色主要按钮
|
||||||
|
export const GreenButton = (props: any) => <Button variant="primary" {...props} />;
|
||||||
|
|
||||||
|
// IconButton: SVG 图标按钮
|
||||||
|
export const IconButton = (props: any) => {
|
||||||
|
const { icon, ...rest } = props;
|
||||||
|
return <Button variant="icon" leftIcon={icon} {...rest} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// GhostButton: 透明导航按钮
|
||||||
|
export const GhostButton = (props: any) => {
|
||||||
|
const { className, children, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Button variant="ghost" className={className} {...rest}>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// IconClick: 图片图标按钮
|
||||||
|
export const IconClick = (props: any) => {
|
||||||
|
// IconClick 使用 src/alt 属性,需要映射到 Button 的 iconSrc/iconAlt
|
||||||
|
const { src, alt, size, disableOnHoverBgChange, className, ...rest } = props;
|
||||||
|
let buttonSize: "sm" | "md" | "lg" = "md";
|
||||||
|
if (typeof size === "number") {
|
||||||
|
if (size <= 20) buttonSize = "sm";
|
||||||
|
else if (size >= 32) buttonSize = "lg";
|
||||||
|
} else if (typeof size === "string") {
|
||||||
|
buttonSize = (size === "sm" || size === "md" || size === "lg") ? size : "md";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果禁用悬停背景变化,通过 className 覆盖
|
||||||
|
const hoverClass = disableOnHoverBgChange ? "hover:bg-black/30 hover:cursor-pointer border-0 bg-transparent shadow-none" : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="icon"
|
||||||
|
iconSrc={src}
|
||||||
|
iconAlt={alt}
|
||||||
|
size={buttonSize}
|
||||||
|
className={`${hoverClass} ${className || ""}`}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// PlainButton: 基础小按钮
|
||||||
|
export const PlainButton = (props: any) => <Button variant="secondary" size="sm" {...props} />;
|
||||||
Reference in New Issue
Block a user