This commit is contained in:
@@ -26,8 +26,8 @@ steps:
|
|||||||
DATABASE_URL:
|
DATABASE_URL:
|
||||||
from_secret: database_url
|
from_secret: database_url
|
||||||
commands:
|
commands:
|
||||||
- npm install prisma
|
- npm install -g prisma
|
||||||
- npx prisma migrate deploy
|
- prisma migrate deploy
|
||||||
|
|
||||||
- name: deploy
|
- name: deploy
|
||||||
image: appleboy/drone-ssh
|
image: appleboy/drone-ssh
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack --experimental-https",
|
"dev": "next dev --turbopack --experimental-https",
|
||||||
"build": "next build --turbopack",
|
"build": "next build --turbopack",
|
||||||
@@ -10,25 +11,27 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.19.0",
|
"@prisma/adapter-pg": "^7.1.0",
|
||||||
|
"@prisma/client": "^7.1.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"edge-tts-universal": "^1.3.2",
|
"edge-tts-universal": "^1.3.3",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"next-auth": "^4.24.13",
|
"next-auth": "5.0.0-beta.30",
|
||||||
"next-intl": "^4.5.2",
|
"next-intl": "^4.5.8",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"unstorage": "^1.17.2",
|
"unstorage": "^1.17.3",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20.19.25",
|
"@types/node": "^20.19.25",
|
||||||
"@types/react": "^19.2.4",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-next": "15.5.3",
|
"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 {
|
datasource db {
|
||||||
provider = "postgresql"
|
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 Pair {
|
||||||
model text_pair {
|
id Int @id @default(autoincrement())
|
||||||
id Int @id(map: "text_pairs_pkey") @default(autoincrement())
|
locale1 String @db.VarChar(10)
|
||||||
locale1 String @db.VarChar(10)
|
locale2 String @db.VarChar(10)
|
||||||
locale2 String @db.VarChar(10)
|
text1 String
|
||||||
text1 String
|
text2 String
|
||||||
text2 String
|
ipa1 String?
|
||||||
folder_id Int
|
ipa2 String?
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
folderId Int @map("folder_id")
|
||||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
folders folder @relation(fields: [folder_id], references: [id], onDelete: Cascade, map: "fk_text_pairs_folder")
|
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 {
|
model Folder {
|
||||||
id Int @id(map: "folders_pkey") @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
owner String
|
userId Int @map("user_id")
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
text_pair text_pair[]
|
|
||||||
|
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";
|
"use client";
|
||||||
|
|
||||||
import Container from "@/components/cards/Container";
|
import Container from "@/components/cards/Container";
|
||||||
import { folder } from "../../../../generated/prisma/client";
|
|
||||||
import { Folder } from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Center } from "@/components/Center";
|
import { Center } from "@/components/Center";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Folder } from "../../../../generated/prisma/browser";
|
||||||
|
import { Folder as Fd } from "lucide-react";
|
||||||
|
|
||||||
interface FolderSelectorProps {
|
interface FolderSelectorProps {
|
||||||
folders: (folder & { total_pairs: number })[];
|
folders: (Folder & { total: number })[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
|
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"
|
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">
|
<div className="flex-1 flex gap-2">
|
||||||
<span className="group-hover:text-blue-500">
|
<span className="group-hover:text-blue-500">
|
||||||
{t("folderInfo", {
|
{t("folderInfo", {
|
||||||
id: folder.id,
|
id: folder.id,
|
||||||
name: folder.name,
|
name: folder.name,
|
||||||
count: folder.total_pairs,
|
count: folder.total,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
"use client";
|
"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 { useEffect, useState } from "react";
|
||||||
import LightButton from "@/components/buttons/LightButton";
|
import LightButton from "@/components/buttons/LightButton";
|
||||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||||
@@ -11,13 +8,14 @@ import { VOICES } from "@/config/locales";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import { isNonNegativeInteger } from "@/lib/utils";
|
import { isNonNegativeInteger } from "@/lib/utils";
|
||||||
|
import { Pair } from "../../../../generated/prisma/browser";
|
||||||
|
|
||||||
const myFont = localFont({
|
const myFont = localFont({
|
||||||
src: "../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf",
|
src: "../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf",
|
||||||
});
|
});
|
||||||
|
|
||||||
interface MemorizeProps {
|
interface MemorizeProps {
|
||||||
textPairs: text_pair[];
|
textPairs: Pair[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
||||||
@@ -29,7 +27,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
|||||||
const [show, setShow] = useState<"question" | "answer">("question");
|
const [show, setShow] = useState<"question" | "answer">("question");
|
||||||
const { load, play } = useAudioPlayer();
|
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}
|
{index + 1}
|
||||||
{"/" + getTextPairs().length}
|
{"/" + getTextPairs().length}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[40dvh] px-16">
|
<div className={`h-[40dvh] md:px-16 px-4 ${myFont.className}`}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const createText = (text: string) => {
|
const createText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getServerSession } from "next-auth";
|
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import {
|
import {
|
||||||
getFoldersWithTotalPairsByOwner,
|
getFoldersWithTotalPairsByUserId,
|
||||||
getOwnerByFolderId,
|
getUserIdByFolderId,
|
||||||
} from "@/lib/actions/services/folderService";
|
} from "@/lib/actions/services/folderService";
|
||||||
import { isNonNegativeInteger } from "@/lib/utils";
|
import { isNonNegativeInteger } from "@/lib/utils";
|
||||||
import FolderSelector from "./FolderSelector";
|
import FolderSelector from "./FolderSelector";
|
||||||
import Memorize from "./Memorize";
|
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({
|
export default async function MemorizePage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ folder_id?: string }>;
|
searchParams: Promise<{ folder_id?: string }>;
|
||||||
}) {
|
}) {
|
||||||
const session = await getServerSession();
|
const session = await auth();
|
||||||
const username = session?.user?.name;
|
const userId = session?.user?.id;
|
||||||
const t = await getTranslations("memorize.page");
|
const t = await getTranslations("memorize.page");
|
||||||
|
|
||||||
const tParam = (await searchParams).folder_id;
|
const tParam = (await searchParams).folder_id;
|
||||||
@@ -28,23 +28,26 @@ export default async function MemorizePage({
|
|||||||
: null
|
: null
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!username)
|
if (!userId) {
|
||||||
redirect(
|
redirect(
|
||||||
`/login?redirect=/memorize${folder_id ? `?folder_id=${folder_id}` : ""}`,
|
`/login?redirect=/memorize${folder_id ? `?folder_id=${folder_id}` : ""}`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uid = Number(userId);
|
||||||
|
|
||||||
if (!folder_id) {
|
if (!folder_id) {
|
||||||
return (
|
return (
|
||||||
<FolderSelector
|
<FolderSelector
|
||||||
folders={await getFoldersWithTotalPairsByOwner(username)}
|
folders={await getFoldersWithTotalPairsByUserId(uid)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const owner = await getOwnerByFolderId(folder_id);
|
const owner = await getUserIdByFolderId(folder_id);
|
||||||
if (owner !== username) {
|
if (owner !== uid) {
|
||||||
return <p>{t("unauthorized")}</p>;
|
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 { useSession } from "next-auth/react";
|
||||||
import { Dispatch, useEffect, useState } from "react";
|
import { Dispatch, useEffect, useState } from "react";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { folder } from "../../../../generated/prisma/browser";
|
import { Folder } from "../../../../generated/prisma/browser";
|
||||||
import { getFoldersByOwner } from "@/lib/actions/services/folderService";
|
import { getFoldersByUserId } from "@/lib/actions/services/folderService";
|
||||||
import { Folder } from "lucide-react";
|
import { Folder as Fd } from "lucide-react";
|
||||||
import { createTextPair } from "@/lib/actions/services/textPairService";
|
import { createPair } from "@/lib/actions/services/pairService";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
@@ -18,13 +18,13 @@ interface AddToFolderProps {
|
|||||||
|
|
||||||
const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
|
const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const [folders, setFolders] = useState<folder[]>([]);
|
const [folders, setFolders] = useState<Folder[]>([]);
|
||||||
const t = useTranslations("translator.add_to_folder");
|
const t = useTranslations("translator.add_to_folder");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const username = session.data!.user!.name as string;
|
const userId = Number(session.data!.user!.id);
|
||||||
getFoldersByOwner(username)
|
getFoldersByUserId(userId)
|
||||||
.then(setFolders)
|
.then(setFolders)
|
||||||
.then(() => setLoading(false));
|
.then(() => setLoading(false));
|
||||||
}, [session.data]);
|
}, [session.data]);
|
||||||
@@ -50,12 +50,12 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
|
|||||||
key={folder.id}
|
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"
|
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={() => {
|
onClick={() => {
|
||||||
createTextPair({
|
createPair({
|
||||||
text1: item.text1,
|
text1: item.text1,
|
||||||
text2: item.text2,
|
text2: item.text2,
|
||||||
locale1: item.locale1,
|
locale1: item.locale1,
|
||||||
locale2: item.locale2,
|
locale2: item.locale2,
|
||||||
folders: {
|
folder: {
|
||||||
connect: {
|
connect: {
|
||||||
id: folder.id,
|
id: folder.id,
|
||||||
},
|
},
|
||||||
@@ -70,7 +70,7 @@ const AddToFolder: React.FC<AddToFolderProps> = ({ item, setShow }) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Folder />
|
<Fd />
|
||||||
{t("folderInfo", { id: folder.id, name: folder.name })}
|
{t("folderInfo", { id: folder.id, name: folder.name })}
|
||||||
</button>
|
</button>
|
||||||
))) || <div>{t("noFolders")}</div>}
|
))) || <div>{t("noFolders")}</div>}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import Container from "@/components/cards/Container";
|
import Container from "@/components/cards/Container";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { folder } from "../../../../generated/prisma/browser";
|
import { Folder } from "../../../../generated/prisma/browser";
|
||||||
import { getFoldersByOwner } from "@/lib/actions/services/folderService";
|
import { getFoldersByUserId } from "@/lib/actions/services/folderService";
|
||||||
import LightButton from "@/components/buttons/LightButton";
|
import LightButton from "@/components/buttons/LightButton";
|
||||||
import { Folder } from "lucide-react";
|
import { Folder as Fd } from "lucide-react";
|
||||||
|
|
||||||
interface FolderSelectorProps {
|
interface FolderSelectorProps {
|
||||||
setSelectedFolderId: (id: number) => void;
|
setSelectedFolderId: (id: number) => void;
|
||||||
username: string;
|
userId: number;
|
||||||
cancel: () => void;
|
cancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FolderSelector: React.FC<FolderSelectorProps> = ({
|
const FolderSelector: React.FC<FolderSelectorProps> = ({
|
||||||
setSelectedFolderId,
|
setSelectedFolderId,
|
||||||
username,
|
userId,
|
||||||
cancel,
|
cancel,
|
||||||
}) => {
|
}) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [folders, setFolders] = useState<folder[]>([]);
|
const [folders, setFolders] = useState<Folder[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getFoldersByOwner(username)
|
getFoldersByUserId(userId)
|
||||||
.then(setFolders)
|
.then(setFolders)
|
||||||
.then(() => setLoading(false));
|
.then(() => setLoading(false));
|
||||||
}, [username]);
|
}, [userId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -41,7 +41,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({
|
|||||||
key={folder.id}
|
key={folder.id}
|
||||||
onClick={() => setSelectedFolderId(folder.id)}
|
onClick={() => setSelectedFolderId(folder.id)}
|
||||||
>
|
>
|
||||||
<Folder />
|
<Fd />
|
||||||
{folder.id}. {folder.name}
|
{folder.id}. {folder.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import FolderSelector from "./FolderSelector";
|
import FolderSelector from "./FolderSelector";
|
||||||
import { useSession } from "next-auth/react";
|
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";
|
import { shallowEqual } from "@/lib/utils";
|
||||||
|
|
||||||
export default function TranslatorPage() {
|
export default function TranslatorPage() {
|
||||||
@@ -109,12 +109,12 @@ export default function TranslatorPage() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (autoSave && autoSaveFolderId) {
|
if (autoSave && autoSaveFolderId) {
|
||||||
createTextPair({
|
createPair({
|
||||||
text1: llmres.text1,
|
text1: llmres.text1,
|
||||||
text2: llmres.text2,
|
text2: llmres.text2,
|
||||||
locale1: llmres.locale1,
|
locale1: llmres.locale1,
|
||||||
locale2: llmres.locale2,
|
locale2: llmres.locale2,
|
||||||
folders: {
|
folder: {
|
||||||
connect: {
|
connect: {
|
||||||
id: autoSaveFolderId,
|
id: autoSaveFolderId,
|
||||||
},
|
},
|
||||||
@@ -128,10 +128,10 @@ export default function TranslatorPage() {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error(
|
toast.error(
|
||||||
llmres.text1 +
|
llmres.text1 +
|
||||||
"保存到文件夹" +
|
"保存到文件夹" +
|
||||||
autoSaveFolderId +
|
autoSaveFolderId +
|
||||||
"失败:" +
|
"失败:" +
|
||||||
error.message,
|
error.message,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -364,7 +364,7 @@ export default function TranslatorPage() {
|
|||||||
)}
|
)}
|
||||||
{autoSave && !autoSaveFolderId && (
|
{autoSave && !autoSaveFolderId && (
|
||||||
<FolderSelector
|
<FolderSelector
|
||||||
username={session.data!.user!.name as string}
|
userId={Number(session.data!.user!.id)}
|
||||||
cancel={() => setAutoSave(false)}
|
cancel={() => setAutoSave(false)}
|
||||||
setSelectedFolderId={(id) => setAutoSaveFolderId(id)}
|
setSelectedFolderId={(id) => setAutoSaveFolderId(id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
import NextAuth, { AuthOptions } from "next-auth";
|
import { handlers } from "../../../../auth";
|
||||||
import GithubProvider from "next-auth/providers/github";
|
|
||||||
|
|
||||||
export const authOptions: AuthOptions = {
|
export const { GET, POST } = handlers;
|
||||||
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 };
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Folder,
|
Folder as Fd,
|
||||||
FolderPen,
|
FolderPen,
|
||||||
FolderPlus,
|
FolderPlus,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -10,18 +10,18 @@ import {
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Center } from "@/components/Center";
|
import { Center } from "@/components/Center";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { folder } from "../../../generated/prisma/browser";
|
import { Folder } from "../../../generated/prisma/browser";
|
||||||
import {
|
import {
|
||||||
createFolder,
|
createFolder,
|
||||||
deleteFolderById,
|
deleteFolderById,
|
||||||
getFoldersWithTotalPairsByOwner,
|
getFoldersWithTotalPairsByUserId,
|
||||||
renameFolderById,
|
renameFolderById,
|
||||||
} from "@/lib/actions/services/folderService";
|
} from "@/lib/actions/services/folderService";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface FolderProps {
|
interface FolderProps {
|
||||||
folder: folder & { total_pairs: number };
|
folder: Folder & { total: number };
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 flex-1">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
<div className="w-10 h-10 rounded-lg bg-linear-to-br from-blue-50 to-blue-100 flex items-center justify-center group-hover:from-blue-100 group-hover:to-blue-200 transition-colors">
|
<div className="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>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -46,7 +46,7 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
|
|||||||
{t("folderInfo", {
|
{t("folderInfo", {
|
||||||
id: folder.id,
|
id: folder.id,
|
||||||
name: folder.name,
|
name: folder.name,
|
||||||
totalPairs: folder.total_pairs,
|
totalPairs: folder.total,
|
||||||
})}
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</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 t = useTranslations("folders");
|
||||||
const [folders, setFolders] = useState<(folder & { total_pairs: number })[]>(
|
const [folders, setFolders] = useState<(Folder & { total: number })[]>(
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
getFoldersWithTotalPairsByOwner(username)
|
getFoldersWithTotalPairsByUserId(userId)
|
||||||
.then((folders) => {
|
.then((folders) => {
|
||||||
setFolders(folders);
|
setFolders(folders);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -103,11 +103,11 @@ export default function FoldersClient({ username }: { username: string }) {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
toast.error("加载出错,请重试。");
|
toast.error("加载出错,请重试。");
|
||||||
});
|
});
|
||||||
}, [username]);
|
}, [userId]);
|
||||||
|
|
||||||
const updateFolders = async () => {
|
const updateFolders = async () => {
|
||||||
try {
|
try {
|
||||||
const updatedFolders = await getFoldersWithTotalPairsByOwner(username);
|
const updatedFolders = await getFoldersWithTotalPairsByUserId(userId);
|
||||||
setFolders(updatedFolders);
|
setFolders(updatedFolders);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -129,7 +129,7 @@ export default function FoldersClient({ username }: { username: string }) {
|
|||||||
try {
|
try {
|
||||||
await createFolder({
|
await createFolder({
|
||||||
name: folderName,
|
name: folderName,
|
||||||
owner: username,
|
user: { connect: { id: userId } },
|
||||||
});
|
});
|
||||||
await updateFolders();
|
await updateFolders();
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { useEffect, useState } from "react";
|
|||||||
import { redirect, useRouter } from "next/navigation";
|
import { redirect, useRouter } from "next/navigation";
|
||||||
import Container from "@/components/cards/Container";
|
import Container from "@/components/cards/Container";
|
||||||
import {
|
import {
|
||||||
createTextPair,
|
createPair,
|
||||||
deleteTextPairById,
|
deletePairById,
|
||||||
getTextPairsByFolderId,
|
getPairsByFolderId,
|
||||||
} from "@/lib/actions/services/textPairService";
|
} from "@/lib/actions/services/pairService";
|
||||||
import AddTextPairModal from "./AddTextPairModal";
|
import AddTextPairModal from "./AddTextPairModal";
|
||||||
import TextPairCard from "./TextPairCard";
|
import TextPairCard from "./TextPairCard";
|
||||||
import LightButton from "@/components/buttons/LightButton";
|
import LightButton from "@/components/buttons/LightButton";
|
||||||
@@ -34,7 +34,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
|||||||
const fetchTextPairs = async () => {
|
const fetchTextPairs = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await getTextPairsByFolderId(folderId);
|
const data = await getPairsByFolderId(folderId);
|
||||||
setTextPairs(data as TextPair[]);
|
setTextPairs(data as TextPair[]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch text pairs:", error);
|
console.error("Failed to fetch text pairs:", error);
|
||||||
@@ -47,7 +47,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
|||||||
|
|
||||||
const refreshTextPairs = async () => {
|
const refreshTextPairs = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getTextPairsByFolderId(folderId);
|
const data = await getPairsByFolderId(folderId);
|
||||||
setTextPairs(data as TextPair[]);
|
setTextPairs(data as TextPair[]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch text pairs:", error);
|
console.error("Failed to fetch text pairs:", error);
|
||||||
@@ -118,7 +118,7 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
|||||||
key={textPair.id}
|
key={textPair.id}
|
||||||
textPair={textPair}
|
textPair={textPair}
|
||||||
onDel={() => {
|
onDel={() => {
|
||||||
deleteTextPairById(textPair.id);
|
deletePairById(textPair.id);
|
||||||
refreshTextPairs();
|
refreshTextPairs();
|
||||||
}}
|
}}
|
||||||
refreshTextPairs={refreshTextPairs}
|
refreshTextPairs={refreshTextPairs}
|
||||||
@@ -137,12 +137,12 @@ export default function InFolder({ folderId }: { folderId: number }) {
|
|||||||
locale1: string,
|
locale1: string,
|
||||||
locale2: string,
|
locale2: string,
|
||||||
) => {
|
) => {
|
||||||
await createTextPair({
|
await createPair({
|
||||||
text1: text1,
|
text1: text1,
|
||||||
text2: text2,
|
text2: text2,
|
||||||
locale1: locale1,
|
locale1: locale1,
|
||||||
locale2: locale2,
|
locale2: locale2,
|
||||||
folders: {
|
folder: {
|
||||||
connect: {
|
connect: {
|
||||||
id: folderId,
|
id: folderId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Edit, Trash2 } from "lucide-react";
|
import { Edit, Trash2 } from "lucide-react";
|
||||||
import { TextPair } from "./InFolder";
|
import { TextPair } from "./InFolder";
|
||||||
import { updateTextPairById } from "@/lib/actions/services/textPairService";
|
import { updatePairById } from "@/lib/actions/services/pairService";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { text_pairUpdateInput } from "../../../../generated/prisma/models";
|
|
||||||
import UpdateTextPairModal from "./UpdateTextPairModal";
|
import UpdateTextPairModal from "./UpdateTextPairModal";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { PairUpdateInput } from "../../../../generated/prisma/models";
|
||||||
|
|
||||||
interface TextPairCardProps {
|
interface TextPairCardProps {
|
||||||
textPair: TextPair;
|
textPair: TextPair;
|
||||||
@@ -66,8 +66,8 @@ export default function TextPairCard({
|
|||||||
<UpdateTextPairModal
|
<UpdateTextPairModal
|
||||||
isOpen={openUpdateModal}
|
isOpen={openUpdateModal}
|
||||||
onClose={() => setOpenUpdateModal(false)}
|
onClose={() => setOpenUpdateModal(false)}
|
||||||
onUpdate={async (id: number, data: text_pairUpdateInput) => {
|
onUpdate={async (id: number, data: PairUpdateInput) => {
|
||||||
await updateTextPairById(id, data);
|
await updatePairById(id, data);
|
||||||
setOpenUpdateModal(false);
|
setOpenUpdateModal(false);
|
||||||
refreshTextPairs();
|
refreshTextPairs();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import LightButton from "@/components/buttons/LightButton";
|
|||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { text_pairUpdateInput } from "../../../../generated/prisma/models";
|
import { PairUpdateInput } from "../../../../generated/prisma/models";
|
||||||
import { TextPair } from "./InFolder";
|
import { TextPair } from "./InFolder";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ interface UpdateTextPairModalProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
textPair: TextPair;
|
textPair: TextPair;
|
||||||
onUpdate: (id: number, tp: text_pairUpdateInput) => void;
|
onUpdate: (id: number, tp: PairUpdateInput) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UpdateTextPairModal({
|
export default function UpdateTextPairModal({
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getServerSession } from "next-auth";
|
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import InFolder from "./InFolder";
|
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({
|
export default async function FoldersPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ folder_id: number }>;
|
params: Promise<{ folder_id: number }>;
|
||||||
}) {
|
}) {
|
||||||
const session = await getServerSession();
|
const session = await auth();
|
||||||
const { folder_id } = await params;
|
const { folder_id } = await params;
|
||||||
const id = Number(folder_id);
|
|
||||||
const t = await getTranslations("folder_id");
|
const t = await getTranslations("folder_id");
|
||||||
|
|
||||||
if (!id) {
|
if (!folder_id) {
|
||||||
redirect("/folders");
|
redirect("/folders");
|
||||||
}
|
}
|
||||||
if (!session?.user?.name) redirect(`/login?redirect=/folders/${id}`);
|
if (!session?.user?.id) redirect(`/login?redirect=/folders/${folder_id}`);
|
||||||
if ((await getOwnerByFolderId(id)) !== session.user.name) {
|
if ((await getUserIdByFolderId(Number(folder_id))) !== Number(session.user.id)) {
|
||||||
return <p>{t("unauthorized")}</p>;
|
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 FoldersClient from "./FoldersClient";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getServerSession } from "next-auth";
|
|
||||||
export default async function FoldersPage() {
|
export default async function FoldersPage() {
|
||||||
const session = await getServerSession();
|
const session = await auth();
|
||||||
if (!session?.user?.name) redirect(`/login?redirect=/folders`);
|
if (!session?.user?.id) redirect(`/login?redirect=/folders`);
|
||||||
return <FoldersClient username={session.user.name} />;
|
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";
|
"use server";
|
||||||
|
|
||||||
import {
|
import { FolderCreateInput, FolderUpdateInput } from "../../../../generated/prisma/models";
|
||||||
folderCreateInput,
|
|
||||||
folderUpdateInput,
|
|
||||||
} from "../../../../generated/prisma/models";
|
|
||||||
import prisma from "../../db";
|
import prisma from "../../db";
|
||||||
|
|
||||||
export async function getFoldersByOwner(owner: string) {
|
export async function getFoldersByUserId(userId: number) {
|
||||||
const folders = await prisma.folder.findMany({
|
const folders = await prisma.folder.findMany({
|
||||||
where: {
|
where: {
|
||||||
owner: owner,
|
userId: userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return folders;
|
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({
|
const folders = await prisma.folder.findMany({
|
||||||
where: {
|
where: { userId },
|
||||||
owner: owner,
|
|
||||||
},
|
|
||||||
include: {
|
include: {
|
||||||
text_pair: {
|
_count: {
|
||||||
select: {
|
select: { pairs: true },
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return folders.map((folder) => ({
|
return folders.map(folder => ({
|
||||||
...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({
|
await prisma.folder.create({
|
||||||
data: folder,
|
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({
|
await prisma.folder.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
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({
|
const folder = await prisma.folder.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
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 { 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;
|
export default prisma;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "es2023",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -19,9 +23,18 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user