This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"title": "LL",
|
"title": "learn-languages",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"profile": "Profile"
|
"profile": "Profile",
|
||||||
|
"folders": "Folders"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
"about": "关于",
|
"about": "关于",
|
||||||
"sourceCode": "源码",
|
"sourceCode": "源码",
|
||||||
"login": "登录",
|
"login": "登录",
|
||||||
"profile": "个人资料"
|
"profile": "个人资料",
|
||||||
|
"folders": "文件夹"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { authOptions } from "../auth/[...nextauth]/route";
|
import { authOptions } from "../auth/[...nextauth]/route";
|
||||||
import { FolderController } from "@/lib/db";
|
import { FolderController } from "@/lib/db";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET() {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (session) {
|
if (session) {
|
||||||
return new NextResponse(
|
return new NextResponse(
|
||||||
|
|||||||
103
src/app/folders/FoldersClient.tsx
Normal file
103
src/app/folders/FoldersClient.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import DarkButton from "@/components/buttons/DarkButton";
|
||||||
|
import LightButton from "@/components/buttons/LightButton";
|
||||||
|
import ACard from "@/components/cards/ACard";
|
||||||
|
import { Center } from "@/components/Center";
|
||||||
|
import {
|
||||||
|
createFolder,
|
||||||
|
deleteFolderById,
|
||||||
|
getFoldersWithTextPairsCountByOwner,
|
||||||
|
} from "@/lib/controllers/FolderController";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import InFolder from "./InFolder";
|
||||||
|
|
||||||
|
interface Folder {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
text_pairs_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FolderProps {
|
||||||
|
folder: Folder;
|
||||||
|
deleteCallback: () => void;
|
||||||
|
openCallback: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FolderCard = ({ folder, deleteCallback, openCallback }: FolderProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center justify-center border">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div>ID: {folder.id}</div>
|
||||||
|
<div>Name: {folder.name}</div>
|
||||||
|
<div>Text Pairs Count: {folder.text_pairs_count}</div>
|
||||||
|
</div>
|
||||||
|
<DarkButton className="w-fit h-fit m-2" onClick={openCallback}>
|
||||||
|
open
|
||||||
|
</DarkButton>
|
||||||
|
<DarkButton className="w-fit h-fit m-2" onClick={deleteCallback}>
|
||||||
|
delete
|
||||||
|
</DarkButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FoldersClient({ username }: { username: string }) {
|
||||||
|
const [folders, setFolders] = useState<Folder[]>([]);
|
||||||
|
const [page, setPage] = useState<"folders" | "in folder">("folders");
|
||||||
|
const [folderId, setFolderId] = useState<number>(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getFoldersWithTextPairsCountByOwner(username).then((folders) => {
|
||||||
|
setFolders(folders as Folder[]);
|
||||||
|
});
|
||||||
|
}, [username]);
|
||||||
|
|
||||||
|
const updateFolders = async () => {
|
||||||
|
const updatedFolders = await getFoldersWithTextPairsCountByOwner(username);
|
||||||
|
setFolders(updatedFolders as Folder[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (page === "folders")
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<ACard className="flex flex-col">
|
||||||
|
<h1 className="text-4xl font-extrabold text-center">Your Folders</h1>
|
||||||
|
<LightButton
|
||||||
|
className="w-fit"
|
||||||
|
onClick={async () => {
|
||||||
|
const folderName = prompt("Enter folder name:");
|
||||||
|
if (!folderName) return;
|
||||||
|
await createFolder(folderName, username);
|
||||||
|
await updateFolders();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Folder
|
||||||
|
</LightButton>
|
||||||
|
<div className="overflow-y-auto">
|
||||||
|
{folders.map((folder) => (
|
||||||
|
<FolderCard
|
||||||
|
key={folder.id}
|
||||||
|
folder={folder}
|
||||||
|
deleteCallback={() => {
|
||||||
|
const confirm = prompt(
|
||||||
|
"Input folder's name to delete this folder.",
|
||||||
|
);
|
||||||
|
if (confirm === folder.name) {
|
||||||
|
deleteFolderById(folder.id).then(updateFolders);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
openCallback={() => {
|
||||||
|
setFolderId(folder.id);
|
||||||
|
setPage("in folder");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ACard>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
else if (page === "in folder") {
|
||||||
|
return <InFolder username={username} folderId={folderId} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/app/folders/InFolder.tsx
Normal file
38
src/app/folders/InFolder.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { getTextPairsByFolderId } from "@/lib/controllers/TextPairController";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
username: string;
|
||||||
|
folderId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TextPair {
|
||||||
|
id: number;
|
||||||
|
text1: string;
|
||||||
|
text2: string;
|
||||||
|
locale1: string;
|
||||||
|
locale2: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InFolder({ folderId }: Props) {
|
||||||
|
const [textPairs, setTextPairs] = useState<TextPair[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getTextPairsByFolderId(folderId).then((textPairs) => {
|
||||||
|
setTextPairs(textPairs as TextPair[]);
|
||||||
|
});
|
||||||
|
}, [folderId, textPairs]);
|
||||||
|
|
||||||
|
const updateTextPairs = async () => {
|
||||||
|
const updatedTextPairs = await getTextPairsByFolderId(folderId);
|
||||||
|
setTextPairs(updatedTextPairs as TextPair[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>In Folder</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/app/folders/page.tsx
Normal file
10
src/app/folders/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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`);
|
||||||
|
return (
|
||||||
|
<FoldersClient username={session.user.name} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -36,19 +36,7 @@ export default function MePage() {
|
|||||||
<p>{session.data?.user?.name}</p>
|
<p>{session.data?.user?.name}</p>
|
||||||
<p>Email: {session.data?.user?.email}</p>
|
<p>Email: {session.data?.user?.email}</p>
|
||||||
<DarkButton onClick={signOut}>Logout</DarkButton>
|
<DarkButton onClick={signOut}>Logout</DarkButton>
|
||||||
<LightButton
|
|
||||||
onClick={() => {
|
|
||||||
fetch("/api/folders", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ name: "New Folder" }),
|
|
||||||
}).then(async (res) => console.log(await res.json()));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
POST
|
|
||||||
</LightButton>
|
|
||||||
</ACard>
|
</ACard>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export function Navbar() {
|
|||||||
const session = useSession();
|
const session = useSession();
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center w-full h-16 px-8 bg-[#35786f] text-white">
|
<div className="flex justify-between items-center w-full h-16 px-8 bg-[#35786f] text-white">
|
||||||
<Link href={"/"} className="text-xl flex">
|
<div className="flex gap-4 text-xl justify-center items-center">
|
||||||
|
<Link href={"/"} className="text-xl flex border-b">
|
||||||
<Image
|
<Image
|
||||||
src={"/favicon.ico"}
|
src={"/favicon.ico"}
|
||||||
alt="logo"
|
alt="logo"
|
||||||
@@ -43,9 +44,11 @@ export function Navbar() {
|
|||||||
height="32"
|
height="32"
|
||||||
className="rounded-4xl"
|
className="rounded-4xl"
|
||||||
></Image>
|
></Image>
|
||||||
<span className="font-bold">{t("title")}</span>
|
<span className="font-bold text-pink-200">{t("title")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex gap-4 text-xl">
|
<MyLink href="/folders">{t("folders")}</MyLink>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 text-xl justify-center items-center">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{showLanguageMenu && (
|
{showLanguageMenu && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
50
src/lib/controllers/FolderController.ts
Normal file
50
src/lib/controllers/FolderController.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { pool } from "../db";
|
||||||
|
|
||||||
|
export async function deleteFolderById(id: number) {
|
||||||
|
try {
|
||||||
|
await pool.query("DELETE FROM folders WHERE id = $1", [id]);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFoldersByOwner(owner: string) {
|
||||||
|
try {
|
||||||
|
const folders = await pool.query("SELECT * FROM folders WHERE owner = $1", [
|
||||||
|
owner,
|
||||||
|
]);
|
||||||
|
return folders.rows;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFoldersWithTextPairsCountByOwner(owner: string) {
|
||||||
|
try {
|
||||||
|
const folders = await pool.query(
|
||||||
|
`select f.id, f.name, f.owner, count(tp.id) as text_pairs_count from folders f
|
||||||
|
left join text_pairs tp on tp.folder_id = f.id
|
||||||
|
where f.owner = $1
|
||||||
|
group by f.id, f.name, f.owner`,
|
||||||
|
[owner],
|
||||||
|
);
|
||||||
|
return folders.rows;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFolder(name: string, owner: string) {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
await pool.query("INSERT INTO folders (name, owner) VALUES ($1, $2)", [
|
||||||
|
name.trim(),
|
||||||
|
owner,
|
||||||
|
])
|
||||||
|
).rows[0];
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/lib/controllers/TextPairController.ts
Normal file
69
src/lib/controllers/TextPairController.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { pool } from "../db";
|
||||||
|
|
||||||
|
export async function createTextPair(
|
||||||
|
locale1: string,
|
||||||
|
locale2: string,
|
||||||
|
text1: string,
|
||||||
|
text2: string,
|
||||||
|
folderId: number,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
"INSERT INTO text_pairs (locale1, locale2, text1, text2, folder_id) VALUES ($1, $2, $3, $4, $5)",
|
||||||
|
[locale1.trim(), locale2.trim(), text1.trim(), text2.trim(), folderId],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTextPairById(id: number) {
|
||||||
|
try {
|
||||||
|
await pool.query("DELETE FROM text_pairs WHERE id = $1", [id]);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateWordPairById(
|
||||||
|
id: number,
|
||||||
|
locale1: string,
|
||||||
|
locale2: string,
|
||||||
|
text1: string,
|
||||||
|
text2: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
"UPDATE text_pairs SET locale1 = $1, locale2 = $2, text1 = $3, text2 = $4 WHERE id = $5",
|
||||||
|
[locale1.trim(), locale2.trim(), text1.trim(), text2.trim(), id],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTextPairsByFolderId(folderId: number) {
|
||||||
|
try {
|
||||||
|
const textPairs = await pool.query(
|
||||||
|
"SELECT * FROM text_pairs WHERE folder_id = $1",
|
||||||
|
[folderId],
|
||||||
|
);
|
||||||
|
return textPairs.rows;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTextPairsCountByFolderId(folderId: number) {
|
||||||
|
try {
|
||||||
|
const count = await pool.query(
|
||||||
|
"SELECT COUNT(*) FROM text_pairs WHERE folder_id = $1",
|
||||||
|
[folderId],
|
||||||
|
);
|
||||||
|
return count.rows[0].count;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/lib/db.ts
139
src/lib/db.ts
@@ -1,4 +1,3 @@
|
|||||||
import bcrypt from "bcryptjs";
|
|
||||||
import { Pool } from "pg";
|
import { Pool } from "pg";
|
||||||
|
|
||||||
export const pool = new Pool({
|
export const pool = new Pool({
|
||||||
@@ -9,141 +8,3 @@ export const pool = new Pool({
|
|||||||
connectionTimeoutMillis: 2000,
|
connectionTimeoutMillis: 2000,
|
||||||
maxLifetimeSeconds: 60,
|
maxLifetimeSeconds: 60,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class UserController {
|
|
||||||
static async createUser(username: string, password: string) {
|
|
||||||
const encodedPassword = await bcrypt.hash(password, 10);
|
|
||||||
try {
|
|
||||||
await pool.query(
|
|
||||||
"INSERT INTO users (username, password) VALUES ($1, $2)",
|
|
||||||
[username, encodedPassword],
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async getUserByUsername(username: string) {
|
|
||||||
try {
|
|
||||||
const user = await pool.query("SELECT * FROM users WHERE username = $1", [
|
|
||||||
username,
|
|
||||||
]);
|
|
||||||
return user.rows[0];
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async deleteUserById(id: number) {
|
|
||||||
try {
|
|
||||||
await pool.query("DELETE FROM users WHERE id = $1", [id]);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FolderController {
|
|
||||||
static async getFolderById(id: number) {
|
|
||||||
try {
|
|
||||||
const folder = await pool.query("SELECT * FROM folders WHERE id = $1", [
|
|
||||||
id,
|
|
||||||
]);
|
|
||||||
return folder.rows[0];
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async deleteFolderById(id: number) {
|
|
||||||
try {
|
|
||||||
await pool.query("DELETE FROM folders WHERE id = $1", [id]);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async getFoldersByOwner(owner: string) {
|
|
||||||
try {
|
|
||||||
const folders = await pool.query(
|
|
||||||
"SELECT * FROM folders WHERE owner = $1",
|
|
||||||
[owner],
|
|
||||||
);
|
|
||||||
return folders.rows;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async createFolder(name: string, owner: string) {
|
|
||||||
try {
|
|
||||||
return (
|
|
||||||
await pool.query("INSERT INTO folders (name, owner) VALUES ($1, $2)", [
|
|
||||||
name,
|
|
||||||
owner,
|
|
||||||
])
|
|
||||||
).rows[0];
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WordPairController {
|
|
||||||
static async createWordPair(
|
|
||||||
locale1: string,
|
|
||||||
locale2: string,
|
|
||||||
text1: string,
|
|
||||||
text2: string,
|
|
||||||
folderId: number,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
await pool.query(
|
|
||||||
"INSERT INTO word_pairs (locale1, locale2, text1, text2, folder_id) VALUES ($1, $2, $3, $4, $5)",
|
|
||||||
[locale1, locale2, text1, text2, folderId],
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async getWordPairById(id: number) {
|
|
||||||
try {
|
|
||||||
const wordPair = await pool.query(
|
|
||||||
"SELECT * FROM word_pairs WHERE id = $1",
|
|
||||||
[id],
|
|
||||||
);
|
|
||||||
return wordPair.rows[0];
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async deleteWordPairById(id: number) {
|
|
||||||
try {
|
|
||||||
await pool.query("DELETE FROM word_pairs WHERE id = $1", [id]);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async updateWordPairById(
|
|
||||||
id: number,
|
|
||||||
locale1: string,
|
|
||||||
locale2: string,
|
|
||||||
text1: string,
|
|
||||||
text2: string,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
await pool.query(
|
|
||||||
"UPDATE word_pairs SET locale1 = $1, locale2 = $2, text1 = $3, text2 = $4 WHERE id = $5",
|
|
||||||
[locale1, locale2, text1, text2, id],
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async getWordPairsByFolderId(folderId: number) {
|
|
||||||
try {
|
|
||||||
const wordPairs = await pool.query(
|
|
||||||
"SELECT * FROM word_pairs WHERE folder_id = $1",
|
|
||||||
[folderId],
|
|
||||||
);
|
|
||||||
return wordPairs.rows;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user