This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"title": "LL",
|
||||
"title": "learn-languages",
|
||||
"about": "About",
|
||||
"sourceCode": "GitHub",
|
||||
"login": "Login",
|
||||
"profile": "Profile"
|
||||
"profile": "Profile",
|
||||
"folders": "Folders"
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
"about": "关于",
|
||||
"sourceCode": "源码",
|
||||
"login": "登录",
|
||||
"profile": "个人资料"
|
||||
"profile": "个人资料",
|
||||
"folders": "文件夹"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { authOptions } from "../auth/[...nextauth]/route";
|
||||
import { FolderController } from "@/lib/db";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
export async function GET() {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (session) {
|
||||
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>Email: {session.data?.user?.email}</p>
|
||||
<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>
|
||||
</Center>
|
||||
);
|
||||
|
||||
@@ -35,17 +35,20 @@ export function Navbar() {
|
||||
const session = useSession();
|
||||
return (
|
||||
<div className="flex justify-between items-center w-full h-16 px-8 bg-[#35786f] text-white">
|
||||
<Link href={"/"} className="text-xl flex">
|
||||
<Image
|
||||
src={"/favicon.ico"}
|
||||
alt="logo"
|
||||
width="32"
|
||||
height="32"
|
||||
className="rounded-4xl"
|
||||
></Image>
|
||||
<span className="font-bold">{t("title")}</span>
|
||||
</Link>
|
||||
<div className="flex gap-4 text-xl">
|
||||
<div className="flex gap-4 text-xl justify-center items-center">
|
||||
<Link href={"/"} className="text-xl flex border-b">
|
||||
<Image
|
||||
src={"/favicon.ico"}
|
||||
alt="logo"
|
||||
width="32"
|
||||
height="32"
|
||||
className="rounded-4xl"
|
||||
></Image>
|
||||
<span className="font-bold text-pink-200">{t("title")}</span>
|
||||
</Link>
|
||||
<MyLink href="/folders">{t("folders")}</MyLink>
|
||||
</div>
|
||||
<div className="flex gap-4 text-xl justify-center items-center">
|
||||
<div className="relative">
|
||||
{showLanguageMenu && (
|
||||
<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";
|
||||
|
||||
export const pool = new Pool({
|
||||
@@ -9,141 +8,3 @@ export const pool = new Pool({
|
||||
connectionTimeoutMillis: 2000,
|
||||
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