This commit is contained in:
2025-12-29 10:06:16 +08:00
parent d8f0117359
commit af259d4691
10 changed files with 452 additions and 267 deletions

View File

@@ -1,8 +1,6 @@
"use client"; "use client";
import Container from "@/components/ui/Container";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Center } from "@/components/common/Center";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { Folder } from "../../../../generated/prisma/browser"; import { Folder } from "../../../../generated/prisma/browser";
@@ -15,49 +13,77 @@ interface FolderSelectorProps {
const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => { const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
const t = useTranslations("memorize.folder_selector"); const t = useTranslations("memorize.folder_selector");
const router = useRouter(); const router = useRouter();
return ( return (
<Center> <div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8">
<Container className="p-6 gap-4 flex flex-col"> <div className="w-full max-w-2xl">
{(folders.length === 0 && ( <div className="bg-white rounded-2xl shadow-xl p-6 md:p-8">
<h1 className="text-2xl text-gray-900 font-light"> {folders.length === 0 ? (
{t("noFolders")} <div className="text-center">
<Link className="text-blue-900 border-b" href={"/folders"}> <h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-4">
folders {t("noFolders")}
</Link> </h1>
</h1> <Link
)) || ( className="inline-block px-6 py-2 bg-[#35786f] text-white rounded-full hover:bg-[#2d5f58] transition-colors"
<> href="/folders"
<h1 className="text-2xl text-gray-900 font-light"> >
{t("selectFolder")} Go to Folders
</h1> </Link>
<div className="text-gray-900 border border-gray-200 rounded-2xl max-h-96 overflow-y-auto">
{folders
.toSorted((a, b) => a.id - b.id)
.map((folder) => (
<div
key={folder.id}
onClick={() =>
router.push(`/memorize?folder_id=${folder.id}`)
}
className="flex flex-row justify-center items-center group p-2 gap-2 hover:cursor-pointer hover:bg-gray-50"
>
<Fd />
<div className="flex-1 flex gap-2">
<span className="group-hover:text-blue-500">
{t("folderInfo", {
id: folder.id,
name: folder.name,
count: folder.total,
})}
</span>
</div>
</div>
))}
</div> </div>
</> ) : (
)} <>
</Container> <h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-6">
</Center> {t("selectFolder")}
</h1>
<div className="border border-gray-200 rounded-2xl max-h-96 overflow-y-auto">
{folders
.toSorted((a, b) => a.id - b.id)
.map((folder) => (
<div
key={folder.id}
onClick={() =>
router.push(`/memorize?folder_id=${folder.id}`)
}
className="flex flex-row items-center p-4 gap-3 hover:cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0"
>
<div className="flex-shrink-0">
<Fd className="text-gray-600" size={24} />
</div>
<div className="flex-1">
<div className="font-medium text-gray-900">
{folder.name}
</div>
<div className="text-sm text-gray-500">
{t("folderInfo", {
id: folder.id,
name: folder.name,
count: folder.total,
})}
</div>
</div>
<div className="text-gray-400">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</div>
))}
</div>
</>
)}
</div>
</div>
</div>
); );
}; };

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import LightButton from "@/components/ui/buttons/LightButton";
import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { getTTSAudioUrl } from "@/lib/browser/tts"; import { getTTSAudioUrl } from "@/lib/browser/tts";
import { VOICES } from "@/config/locales"; import { VOICES } from "@/config/locales";
@@ -28,7 +27,13 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
const { load, play } = useAudioPlayer(); const { load, play } = useAudioPlayer();
if (textPairs.length === 0) { if (textPairs.length === 0) {
return <p>{t("noTextPairs")}</p>; return (
<div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4">
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full text-center">
<p className="text-gray-700">{t("noTextPairs")}</p>
</div>
</div>
);
} }
const rng = new SeededRandom(textPairs[0].folderId); const rng = new SeededRandom(textPairs[0].folderId);
@@ -38,135 +43,160 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
const getTextPairs = () => disorder ? disorderedTextPairs : textPairs; const getTextPairs = () => disorder ? disorderedTextPairs : textPairs;
const handleIndexClick = () => {
const newIndex = prompt("Input a index number.")?.trim();
if (
newIndex &&
isNonNegativeInteger(newIndex) &&
parseInt(newIndex) <= textPairs.length &&
parseInt(newIndex) > 0
) {
setIndex(parseInt(newIndex) - 1);
}
};
const handleNext = async () => {
if (show === "answer") {
const newIndex = (index + 1) % getTextPairs().length;
setIndex(newIndex);
if (dictation)
getTTSAudioUrl(
getTextPairs()[newIndex][reverse ? "text2" : "text1"],
VOICES.find(
(v) =>
v.locale ===
getTextPairs()[newIndex][
reverse ? "locale2" : "locale1"
],
)!.short_name,
).then((url) => {
load(url);
play();
});
}
setShow(show === "question" ? "answer" : "question");
};
const handlePrevious = () => {
setIndex(
(index - 1 + getTextPairs().length) % getTextPairs().length,
);
setShow("question");
};
const toggleReverse = () => setReverse(!reverse);
const toggleDictation = () => setDictation(!dictation);
const toggleDisorder = () => setDisorder(!disorder);
const createText = (text: string) => {
return (
<div className="text-gray-900 text-xl md:text-2xl p-6 h-[20dvh] overflow-y-auto text-center">
{text}
</div>
);
};
const [text1, text2] = reverse
? [getTextPairs()[index].text2, getTextPairs()[index].text1]
: [getTextPairs()[index].text1, getTextPairs()[index].text2];
return ( return (
<> <div className="min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8">
{(getTextPairs().length > 0 && ( <div className="w-full max-w-2xl">
<> <div className="bg-white rounded-2xl shadow-xl p-6 md:p-8">
<div className="text-center"> {/* 进度指示器 */}
<div <div className="flex justify-center mb-4">
className="text-sm text-gray-500" <button
onClick={() => { onClick={handleIndexClick}
const newIndex = prompt("Input a index number.")?.trim(); className="text-sm text-gray-500 hover:text-gray-700 transition-colors"
if (
newIndex &&
isNonNegativeInteger(newIndex) &&
parseInt(newIndex) <= textPairs.length &&
parseInt(newIndex) > 0
) {
setIndex(parseInt(newIndex) - 1);
}
}}
> >
{index + 1} {index + 1} / {getTextPairs().length}
{"/" + getTextPairs().length} </button>
</div> </div>
<div className={`h-[40dvh] md:px-16 px-4 ${myFont.className}`}>
{(() => { {/* 文本显示区域 */}
const createText = (text: string) => { <div className={`h-[40dvh] ${myFont.className} mb-4`}>
{(() => {
if (dictation) {
if (show === "question") {
return ( return (
<div className="text-gray-900 text-xl border-y border-y-gray-200 p-4 md:text-3xl h-[20dvh] overflow-y-auto"> <div className="h-full flex items-center justify-center">
{text} <div className="text-gray-400 text-4xl">?</div>
</div> </div>
); );
};
const [text1, text2] = reverse
? [getTextPairs()[index].text2, getTextPairs()[index].text1]
: [getTextPairs()[index].text1, getTextPairs()[index].text2];
if (dictation) {
// dictation
if (show === "question") {
return createText("");
} else {
return (
<>
{createText(text1)}
{createText(text2)}
</>
);
}
} else { } else {
// non-dictation return (
if (show === "question") { <div className="space-y-2">
return createText(text1); {createText(text1)}
} else { <div className="border-t border-gray-200"></div>
return ( {createText(text2)}
<> </div>
{createText(text1)} );
{createText(text2)}
</>
);
}
} }
})()} } else {
</div> if (show === "question") {
return createText(text1);
} else {
return (
<div className="space-y-2">
{createText(text1)}
<div className="border-t border-gray-200"></div>
{createText(text2)}
</div>
);
}
}
})()}
</div> </div>
{/* 底部按钮 */}
<div className="flex flex-row gap-2 items-center justify-center flex-wrap"> <div className="flex flex-row gap-2 items-center justify-center flex-wrap">
<LightButton <button
className="w-20" onClick={handleNext}
onClick={async () => { className="px-4 py-2 rounded-full bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors text-sm"
if (show === "answer") {
const newIndex = (index + 1) % getTextPairs().length;
setIndex(newIndex);
if (dictation)
getTTSAudioUrl(
getTextPairs()[newIndex][reverse ? "text2" : "text1"],
VOICES.find(
(v) =>
v.locale ===
getTextPairs()[newIndex][
reverse ? "locale2" : "locale1"
],
)!.short_name,
).then((url) => {
load(url);
play();
});
}
setShow(show === "question" ? "answer" : "question");
}}
> >
{show === "question" ? t("answer") : t("next")} {show === "question" ? t("answer") : t("next")}
</LightButton> </button>
<LightButton <button
onClick={() => { onClick={handlePrevious}
setIndex( className="px-4 py-2 rounded-full bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors text-sm"
(index - 1 + getTextPairs().length) % getTextPairs().length,
);
setShow("question");
}}
> >
{t("previous")} {t("previous")}
</LightButton> </button>
<LightButton <button
onClick={() => { onClick={toggleReverse}
setReverse(!reverse); className={`px-4 py-2 rounded-full transition-colors text-sm ${
}} reverse
selected={reverse} ? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
> >
{t("reverse")} {t("reverse")}
</LightButton> </button>
<LightButton <button
onClick={() => { onClick={toggleDictation}
setDictation(!dictation); className={`px-4 py-2 rounded-full transition-colors text-sm ${
}} dictation
selected={dictation} ? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
> >
{t("dictation")} {t("dictation")}
</LightButton> </button>
<LightButton <button
onClick={() => { onClick={toggleDisorder}
setDisorder(!disorder); className={`px-4 py-2 rounded-full transition-colors text-sm ${
}} disorder
selected={disorder} ? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
> >
{t("disorder")} {t("disorder")}
</LightButton> </button>
</div> </div>
</> </div>
)) || <p>{t("noTextPairs")}</p>} </div>
</> </div>
); );
}; };

View File

@@ -8,7 +8,6 @@ import {
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Center } from "@/components/common/Center";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Folder } from "../../../generated/prisma/browser"; import { Folder } from "../../../generated/prisma/browser";
import { import {
@@ -19,6 +18,9 @@ import {
} from "@/lib/server/services/folderService"; } from "@/lib/server/services/folderService";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { toast } from "sonner"; import { toast } from "sonner";
import PageLayout from "@/components/ui/PageLayout";
import PageHeader from "@/components/ui/PageHeader";
import CardList from "@/components/ui/CardList";
interface FolderProps { interface FolderProps {
folder: Folder & { total: number }; folder: Folder & { total: number };
@@ -27,8 +29,8 @@ interface FolderProps {
const FolderCard = ({ folder, refresh }: FolderProps) => { const FolderCard = ({ folder, refresh }: FolderProps) => {
const router = useRouter(); const router = useRouter();
const t = useTranslations("folders"); const t = useTranslations("folders");
return ( return (
<div <div
className="flex justify-between items-center group p-4 border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors" className="flex justify-between items-center group p-4 border-b border-gray-100 hover:bg-gray-50 cursor-pointer transition-colors"
@@ -37,24 +39,23 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
}} }}
> >
<div className="flex items-center gap-3 flex-1"> <div className="flex items-center gap-3 flex-1">
<div className="w-10 h-10 rounded-lg bg-linear-to-br from-blue-50 to-blue-100 flex items-center justify-center group-hover:from-blue-100 group-hover:to-blue-200 transition-colors"> <div className="shrink-0">
<Fd></Fd> <Fd className="text-gray-600" size={24} />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h3 className="font-medium text-gray-900"> <h3 className="font-medium text-gray-900">{folder.name}</h3>
<p className="text-sm text-gray-500">
{t("folderInfo", { {t("folderInfo", {
id: folder.id, id: folder.id,
name: folder.name, name: folder.name,
totalPairs: folder.total, totalPairs: folder.total,
})} })}
</h3> </p>
</div> </div>
<div className="text-xs text-gray-400">#{folder.id}</div>
</div> </div>
<div className="flex items-center gap-2 opacity-50 group-hover:opacity-100 transition-opacity"> <div className="flex items-center gap-2">
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -63,7 +64,7 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
renameFolderById(folder.id, newName).then(refresh); renameFolderById(folder.id, newName).then(refresh);
} }
}} }}
className="p-2 text-gray-400 hover:bg-red-50 rounded-lg transition-colors" className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
> >
<FolderPen size={16} /> <FolderPen size={16} />
</button> </button>
@@ -113,40 +114,38 @@ export default function FoldersClient({ userId }: { userId: string }) {
console.error(error); console.error(error);
} }
}; };
return ( return (
<Center> <PageLayout>
<div className="w-full max-w-2xl mx-auto bg-white border border-gray-200 rounded-2xl p-6"> <PageHeader title={t("title")} subtitle={t("subtitle")} />
<div className="mb-6">
<h1 className="text-2xl font-light text-gray-900">{t("title")}</h1>
<p className="text-sm text-gray-500 mt-1">{t("subtitle")}</p>
</div>
<button <button
onClick={async () => { onClick={async () => {
const folderName = prompt(t("enterFolderName")); const folderName = prompt(t("enterFolderName"));
if (!folderName) return; if (!folderName) return;
setLoading(true); setLoading(true);
try { try {
await createFolder({ await createFolder({
name: folderName, name: folderName,
user: { connect: { id: userId } }, user: { connect: { id: userId } },
}); });
await updateFolders(); await updateFolders();
} finally { } finally {
setLoading(false); setLoading(false);
} }
}} }}
disabled={loading} disabled={loading}
className="w-full p-3 border-2 border-dashed border-gray-300 rounded-xl text-gray-500 hover:border-gray-400 hover:text-gray-600 transition-colors flex items-center justify-center gap-2" className="w-full p-3 border-2 border-dashed border-gray-300 rounded-xl text-gray-500 hover:border-gray-400 hover:text-gray-600 transition-colors flex items-center justify-center gap-2"
> >
<FolderPlus size={18} /> <FolderPlus size={18} />
<span>{loading ? t("creating") : t("newFolder")}</span> <span>{loading ? t("creating") : t("newFolder")}</span>
</button> </button>
<div className="mt-4 max-h-96 overflow-y-auto"> <div className="mt-4">
<CardList>
{folders.length === 0 ? ( {folders.length === 0 ? (
<div className="text-center py-12 text-gray-400"> <div className="text-center py-12 text-gray-400">
<div className="w-16 h-16 mx-auto mb-3 rounded-lg bg-gray-100 flex items-center justify-center"> <div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
<FolderPlus size={24} className="text-gray-400" /> <FolderPlus size={24} className="text-gray-400" />
</div> </div>
<p className="text-sm">{t("noFoldersYet")}</p> <p className="text-sm">{t("noFoldersYet")}</p>
@@ -164,8 +163,8 @@ export default function FoldersClient({ userId }: { userId: string }) {
))} ))}
</div> </div>
)} )}
</div> </CardList>
</div> </div>
</Center> </PageLayout>
); );
} }

View File

@@ -1,10 +1,8 @@
"use client"; "use client";
import { ArrowLeft, Plus } from "lucide-react"; import { ArrowLeft, Plus } from "lucide-react";
import { Center } from "@/components/common/Center";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { redirect, useRouter } from "next/navigation"; import { redirect, useRouter } from "next/navigation";
import Container from "@/components/ui/Container";
import { import {
createPair, createPair,
deletePairById, deletePairById,
@@ -12,8 +10,11 @@ import {
} from "@/lib/server/services/pairService"; } from "@/lib/server/services/pairService";
import AddTextPairModal from "./AddTextPairModal"; import AddTextPairModal from "./AddTextPairModal";
import TextPairCard from "./TextPairCard"; import TextPairCard from "./TextPairCard";
import LightButton from "@/components/ui/buttons/LightButton";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import PageLayout from "@/components/ui/PageLayout";
import GreenButton from "@/components/ui/buttons/GreenButton";
import IconButton from "@/components/ui/buttons/IconButton";
import CardList from "@/components/ui/CardList";
export interface TextPair { export interface TextPair {
id: number; id: number;
@@ -55,79 +56,73 @@ export default function InFolder({ folderId }: { folderId: number }) {
}; };
return ( return (
<Center> <PageLayout>
<Container className="p-6"> <div className="mb-6">
<div className="mb-6"> <button
<button onClick={router.back}
onClick={router.back} className="flex items-center gap-2 text-gray-500 hover:text-gray-700 transition-colors mb-4"
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 transition-colors mb-4" >
> <ArrowLeft size={16} />
<ArrowLeft size={16} /> <span className="text-sm">{t("back")}</span>
<span className="text-sm">{t("back")}</span> </button>
</button>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-light text-gray-900"> <h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-1">
{t("textPairs")} {t("textPairs")}
</h1> </h1>
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-gray-500">
{t("itemsCount", { count: textPairs.length })} {t("itemsCount", { count: textPairs.length })}
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LightButton <GreenButton
onClick={() => { onClick={() => {
redirect(`/memorize?folder_id=${folderId}`); redirect(`/memorize?folder_id=${folderId}`);
}} }}
> >
{t("memorize")} {t("memorize")}
</LightButton> </GreenButton>
<button <IconButton
className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors" onClick={() => {
onClick={() => { setAddModal(true);
setAddModal(true); }}
}} icon={<Plus size={18} className="text-gray-700" />}
> />
<Plus
size={18}
className="text-gray-600 hover:cursor-pointer"
/>
</button>
</div>
</div> </div>
</div> </div>
</div>
<CardList>
{loading ? (
<div className="p-8 text-center">
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
<p className="text-sm text-gray-500">{t("loadingTextPairs")}</p>
</div>
) : textPairs.length === 0 ? (
<div className="p-12 text-center">
<p className="text-sm text-gray-500 mb-2">{t("noTextPairs")}</p>
</div>
) : (
<div className="divide-y divide-gray-100">
{textPairs
.toSorted((a, b) => a.id - b.id)
.map((textPair) => (
<TextPairCard
key={textPair.id}
textPair={textPair}
onDel={() => {
deletePairById(textPair.id);
refreshTextPairs();
}}
refreshTextPairs={refreshTextPairs}
/>
))}
</div>
)}
</CardList>
<div className="max-h-96 overflow-y-auto rounded-xl border border-gray-200 overflow-hidden">
{loading ? (
<div className="p-8 text-center">
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
<p className="text-sm text-gray-500">{t("loadingTextPairs")}</p>
</div>
) : textPairs.length === 0 ? (
<div className="p-12 text-center">
<p className="text-sm text-gray-500 mb-2">{t("noTextPairs")}</p>
</div>
) : (
<div className="divide-y divide-gray-100">
{textPairs
.toSorted((a, b) => a.id - b.id)
.map((textPair) => (
<TextPairCard
key={textPair.id}
textPair={textPair}
onDel={() => {
deletePairById(textPair.id);
refreshTextPairs();
}}
refreshTextPairs={refreshTextPairs}
/>
))}
</div>
)}
</div>
</Container>
<AddTextPairModal <AddTextPairModal
isOpen={openAddModal} isOpen={openAddModal}
onClose={() => setAddModal(false)} onClose={() => setAddModal(false)}
@@ -151,6 +146,6 @@ export default function InFolder({ folderId }: { folderId: number }) {
refreshTextPairs(); refreshTextPairs();
}} }}
/> />
</Center> </PageLayout>
); );
} }

View File

@@ -0,0 +1,12 @@
interface CardListProps {
children: React.ReactNode;
className?: string;
}
export default function CardList({ children, className = "" }: CardListProps) {
return (
<div className={`max-h-96 overflow-y-auto rounded-xl border border-gray-200 overflow-hidden ${className}`}>
{children}
</div>
);
}

View File

@@ -0,0 +1,17 @@
interface PageHeaderProps {
title: string;
subtitle?: string;
}
export default function PageHeader({ title, subtitle }: PageHeaderProps) {
return (
<div className="mb-6">
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-2">
{title}
</h1>
{subtitle && (
<p className="text-sm text-gray-500">{subtitle}</p>
)}
</div>
);
}

View File

@@ -0,0 +1,16 @@
interface PageLayoutProps {
children: React.ReactNode;
className?: string;
}
export default function PageLayout({ children, className = "" }: PageLayoutProps) {
return (
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] flex items-center justify-center px-4 py-8 ${className}`}>
<div className="w-full max-w-2xl">
<div className="bg-white rounded-2xl shadow-xl p-6 md:p-8">
{children}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import PlainButton, { ButtonType } from "./PlainButton";
interface GrayButtonProps {
onClick?: () => void;
children: React.ReactNode;
className?: string;
selected?: boolean;
type?: ButtonType;
disabled?: boolean;
}
export default function GrayButton({
onClick,
children,
className = "",
selected = false,
type = "button",
disabled = false,
}: GrayButtonProps) {
return (
<PlainButton
onClick={onClick}
className={`px-4 py-2 rounded-full transition-colors text-sm ${
selected
? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
} ${className}`}
type={type}
disabled={disabled}
>
{children}
</PlainButton>
);
}

View File

@@ -0,0 +1,28 @@
import PlainButton, { ButtonType } from "./PlainButton";
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 bg-[#35786f] text-white rounded-full hover:bg-[#2d5f58] transition-colors text-sm font-medium ${className}`}
type={type}
disabled={disabled}
>
{children}
</PlainButton>
);
}

View File

@@ -0,0 +1,28 @@
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>
);
}