...
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Container from "@/components/ui/Container";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Center } from "@/components/common/Center";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { Folder } from "../../../../generated/prisma/browser";
|
||||
@@ -15,22 +13,29 @@ interface FolderSelectorProps {
|
||||
const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
|
||||
const t = useTranslations("memorize.folder_selector");
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Container className="p-6 gap-4 flex flex-col">
|
||||
{(folders.length === 0 && (
|
||||
<h1 className="text-2xl text-gray-900 font-light">
|
||||
<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="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
{folders.length === 0 ? (
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-4">
|
||||
{t("noFolders")}
|
||||
<Link className="text-blue-900 border-b" href={"/folders"}>
|
||||
folders
|
||||
</Link>
|
||||
</h1>
|
||||
)) || (
|
||||
<Link
|
||||
className="inline-block px-6 py-2 bg-[#35786f] text-white rounded-full hover:bg-[#2d5f58] transition-colors"
|
||||
href="/folders"
|
||||
>
|
||||
Go to Folders
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h1 className="text-2xl text-gray-900 font-light">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-6">
|
||||
{t("selectFolder")}
|
||||
</h1>
|
||||
<div className="text-gray-900 border border-gray-200 rounded-2xl max-h-96 overflow-y-auto">
|
||||
<div className="border border-gray-200 rounded-2xl max-h-96 overflow-y-auto">
|
||||
{folders
|
||||
.toSorted((a, b) => a.id - b.id)
|
||||
.map((folder) => (
|
||||
@@ -39,25 +44,46 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
|
||||
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"
|
||||
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"
|
||||
>
|
||||
<Fd />
|
||||
<div className="flex-1 flex gap-2">
|
||||
<span className="group-hover:text-blue-500">
|
||||
<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,
|
||||
})}
|
||||
</span>
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
</Center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||
import { getTTSAudioUrl } from "@/lib/browser/tts";
|
||||
import { VOICES } from "@/config/locales";
|
||||
@@ -28,7 +27,13 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
||||
const { load, play } = useAudioPlayer();
|
||||
|
||||
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);
|
||||
@@ -38,14 +43,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
||||
|
||||
const getTextPairs = () => disorder ? disorderedTextPairs : textPairs;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(getTextPairs().length > 0 && (
|
||||
<>
|
||||
<div className="text-center">
|
||||
<div
|
||||
className="text-sm text-gray-500"
|
||||
onClick={() => {
|
||||
const handleIndexClick = () => {
|
||||
const newIndex = prompt("Input a index number.")?.trim();
|
||||
if (
|
||||
newIndex &&
|
||||
@@ -55,57 +53,9 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
||||
) {
|
||||
setIndex(parseInt(newIndex) - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
{"/" + getTextPairs().length}
|
||||
</div>
|
||||
<div className={`h-[40dvh] md:px-16 px-4 ${myFont.className}`}>
|
||||
{(() => {
|
||||
const createText = (text: string) => {
|
||||
return (
|
||||
<div className="text-gray-900 text-xl border-y border-y-gray-200 p-4 md:text-3xl h-[20dvh] overflow-y-auto">
|
||||
{text}
|
||||
</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 {
|
||||
// non-dictation
|
||||
if (show === "question") {
|
||||
return createText(text1);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{createText(text1)}
|
||||
{createText(text2)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 items-center justify-center flex-wrap">
|
||||
<LightButton
|
||||
className="w-20"
|
||||
onClick={async () => {
|
||||
const handleNext = async () => {
|
||||
if (show === "answer") {
|
||||
const newIndex = (index + 1) % getTextPairs().length;
|
||||
setIndex(newIndex);
|
||||
@@ -125,48 +75,128 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
||||
});
|
||||
}
|
||||
setShow(show === "question" ? "answer" : "question");
|
||||
}}
|
||||
>
|
||||
{show === "question" ? t("answer") : t("next")}
|
||||
</LightButton>
|
||||
<LightButton
|
||||
onClick={() => {
|
||||
};
|
||||
|
||||
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 (
|
||||
<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="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
{/* 进度指示器 */}
|
||||
<div className="flex justify-center mb-4">
|
||||
<button
|
||||
onClick={handleIndexClick}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
{index + 1} / {getTextPairs().length}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 文本显示区域 */}
|
||||
<div className={`h-[40dvh] ${myFont.className} mb-4`}>
|
||||
{(() => {
|
||||
if (dictation) {
|
||||
if (show === "question") {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-gray-400 text-4xl">?</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{createText(text1)}
|
||||
<div className="border-t border-gray-200"></div>
|
||||
{createText(text2)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
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 className="flex flex-row gap-2 items-center justify-center flex-wrap">
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="px-4 py-2 rounded-full bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors text-sm"
|
||||
>
|
||||
{show === "question" ? t("answer") : t("next")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrevious}
|
||||
className="px-4 py-2 rounded-full bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors text-sm"
|
||||
>
|
||||
{t("previous")}
|
||||
</LightButton>
|
||||
<LightButton
|
||||
onClick={() => {
|
||||
setReverse(!reverse);
|
||||
}}
|
||||
selected={reverse}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleReverse}
|
||||
className={`px-4 py-2 rounded-full transition-colors text-sm ${
|
||||
reverse
|
||||
? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
|
||||
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
{t("reverse")}
|
||||
</LightButton>
|
||||
<LightButton
|
||||
onClick={() => {
|
||||
setDictation(!dictation);
|
||||
}}
|
||||
selected={dictation}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleDictation}
|
||||
className={`px-4 py-2 rounded-full transition-colors text-sm ${
|
||||
dictation
|
||||
? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
|
||||
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
{t("dictation")}
|
||||
</LightButton>
|
||||
<LightButton
|
||||
onClick={() => {
|
||||
setDisorder(!disorder);
|
||||
}}
|
||||
selected={disorder}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleDisorder}
|
||||
className={`px-4 py-2 rounded-full transition-colors text-sm ${
|
||||
disorder
|
||||
? "bg-[#35786f] text-white hover:bg-[#2d5f58]"
|
||||
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||
}`}
|
||||
>
|
||||
{t("disorder")}
|
||||
</LightButton>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)) || <p>{t("noTextPairs")}</p>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Center } from "@/components/common/Center";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Folder } from "../../../generated/prisma/browser";
|
||||
import {
|
||||
@@ -19,6 +18,9 @@ import {
|
||||
} from "@/lib/server/services/folderService";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import PageLayout from "@/components/ui/PageLayout";
|
||||
import PageHeader from "@/components/ui/PageHeader";
|
||||
import CardList from "@/components/ui/CardList";
|
||||
|
||||
interface FolderProps {
|
||||
folder: Folder & { total: number };
|
||||
@@ -27,8 +29,8 @@ interface FolderProps {
|
||||
|
||||
const FolderCard = ({ folder, refresh }: FolderProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const t = useTranslations("folders");
|
||||
|
||||
return (
|
||||
<div
|
||||
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="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">
|
||||
<Fd></Fd>
|
||||
<div className="shrink-0">
|
||||
<Fd className="text-gray-600" size={24} />
|
||||
</div>
|
||||
|
||||
<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", {
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
totalPairs: folder.total,
|
||||
})}
|
||||
</h3>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-400">#{folder.id}</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
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -63,7 +64,7 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
|
||||
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} />
|
||||
</button>
|
||||
@@ -113,13 +114,10 @@ export default function FoldersClient({ userId }: { userId: string }) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<div className="w-full max-w-2xl mx-auto bg-white border border-gray-200 rounded-2xl p-6">
|
||||
<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>
|
||||
<PageLayout>
|
||||
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
||||
|
||||
<button
|
||||
onClick={async () => {
|
||||
@@ -143,10 +141,11 @@ export default function FoldersClient({ userId }: { userId: string }) {
|
||||
<span>{loading ? t("creating") : t("newFolder")}</span>
|
||||
</button>
|
||||
|
||||
<div className="mt-4 max-h-96 overflow-y-auto">
|
||||
<div className="mt-4">
|
||||
<CardList>
|
||||
{folders.length === 0 ? (
|
||||
<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" />
|
||||
</div>
|
||||
<p className="text-sm">{t("noFoldersYet")}</p>
|
||||
@@ -164,8 +163,8 @@ export default function FoldersClient({ userId }: { userId: string }) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardList>
|
||||
</div>
|
||||
</div>
|
||||
</Center>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, Plus } from "lucide-react";
|
||||
import { Center } from "@/components/common/Center";
|
||||
import { useEffect, useState } from "react";
|
||||
import { redirect, useRouter } from "next/navigation";
|
||||
import Container from "@/components/ui/Container";
|
||||
import {
|
||||
createPair,
|
||||
deletePairById,
|
||||
@@ -12,8 +10,11 @@ import {
|
||||
} from "@/lib/server/services/pairService";
|
||||
import AddTextPairModal from "./AddTextPairModal";
|
||||
import TextPairCard from "./TextPairCard";
|
||||
import LightButton from "@/components/ui/buttons/LightButton";
|
||||
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 {
|
||||
id: number;
|
||||
@@ -55,8 +56,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Container className="p-6">
|
||||
<PageLayout>
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={router.back}
|
||||
@@ -68,38 +68,33 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<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")}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("itemsCount", { count: textPairs.length })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<LightButton
|
||||
<GreenButton
|
||||
onClick={() => {
|
||||
redirect(`/memorize?folder_id=${folderId}`);
|
||||
}}
|
||||
>
|
||||
{t("memorize")}
|
||||
</LightButton>
|
||||
<button
|
||||
className="p-2 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
</GreenButton>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setAddModal(true);
|
||||
}}
|
||||
>
|
||||
<Plus
|
||||
size={18}
|
||||
className="text-gray-600 hover:cursor-pointer"
|
||||
icon={<Plus size={18} className="text-gray-700" />}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-96 overflow-y-auto rounded-xl border border-gray-200 overflow-hidden">
|
||||
<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>
|
||||
@@ -126,8 +121,8 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</CardList>
|
||||
|
||||
<AddTextPairModal
|
||||
isOpen={openAddModal}
|
||||
onClose={() => setAddModal(false)}
|
||||
@@ -151,6 +146,6 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
||||
refreshTextPairs();
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
12
src/components/ui/CardList.tsx
Normal file
12
src/components/ui/CardList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
src/components/ui/PageHeader.tsx
Normal file
17
src/components/ui/PageHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
src/components/ui/PageLayout.tsx
Normal file
16
src/components/ui/PageLayout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
src/components/ui/buttons/GrayButton.tsx
Normal file
34
src/components/ui/buttons/GrayButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/components/ui/buttons/GreenButton.tsx
Normal file
28
src/components/ui/buttons/GreenButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/components/ui/buttons/IconButton.tsx
Normal file
28
src/components/ui/buttons/IconButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user