今天做了好多工作啊
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-12-04 21:07:54 +08:00
parent fcc20fc2e0
commit 41005a4aac
27 changed files with 733 additions and 6294 deletions

View File

@@ -26,8 +26,8 @@ steps:
DATABASE_URL:
from_secret: database_url
commands:
- npm install prisma
- npx prisma migrate deploy
- npm install -g prisma
- prisma migrate deploy
- name: deploy
image: appleboy/drone-ssh

View File

@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"license": "GPL-3.0-only",
"type": "module",
"scripts": {
"dev": "next dev --turbopack --experimental-https",
"build": "next build --turbopack",
@@ -10,25 +11,27 @@
"lint": "eslint"
},
"dependencies": {
"@prisma/client": "^6.19.0",
"@prisma/adapter-pg": "^7.1.0",
"@prisma/client": "^7.1.0",
"bcryptjs": "^3.0.3",
"edge-tts-universal": "^1.3.2",
"edge-tts-universal": "^1.3.3",
"lucide-react": "^0.553.0",
"next": "15.5.3",
"next-auth": "^4.24.13",
"next-intl": "^4.5.2",
"next-auth": "5.0.0-beta.30",
"next-intl": "^4.5.8",
"pg": "^8.16.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"sonner": "^2.0.7",
"unstorage": "^1.17.2",
"unstorage": "^1.17.3",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/eslintrc": "^3.3.3",
"@tailwindcss/postcss": "^4.1.17",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^20.19.25",
"@types/react": "^19.2.4",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.1",
"eslint-config-next": "15.5.3",

6429
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
-- CreateTable
CREATE TABLE "text_pair" (
"id" SERIAL NOT NULL,
"locale1" VARCHAR(10) NOT NULL,
"locale2" VARCHAR(10) NOT NULL,
"text1" TEXT NOT NULL,
"text2" TEXT NOT NULL,
"folder_id" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "text_pairs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "folder" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"owner" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "folders_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "text_pair" ADD CONSTRAINT "fk_text_pairs_folder" FOREIGN KEY ("folder_id") REFERENCES "folder"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,71 @@
/*
Warnings:
- You are about to drop the `folder` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `text_pair` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "text_pair" DROP CONSTRAINT "fk_text_pairs_folder";
-- DropTable
DROP TABLE "folder";
-- DropTable
DROP TABLE "text_pair";
-- CreateTable
CREATE TABLE "pairs" (
"id" SERIAL NOT NULL,
"locale1" VARCHAR(10) NOT NULL,
"locale2" VARCHAR(10) NOT NULL,
"text1" TEXT NOT NULL,
"text2" TEXT NOT NULL,
"ipa1" TEXT,
"ipa2" TEXT,
"folder_id" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "pairs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "folders" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"user_id" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "folders_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "pairs_folder_id_idx" ON "pairs"("folder_id");
-- CreateIndex
CREATE UNIQUE INDEX "pairs_folder_id_locale1_locale2_text1_key" ON "pairs"("folder_id", "locale1", "locale2", "text1");
-- CreateIndex
CREATE INDEX "folders_user_id_idx" ON "folders"("user_id");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- AddForeignKey
ALTER TABLE "pairs" ADD CONSTRAINT "pairs_folder_id_fkey" FOREIGN KEY ("folder_id") REFERENCES "folders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "folders" ADD CONSTRAINT "folders_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@@ -5,27 +5,49 @@ generator client {
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
model text_pair {
id Int @id(map: "text_pairs_pkey") @default(autoincrement())
model Pair {
id Int @id @default(autoincrement())
locale1 String @db.VarChar(10)
locale2 String @db.VarChar(10)
text1 String
text2 String
folder_id Int
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
folders folder @relation(fields: [folder_id], references: [id], onDelete: Cascade, map: "fk_text_pairs_folder")
ipa1 String?
ipa2 String?
folderId Int @map("folder_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
folder Folder @relation(fields: [folderId], references: [id], onDelete: Cascade)
@@unique([folderId, locale1, locale2, text1])
@@index([folderId])
@@map("pairs")
}
model folder {
id Int @id(map: "folders_pkey") @default(autoincrement())
model Folder {
id Int @id @default(autoincrement())
name String
owner String
created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_at DateTime? @default(now()) @db.Timestamptz(6)
text_pair text_pair[]
userId Int @map("user_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
pairs Pair[]
@@index([userId])
@@map("folders")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
folders Folder[]
@@map("users")
}

View File

@@ -1,15 +1,15 @@
"use client";
import Container from "@/components/cards/Container";
import { folder } from "../../../../generated/prisma/client";
import { Folder } from "lucide-react";
import { useRouter } from "next/navigation";
import { Center } from "@/components/Center";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { Folder } from "../../../../generated/prisma/browser";
import { Folder as Fd } from "lucide-react";
interface FolderSelectorProps {
folders: (folder & { total_pairs: number })[];
folders: (Folder & { total: number })[];
}
const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
@@ -41,13 +41,13 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
}
className="flex flex-row justify-center items-center group p-2 gap-2 hover:cursor-pointer hover:bg-gray-50"
>
<Folder />
<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_pairs,
count: folder.total,
})}
</span>
</div>

View File

@@ -1,8 +1,5 @@
"use client";
import { Center } from "@/components/Center";
import { text_pair } from "../../../../generated/prisma/browser";
import Container from "@/components/cards/Container";
import { useEffect, useState } from "react";
import LightButton from "@/components/buttons/LightButton";
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
@@ -11,13 +8,14 @@ import { VOICES } from "@/config/locales";
import { useTranslations } from "next-intl";
import localFont from "next/font/local";
import { isNonNegativeInteger } from "@/lib/utils";
import { Pair } from "../../../../generated/prisma/browser";
const myFont = localFont({
src: "../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf",
});
interface MemorizeProps {
textPairs: text_pair[];
textPairs: Pair[];
}
const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
@@ -29,7 +27,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
const [show, setShow] = useState<"question" | "answer">("question");
const { load, play } = useAudioPlayer();
const [disorderedTextPairs, setDisorderedTextPairs] = useState<text_pair[]>(
const [disorderedTextPairs, setDisorderedTextPairs] = useState<Pair[]>(
[],
);
@@ -66,7 +64,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
{index + 1}
{"/" + getTextPairs().length}
</div>
<div className="h-[40dvh] px-16">
<div className={`h-[40dvh] md:px-16 px-4 ${myFont.className}`}>
{(() => {
const createText = (text: string) => {
return (

View File

@@ -1,24 +1,24 @@
"use server";
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import {
getFoldersWithTotalPairsByOwner,
getOwnerByFolderId,
getFoldersWithTotalPairsByUserId,
getUserIdByFolderId,
} from "@/lib/actions/services/folderService";
import { isNonNegativeInteger } from "@/lib/utils";
import FolderSelector from "./FolderSelector";
import Memorize from "./Memorize";
import { getTextPairsByFolderId } from "@/lib/actions/services/textPairService";
import { getPairsByFolderId } from "@/lib/actions/services/pairService";
import { auth } from "@/auth";
export default async function MemorizePage({
searchParams,
}: {
searchParams: Promise<{ folder_id?: string }>;
}) {
const session = await getServerSession();
const username = session?.user?.name;
const session = await auth();
const userId = session?.user?.id;
const t = await getTranslations("memorize.page");
const tParam = (await searchParams).folder_id;
@@ -28,23 +28,26 @@ export default async function MemorizePage({
: null
: null;
if (!username)
if (!userId) {
redirect(
`/login?redirect=/memorize${folder_id ? `?folder_id=${folder_id}` : ""}`,
);
}
const uid = Number(userId);
if (!folder_id) {
return (
<FolderSelector
folders={await getFoldersWithTotalPairsByOwner(username)}
folders={await getFoldersWithTotalPairsByUserId(uid)}
/>
);
}
const owner = await getOwnerByFolderId(folder_id);
if (owner !== username) {
const owner = await getUserIdByFolderId(folder_id);
if (owner !== uid) {
return <p>{t("unauthorized")}</p>;
}
return <Memorize textPairs={await getTextPairsByFolderId(folder_id)} />;
return <Memorize textPairs={await getPairsByFolderId(folder_id)} />;
}

View File

@@ -4,10 +4,10 @@ import { TranslationHistorySchema } from "@/lib/interfaces";
import { useSession } from "next-auth/react";
import { Dispatch, useEffect, useState } from "react";
import z from "zod";
import { folder } from "../../../../generated/prisma/browser";
import { getFoldersByOwner } from "@/lib/actions/services/folderService";
import { Folder } from "lucide-react";
import { createTextPair } from "@/lib/actions/services/textPairService";
import { Folder } from "../../../../generated/prisma/browser";
import { getFoldersByUserId } from "@/lib/actions/services/folderService";
import { Folder as Fd } from "lucide-react";
import { createPair } from "@/lib/actions/services/pairService";
import { toast } from "sonner";
import { useTranslations } from "next-intl";
@@ -18,13 +18,13 @@ interface AddToFolderProps {
const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
const session = useSession();
const [folders, setFolders] = useState<folder[]>([]);
const [folders, setFolders] = useState<Folder[]>([]);
const t = useTranslations("translator.add_to_folder");
const [loading, setLoading] = useState(true);
useEffect(() => {
const username = session.data!.user!.name as string;
getFoldersByOwner(username)
const userId = Number(session.data!.user!.id);
getFoldersByUserId(userId)
.then(setFolders)
.then(() => setLoading(false));
}, [session.data]);
@@ -50,12 +50,12 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
key={folder.id}
className="p-2 flex items-center justify-start hover:bg-gray-50 gap-2 hover:cursor-pointer w-full border-b border-gray-200"
onClick={() => {
createTextPair({
createPair({
text1: item.text1,
text2: item.text2,
locale1: item.locale1,
locale2: item.locale2,
folders: {
folder: {
connect: {
id: folder.id,
},
@@ -70,7 +70,7 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
});
}}
>
<Folder />
<Fd />
{t("folderInfo", { id: folder.id, name: folder.name })}
</button>
))) || <div>{t("noFolders")}</div>}

View File

@@ -1,29 +1,29 @@
import Container from "@/components/cards/Container";
import { useEffect, useState } from "react";
import { folder } from "../../../../generated/prisma/browser";
import { getFoldersByOwner } from "@/lib/actions/services/folderService";
import { Folder } from "../../../../generated/prisma/browser";
import { getFoldersByUserId } from "@/lib/actions/services/folderService";
import LightButton from "@/components/buttons/LightButton";
import { Folder } from "lucide-react";
import { Folder as Fd } from "lucide-react";
interface FolderSelectorProps {
setSelectedFolderId: (id: number) => void;
username: string;
userId: number;
cancel: () => void;
}
const FolderSelector: React.FC<FolderSelectorProps> = ({
setSelectedFolderId,
username,
userId,
cancel,
}) => {
const [loading, setLoading] = useState(false);
const [folders, setFolders] = useState<folder[]>([]);
const [folders, setFolders] = useState<Folder[]>([]);
useEffect(() => {
getFoldersByOwner(username)
getFoldersByUserId(userId)
.then(setFolders)
.then(() => setLoading(false));
}, [username]);
}, [userId]);
return (
<div
@@ -41,7 +41,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({
key={folder.id}
onClick={() => setSelectedFolderId(folder.id)}
>
<Folder />
<Fd />
{folder.id}. {folder.name}
</button>
))}

View File

@@ -21,7 +21,7 @@ import {
import { toast } from "sonner";
import FolderSelector from "./FolderSelector";
import { useSession } from "next-auth/react";
import { createTextPair } from "@/lib/actions/services/textPairService";
import { createPair } from "@/lib/actions/services/pairService";
import { shallowEqual } from "@/lib/utils";
export default function TranslatorPage() {
@@ -109,12 +109,12 @@ export default function TranslatorPage() {
}),
);
if (autoSave && autoSaveFolderId) {
createTextPair({
createPair({
text1: llmres.text1,
text2: llmres.text2,
locale1: llmres.locale1,
locale2: llmres.locale2,
folders: {
folder: {
connect: {
id: autoSaveFolderId,
},
@@ -364,7 +364,7 @@ export default function TranslatorPage() {
)}
{autoSave && !autoSaveFolderId && (
<FolderSelector
username={session.data!.user!.name as string}
userId={Number(session.data!.user!.id)}
cancel={() => setAutoSave(false)}
setSelectedFolderId={(id) => setAutoSaveFolderId(id)}
/>

View File

@@ -1,15 +1,3 @@
import NextAuth, { AuthOptions } from "next-auth";
import GithubProvider from "next-auth/providers/github";
import { handlers } from "../../../../auth";
export const authOptions: AuthOptions = {
providers: [
GithubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
],
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
export const { GET, POST } = handlers;

View File

@@ -2,7 +2,7 @@
import {
ChevronRight,
Folder,
Folder as Fd,
FolderPen,
FolderPlus,
Trash2,
@@ -10,18 +10,18 @@ import {
import { useEffect, useState } from "react";
import { Center } from "@/components/Center";
import { useRouter } from "next/navigation";
import { folder } from "../../../generated/prisma/browser";
import { Folder } from "../../../generated/prisma/browser";
import {
createFolder,
deleteFolderById,
getFoldersWithTotalPairsByOwner,
getFoldersWithTotalPairsByUserId,
renameFolderById,
} from "@/lib/actions/services/folderService";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
interface FolderProps {
folder: folder & { total_pairs: number };
folder: Folder & { total: number };
refresh: () => void;
}
@@ -38,7 +38,7 @@ 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">
<Folder></Folder>
<Fd></Fd>
</div>
<div className="flex-1">
@@ -46,7 +46,7 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
{t("folderInfo", {
id: folder.id,
name: folder.name,
totalPairs: folder.total_pairs,
totalPairs: folder.total,
})}
</h3>
</div>
@@ -85,16 +85,16 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
);
};
export default function FoldersClient({ username }: { username: string }) {
export default function FoldersClient({ userId }: { userId: number }) {
const t = useTranslations("folders");
const [folders, setFolders] = useState<(folder & { total_pairs: number })[]>(
const [folders, setFolders] = useState<(Folder & { total: number })[]>(
[],
);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
getFoldersWithTotalPairsByOwner(username)
getFoldersWithTotalPairsByUserId(userId)
.then((folders) => {
setFolders(folders);
setLoading(false);
@@ -103,11 +103,11 @@ export default function FoldersClient({ username }: { username: string }) {
console.error(error);
toast.error("加载出错,请重试。");
});
}, [username]);
}, [userId]);
const updateFolders = async () => {
try {
const updatedFolders = await getFoldersWithTotalPairsByOwner(username);
const updatedFolders = await getFoldersWithTotalPairsByUserId(userId);
setFolders(updatedFolders);
} catch (error) {
console.error(error);
@@ -129,7 +129,7 @@ export default function FoldersClient({ username }: { username: string }) {
try {
await createFolder({
name: folderName,
owner: username,
user: { connect: { id: userId } },
});
await updateFolders();
} finally {

View File

@@ -6,10 +6,10 @@ import { useEffect, useState } from "react";
import { redirect, useRouter } from "next/navigation";
import Container from "@/components/cards/Container";
import {
createTextPair,
deleteTextPairById,
getTextPairsByFolderId,
} from "@/lib/actions/services/textPairService";
createPair,
deletePairById,
getPairsByFolderId,
} from "@/lib/actions/services/pairService";
import AddTextPairModal from "./AddTextPairModal";
import TextPairCard from "./TextPairCard";
import LightButton from "@/components/buttons/LightButton";
@@ -34,7 +34,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
const fetchTextPairs = async () => {
setLoading(true);
try {
const data = await getTextPairsByFolderId(folderId);
const data = await getPairsByFolderId(folderId);
setTextPairs(data as TextPair[]);
} catch (error) {
console.error("Failed to fetch text pairs:", error);
@@ -47,7 +47,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
const refreshTextPairs = async () => {
try {
const data = await getTextPairsByFolderId(folderId);
const data = await getPairsByFolderId(folderId);
setTextPairs(data as TextPair[]);
} catch (error) {
console.error("Failed to fetch text pairs:", error);
@@ -118,7 +118,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
key={textPair.id}
textPair={textPair}
onDel={() => {
deleteTextPairById(textPair.id);
deletePairById(textPair.id);
refreshTextPairs();
}}
refreshTextPairs={refreshTextPairs}
@@ -137,12 +137,12 @@ export default function InFolder({ folderId }: { folderId: number }) {
locale1: string,
locale2: string,
) => {
await createTextPair({
await createPair({
text1: text1,
text2: text2,
locale1: locale1,
locale2: locale2,
folders: {
folder: {
connect: {
id: folderId,
},

View File

@@ -1,10 +1,10 @@
import { Edit, Trash2 } from "lucide-react";
import { TextPair } from "./InFolder";
import { updateTextPairById } from "@/lib/actions/services/textPairService";
import { updatePairById } from "@/lib/actions/services/pairService";
import { useState } from "react";
import { text_pairUpdateInput } from "../../../../generated/prisma/models";
import UpdateTextPairModal from "./UpdateTextPairModal";
import { useTranslations } from "next-intl";
import { PairUpdateInput } from "../../../../generated/prisma/models";
interface TextPairCardProps {
textPair: TextPair;
@@ -66,8 +66,8 @@ export default function TextPairCard({
<UpdateTextPairModal
isOpen={openUpdateModal}
onClose={() => setOpenUpdateModal(false)}
onUpdate={async (id: number, data: text_pairUpdateInput) => {
await updateTextPairById(id, data);
onUpdate={async (id: number, data: PairUpdateInput) => {
await updatePairById(id, data);
setOpenUpdateModal(false);
refreshTextPairs();
}}

View File

@@ -2,7 +2,7 @@ import LightButton from "@/components/buttons/LightButton";
import Input from "@/components/Input";
import { X } from "lucide-react";
import { useRef } from "react";
import { text_pairUpdateInput } from "../../../../generated/prisma/models";
import { PairUpdateInput } from "../../../../generated/prisma/models";
import { TextPair } from "./InFolder";
import { useTranslations } from "next-intl";
@@ -10,7 +10,7 @@ interface UpdateTextPairModalProps {
isOpen: boolean;
onClose: () => void;
textPair: TextPair;
onUpdate: (id: number, tp: text_pairUpdateInput) => void;
onUpdate: (id: number, tp: PairUpdateInput) => void;
}
export default function UpdateTextPairModal({

View File

@@ -1,24 +1,23 @@
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import InFolder from "./InFolder";
import { getOwnerByFolderId } from "@/lib/actions/services/folderService";
import { getUserIdByFolderId } from "@/lib/actions/services/folderService";
import { auth } from "@/auth";
export default async function FoldersPage({
params,
}: {
params: Promise<{ folder_id: number }>;
}) {
const session = await getServerSession();
const session = await auth();
const { folder_id } = await params;
const id = Number(folder_id);
const t = await getTranslations("folder_id");
if (!id) {
if (!folder_id) {
redirect("/folders");
}
if (!session?.user?.name) redirect(`/login?redirect=/folders/${id}`);
if ((await getOwnerByFolderId(id)) !== session.user.name) {
if (!session?.user?.id) redirect(`/login?redirect=/folders/${folder_id}`);
if ((await getUserIdByFolderId(Number(folder_id))) !== Number(session.user.id)) {
return <p>{t("unauthorized")}</p>;
}
return <InFolder folderId={id} />;
return <InFolder folderId={Number(folder_id)} />;
}

View File

@@ -1,8 +1,8 @@
import { auth } from "@/auth";
import FoldersClient from "./FoldersClient";
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
export default async function FoldersPage() {
const session = await getServerSession();
if (!session?.user?.name) redirect(`/login?redirect=/folders`);
return <FoldersClient username={session.user.name} />;
const session = await auth();
if (!session?.user?.id) redirect(`/login?redirect=/folders`);
return <FoldersClient userId={Number(session.user.id)} />;
}

30
src/auth.ts Normal file
View File

@@ -0,0 +1,30 @@
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import { createUserIfNotExists, getUserIdByEmail } from "./lib/actions/services/userService";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
],
callbacks: {
async signIn({ user }) {
if (!user.email) return false;
await createUserIfNotExists(user.email, user.name);
return true
},
async session({ session }) {
if (session.user?.email) {
const userId = await getUserIdByEmail(session.user.email);
if (userId) {
session.user.id = userId.toString();
}
}
return session;
},
},
});

View File

@@ -1,15 +1,12 @@
"use server";
import {
folderCreateInput,
folderUpdateInput,
} from "../../../../generated/prisma/models";
import { FolderCreateInput, FolderUpdateInput } from "../../../../generated/prisma/models";
import prisma from "../../db";
export async function getFoldersByOwner(owner: string) {
export async function getFoldersByUserId(userId: number) {
const folders = await prisma.folder.findMany({
where: {
owner: owner,
userId: userId,
},
});
return folders;
@@ -26,27 +23,23 @@ export async function renameFolderById(id: number, newName: string) {
});
}
export async function getFoldersWithTotalPairsByOwner(owner: string) {
export async function getFoldersWithTotalPairsByUserId(userId: number) {
const folders = await prisma.folder.findMany({
where: {
owner: owner,
},
where: { userId },
include: {
text_pair: {
select: {
id: true,
},
_count: {
select: { pairs: true },
},
},
});
return folders.map((folder) => ({
return folders.map(folder => ({
...folder,
total_pairs: folder.text_pair.length,
total: folder._count?.pairs ?? 0,
}));
}
export async function createFolder(folder: folderCreateInput) {
export async function createFolder(folder: FolderCreateInput) {
await prisma.folder.create({
data: folder,
});
@@ -60,7 +53,7 @@ export async function deleteFolderById(id: number) {
});
}
export async function updateFolderById(id: number, data: folderUpdateInput) {
export async function updateFolderById(id: number, data: FolderUpdateInput) {
await prisma.folder.update({
where: {
id: id,
@@ -69,11 +62,11 @@ export async function updateFolderById(id: number, data: folderUpdateInput) {
});
}
export async function getOwnerByFolderId(id: number) {
export async function getUserIdByFolderId(id: number) {
const folder = await prisma.folder.findUnique({
where: {
id: id,
},
});
return folder?.owner;
return folder?.userId;
}

View File

@@ -0,0 +1,48 @@
"use server";
import { PairCreateInput, PairUpdateInput } from "../../../../generated/prisma/models";
import prisma from "../../db";
export async function createPair(data: PairCreateInput) {
await prisma.pair.create({
data: data,
});
}
export async function deletePairById(id: number) {
await prisma.pair.delete({
where: {
id: id,
},
});
}
export async function updatePairById(
id: number,
data: PairUpdateInput,
) {
await prisma.pair.update({
where: {
id: id,
},
data: data,
});
}
export async function getPairCountByFolderId(folderId: number) {
const count = await prisma.pair.count({
where: {
folderId: folderId,
},
});
return count;
}
export async function getPairsByFolderId(folderId: number) {
const textPairs = await prisma.pair.findMany({
where: {
folderId: folderId,
},
});
return textPairs;
}

View File

@@ -1,51 +0,0 @@
"use server";
import {
text_pairCreateInput,
text_pairUpdateInput,
} from "../../../../generated/prisma/models";
import prisma from "../../db";
export async function createTextPair(data: text_pairCreateInput) {
await prisma.text_pair.create({
data: data,
});
}
export async function deleteTextPairById(id: number) {
await prisma.text_pair.delete({
where: {
id: id,
},
});
}
export async function updateTextPairById(
id: number,
data: text_pairUpdateInput,
) {
await prisma.text_pair.update({
where: {
id: id,
},
data: data,
});
}
export async function getTextPairCountByFolderId(folderId: number) {
const count = await prisma.text_pair.count({
where: {
folder_id: folderId,
},
});
return count;
}
export async function getTextPairsByFolderId(folderId: number) {
const textPairs = await prisma.text_pair.findMany({
where: {
folder_id: folderId,
},
});
return textPairs;
}

View File

@@ -0,0 +1,28 @@
import prisma from "@/lib/db";
import { UserCreateInput } from "../../../../generated/prisma/models";
export async function createUserIfNotExists(email: string, name?: string | null) {
const user = await prisma.user.upsert({
where: {
email: email,
},
update: {},
create: {
email: email,
name: name || "New User",
} as UserCreateInput,
});
return user;
}
export async function getUserIdByEmail(email: string) {
const user = await prisma.user.findUnique({
where: {
email: email,
},
select: {
id: true,
},
});
return user ? user.id : null;
}

View File

@@ -1,4 +1,10 @@
import { PrismaClient } from "../../generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
const prisma = new PrismaClient();
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});
const prisma = new PrismaClient({
adapter: adapter,
});
export default prisma;

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"target": "es2023",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -19,9 +23,18 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
],
"exclude": [
"node_modules"
]
}