diff --git a/public/messages/en-US/navbar.json b/public/messages/en-US/navbar.json index d0fa739..2486514 100644 --- a/public/messages/en-US/navbar.json +++ b/public/messages/en-US/navbar.json @@ -1,7 +1,8 @@ { - "title": "LL", + "title": "learn-languages", "about": "About", "sourceCode": "GitHub", "login": "Login", - "profile": "Profile" + "profile": "Profile", + "folders": "Folders" } diff --git a/public/messages/zh-CN/navbar.json b/public/messages/zh-CN/navbar.json index b9514e9..2d7bd12 100644 --- a/public/messages/zh-CN/navbar.json +++ b/public/messages/zh-CN/navbar.json @@ -3,5 +3,6 @@ "about": "关于", "sourceCode": "源码", "login": "登录", - "profile": "个人资料" + "profile": "个人资料", + "folders": "文件夹" } diff --git a/src/app/api/folders/route.ts b/src/app/api/folders/route.ts index 1457c8b..4cc893c 100644 --- a/src/app/api/folders/route.ts +++ b/src/app/api/folders/route.ts @@ -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( diff --git a/src/app/folders/FoldersClient.tsx b/src/app/folders/FoldersClient.tsx new file mode 100644 index 0000000..c8cfe15 --- /dev/null +++ b/src/app/folders/FoldersClient.tsx @@ -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 ( +
+
+
ID: {folder.id}
+
Name: {folder.name}
+
Text Pairs Count: {folder.text_pairs_count}
+
+ + open + + + delete + +
+ ); +}; + +export default function FoldersClient({ username }: { username: string }) { + const [folders, setFolders] = useState([]); + const [page, setPage] = useState<"folders" | "in folder">("folders"); + const [folderId, setFolderId] = useState(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 ( +
+ +

Your Folders

+ { + const folderName = prompt("Enter folder name:"); + if (!folderName) return; + await createFolder(folderName, username); + await updateFolders(); + }} + > + Create Folder + +
+ {folders.map((folder) => ( + { + 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"); + }} + /> + ))} +
+
+
+ ); + else if (page === "in folder") { + return ; + } +} diff --git a/src/app/folders/InFolder.tsx b/src/app/folders/InFolder.tsx new file mode 100644 index 0000000..156f929 --- /dev/null +++ b/src/app/folders/InFolder.tsx @@ -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([]); + + useEffect(() => { + getTextPairsByFolderId(folderId).then((textPairs) => { + setTextPairs(textPairs as TextPair[]); + }); + }, [folderId, textPairs]); + + const updateTextPairs = async () => { + const updatedTextPairs = await getTextPairsByFolderId(folderId); + setTextPairs(updatedTextPairs as TextPair[]); + }; + + return ( +
+

In Folder

+
+ ); +} diff --git a/src/app/folders/page.tsx b/src/app/folders/page.tsx new file mode 100644 index 0000000..12bb8f9 --- /dev/null +++ b/src/app/folders/page.tsx @@ -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 ( + + ); +} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 5aa7e32..5ef21c0 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -36,19 +36,7 @@ export default function MePage() {

{session.data?.user?.name}

Email: {session.data?.user?.email}

Logout - { - 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 - + ); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 9e96dfb..ef38600 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -35,17 +35,20 @@ export function Navbar() { const session = useSession(); return (
- - logo - {t("title")} - -
+
+ + logo + {t("title")} + + {t("folders")} +
+
{showLanguageMenu && (
diff --git a/src/lib/controllers/FolderController.ts b/src/lib/controllers/FolderController.ts new file mode 100644 index 0000000..ea18164 --- /dev/null +++ b/src/lib/controllers/FolderController.ts @@ -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); + } +} diff --git a/src/lib/controllers/TextPairController.ts b/src/lib/controllers/TextPairController.ts new file mode 100644 index 0000000..431b88b --- /dev/null +++ b/src/lib/controllers/TextPairController.ts @@ -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); + } +} diff --git a/src/lib/db.ts b/src/lib/db.ts index ea2902f..249e7cc 100644 --- a/src/lib/db.ts +++ b/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); - } - } -} diff --git a/src/interfaces.ts b/src/lib/interfaces.ts similarity index 100% rename from src/interfaces.ts rename to src/lib/interfaces.ts diff --git a/src/utils.ts b/src/lib/utils.ts similarity index 100% rename from src/utils.ts rename to src/lib/utils.ts