This commit is contained in:
@@ -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
|
||||
|
||||
17
package.json
17
package.json
@@ -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
6429
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
71
prisma/migrations/20251204102820_init/migration.sql
Normal file
71
prisma/migrations/20251204102820_init/migration.sql
Normal 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;
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal 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"
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)} />;
|
||||
}
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)} />;
|
||||
}
|
||||
|
||||
@@ -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
30
src/auth.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
48
src/lib/actions/services/pairService.ts
Normal file
48
src/lib/actions/services/pairService.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
28
src/lib/actions/services/userService.ts
Normal file
28
src/lib/actions/services/userService.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user