Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #26

Merged
merged 6 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ max_line_length = 120
trim_trailing_whitespace = true
indent_style = tab
indent_size = 2

[*.yml, *.yaml]
indent_size = 2
indent_style = space
1 change: 0 additions & 1 deletion app/(home)/learn/[...doc_path]/loading.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { MDContentWrapper, TOCWrapper } from "./_components/markdown-wrapper";
import { Toc } from "./_components/toc";

export default function Loading() {
return (
Expand Down
12 changes: 9 additions & 3 deletions app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { HomeStatus } from "@/components/home-status";
import type { Dicts } from "@/types/dict";
import { Dicts, dictNameList } from "@/types/dict";

import { fetchDict } from "@/utils/api";

export default async function HomePage(props: {
searchParams: Promise<{ dict?: Dicts }>;
searchParams: Promise<{ dict?: string }>;
}) {
const searchParams = await props.searchParams;
const targetDict = searchParams.dict;
const targetDict =
searchParams &&
dictNameList.includes(searchParams.dict as unknown as Dicts) &&
searchParams.dict !== Dicts.user
? (searchParams.dict as unknown as Dicts)
: Dicts.popular;

const dict = await fetchDict(targetDict);
return (
<main className="w-full flex flex-col items-center justify-center">
Expand Down
20 changes: 20 additions & 0 deletions app/actions/generate-word-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use server";
import { isDev } from "@/utils/is-dev";
import { WORD_EXAMPLE } from "@/utils/user-dict";
import { fetchChatCompletion } from "../../scripts/open-ai";

export const generateWordAction = async (word: string) => {
const prompt = `参考该 JSON 信息
${JSON.stringify(WORD_EXAMPLE)}
现有单词【${word}】,若单词不是韩文请先根据该单词转换成韩文,单词不需要任何标点,然后以该韩语单词作为 JSON 中的 name 字段,trans为单词释义、example为单词例句(可以使用活用后的单词)、exTrans为例句释义,生成 JSON 字符串,其他字段也应补充完整,仅返回对应的 JSON 字符串不要添加任何其他内容。
`;
isDev && console.log("[generateWordAction][prompt]:", prompt);
const result = await fetchChatCompletion([
{
role: "user",
content: prompt,
},
]);
isDev && console.log("[generateWordAction][result]:", result);
return result?.match(/([\[\{][\s\S]*[\}\]])/)?.[1];
};
9 changes: 9 additions & 0 deletions app/assets/svg/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions app/assets/svg/download.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions app/assets/svg/file-import.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions app/components/header/_component/mobile-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";
import CloseIcon from "@/assets/svg/close.svg";
import MenuIcon from "@/assets/svg/menu.svg";
import clsx from "clsx";
import type { Session } from "next-auth";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
import type { headerConfig } from "..";

const MobileMenu = ({
links,
session,
}: { links: ReturnType<typeof headerConfig>; session: Session | null }) => {
const pathname = usePathname();
const isActive = (href: string) => pathname.includes(href);
const [isOpen, setIsOpen] = useState(false);

const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsOpen(e.target.checked);
};

return (
<>
<label className="btn btn-circle btn-ghost swap swap-rotate">
<input
type="checkbox"
className="hidden"
onChange={handleOnChange}
checked={isOpen}
/>
<MenuIcon className="swap-off fill-current w-8 h-8" />
<CloseIcon className="swap-on fill-current w-8 h-8" />
</label>
<div
className={clsx(
"absolute w-screen h-[calc(100vh-var(--header-height))] left-0 top-[--header-height] flex flex-col backdrop-blur-lg p-2",
!isOpen && "hidden",
)}
>
{links.map(({ href, label }) => (
<Link
key={href}
href={href}
onClick={() => setIsOpen(false)}
className={clsx(
isActive(href) && "bg-slate-400/40",
"p-2 rounded-md cursor-pointer hover:bg-slate-400/40",
)}
>
<span>{label}</span>
</Link>
))}
{session ? (
<div className="p-2">
<span>{session.user?.name}</span>
<Link className="ml-4" href="/api/auth/signout">
Signout
</Link>
</div>
) : (
<Link className="p-2" href="/api/auth/signin">
Sign In
</Link>
)}
</div>
</>
);
};

export { MobileMenu };
8 changes: 6 additions & 2 deletions app/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getServerI18n } from "@/utils/i18n";
import { auth } from "auth";
import Link from "next/link";
import { ActiveLinks } from "./_component/active-links";
import { MobileMenu } from "./_component/mobile-menu";
import { Progress } from "./_component/progress";

export const headerConfig = (t: Awaited<ReturnType<typeof getServerI18n>>) => [
Expand All @@ -29,12 +30,12 @@ const Header = async () => {
const t = await getServerI18n("Header");

return (
<header className="sticky top-0 h-[--header-height] flex border-b border-slate-900/10 w-full backdrop-blur-lg select-none bg-slate-300/10 text-base-content z-10">
<header className="before-backdrop-shadow sticky top-0 h-[--header-height] flex border-b border-slate-900/10 w-full select-none bg-slate-300/10 text-base-content z-20 before:">
<div className="w-full px-4 flex justify-between items-center">
<Link href="/">
<HomeIcon width={32} height={32} viewBox="0 0 24 24" />
</Link>
<div className="flex items-center">
<div className="flex items-center mobile:hidden">
{<ActiveLinks links={headerConfig(t)} />}
<span>
{session ? (
Expand All @@ -49,6 +50,9 @@ const Header = async () => {
)}
</span>
</div>
<div className="hidden mobile:block">
<MobileMenu links={headerConfig(t)} session={session} />
</div>
</div>
<Progress />
</header>
Expand Down
79 changes: 76 additions & 3 deletions app/components/home-drawer/dict-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,106 @@
"use client";
import { generateWordAction } from "@/actions/generate-word-action";
import DownloadIcon from "@/assets/svg/download.svg";
import FileImportIcon from "@/assets/svg/file-import.svg";
import ShuffleIcon from "@/assets/svg/shuffle.svg";
import { Dicts } from "@/types/dict";
import { createToast } from "@/hooks/use-toast";
import { type DictItem, Dicts } from "@/types/dict";
import { addUserDict, downLoadDict, importDict } from "@/utils/user-dict";
import { useTranslations } from "next-intl";
import { useRouter, useSearchParams } from "next/navigation";

const DictMenu = ({
onShuffle,
onUserDictUpdate,
}: {
onShuffle?: () => void;
onUserDictUpdate?: () => void;
}) => {
const searchParams = useSearchParams();
const router = useRouter();
const tIndex = useTranslations("Dict");
const currentDict = searchParams.get("dict") || Dicts.popular;
const isUserDict = currentDict === Dicts.user;

const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
router.push(`/?dict=${e.target.value}`);
};

const createWord = async () => {
// TODO: intl
const word = prompt("Enter new word", "좋아요");
if (word) {
const removeInfoToast = createToast({
type: "info",
delay: 60 * 1000 * 5,
message: (
<span>
<span className="loading loading-spinner loading-sm" />{" "}
Generating...
</span>
),
});

try {
const result = await Promise.all(
word
.split(",")
.map(
async (w) =>
JSON.parse((await generateWordAction(w)) || "{}") as DictItem,
),
);
addUserDict(...result);
onUserDictUpdate?.();
createToast({
type: "success",
message: <span>Generated Success!</span>,
});
} catch (error) {
console.error("[createWord]:\n", error);
createToast({ type: "error", message: "Generated Failed!" });
} finally {
removeInfoToast();
}
}
};

const handleImport = async () => {
importDict(onUserDictUpdate);
};

return (
<div className="sticky top-2 z-10 bg-base-200 rounded-xl mb-3 shadow-md flex justify-between items-center p-1">
<div className="pl-4">
<div className="pl-4 flex items-center">
<ShuffleIcon
width={20}
height={20}
viewBox="0 0 24 24"
className="cursor-pointer"
className="cursor-pointer inline-block"
onClick={onShuffle}
/>
{isUserDict && (
<>
<span
onClick={createWord}
className="inline-block px-2 text-xl cursor-pointer"
>
+
</span>
<DownloadIcon
width={20}
height={20}
onClick={downLoadDict}
className="cursor-pointer inline-block mx-1"
/>
<FileImportIcon
width={20}
height={20}
onClick={handleImport}
className="cursor-pointer inline-block mx-1"
/>
</>
)}
</div>
<select
className="select select-bordered w-28 select-sm"
Expand Down
57 changes: 43 additions & 14 deletions app/components/home-drawer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import CloseIcon from "@/assets/svg/close.svg";
import { ClientOnly } from "@/components/client-only";
import type { Dict } from "@/types/dict";
import { type Dict, Dicts } from "@/types/dict";
import { getTranslation } from "@/utils/convert-input";
import { isServer } from "@/utils/is-server";
import { removeUserDict } from "@/utils/user-dict";
import { useMemoizedFn } from "ahooks";
import clsx from "clsx";
import { useLocale } from "next-intl";
import { useSearchParams } from "next/navigation";
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { DictMenu } from "./dict-menu";
Expand All @@ -14,15 +17,21 @@ const HomeDrawer = ({
curWordIndex,
onClick,
onShuffle,
onUserDictUpdate,
drawerRef,
}: {
dict: Dict;
curWordIndex: number;
onClick: (index: number) => void;
drawerRef: React.RefObject<{ open: () => void }>;
onShuffle: () => void;
onUserDictUpdate: () => void;
}) => {
const locale = useLocale();
const searchParams = useSearchParams();
const currentDict = searchParams.get("dict") || Dicts.popular;
const isUserDict = currentDict === Dicts.user;

const controllerRef = useRef<HTMLInputElement>(null);
const open = useMemoizedFn(() => {
if (controllerRef.current) {
Expand All @@ -42,7 +51,7 @@ const HomeDrawer = ({
return (
<ClientOnly>
{createPortal(
<div className="drawer drawer-end z-20">
<div className="drawer drawer-end z-30">
<input
ref={controllerRef}
id="my-drawer-4"
Expand All @@ -65,28 +74,48 @@ const HomeDrawer = ({
className="drawer-overlay"
/>
<ul className="menu bg-base-100 text-base-content min-h-full w-80 p-4">
<DictMenu onShuffle={onShuffle} />
<DictMenu
onShuffle={onShuffle}
onUserDictUpdate={onUserDictUpdate}
/>
{/* Sidebar content here */}
{dict.map((item, index) => (
<li
key={item.name}
className={clsx("cursor-pointer")}
onClick={() => onClick(index)}
className={clsx(
"cursor-pointer relative group mb-1 last:mb-0",
)}
>
<div
className={clsx({
className={clsx("block", {
active: index === curWordIndex,
})}
>
<span>
{index + 1}. {item.name}
</span>
<span
className="text-right text-nowrap overflow-hidden text-ellipsis pl-12 text-gray-400"
title={getTranslation(item, locale)}
<div
className="grid grid-flow-col"
onClick={() => onClick(index)}
>
{getTranslation(item, locale)}
</span>
<span>
{index + 1}. {item.name}
</span>
<span
className="text-right text-nowrap overflow-hidden text-ellipsis pl-12 text-gray-400"
title={getTranslation(item, locale)}
>
{getTranslation(item, locale)}
</span>
</div>
{isUserDict && dict.length > 1 && (
<div
className="absolute -top-1 -right-1 btn-circle btn btn-xs items-center justify-center opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity"
onClick={() => {
removeUserDict(item.name);
onUserDictUpdate();
}}
>
<CloseIcon className="w-4 h-4" />
</div>
)}
</div>
</li>
))}
Expand Down
Loading