fix: start database fix

This commit is contained in:
Kirill Siukhin 2025-07-14 18:26:50 +05:00
parent 917e460a96
commit 7d76ec1974
13 changed files with 72 additions and 343 deletions

55
package-lock.json generated
View File

@ -1046,9 +1046,9 @@
}
},
"node_modules/@eslint/core": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -1083,9 +1083,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.30.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
"version": "9.31.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
"dev": true,
"license": "MIT",
"engines": {
@ -1119,19 +1119,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1669,16 +1656,16 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz",
"integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==",
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.9.0"
"@tybys/wasm-util": "^0.10.0"
}
},
"node_modules/@next/env": {
@ -2179,9 +2166,9 @@
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
"integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
"dev": true,
"license": "MIT",
"optional": true,
@ -2239,9 +2226,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.6.tgz",
"integrity": "sha512-uYssdp9z5zH5GQ0L4zEJ2ZuavYsJwkozjiUzCRfGtaaQcyjAMJ34aP8idv61QlqTozu6kudyr6JMq9Chf09dfA==",
"version": "20.19.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.7.tgz",
"integrity": "sha512-1GM9z6BJOv86qkPvzh2i6VW5+VVrXxCLknfmTkWEqz+6DqosiY28XUWCTmBcJ0ACzKqx/iwdIREfo1fwExIlkA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3943,9 +3930,9 @@
}
},
"node_modules/eslint": {
"version": "9.30.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
"version": "9.31.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3953,9 +3940,9 @@
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.3.0",
"@eslint/core": "^0.14.0",
"@eslint/core": "^0.15.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.30.1",
"@eslint/js": "9.31.0",
"@eslint/plugin-kit": "^0.3.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",

View File

@ -1,10 +1,8 @@
"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { eq } from "drizzle-orm";
import { db } from "@/lib/db";
import { cookies } from "next/headers";
import { usersTable } from "@/lib/db/schema";
import { db } from "@/lib/db";
import { eq } from "drizzle-orm";
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
@ -82,6 +80,29 @@ export async function register(_prevState: unknown, formData: FormData) {
redirect("/notes");
}
export async function getAuth() {
const cookieStore = await cookies();
const token = cookieStore.get("session")?.value;
if (!token) {
return null;
}
const decodedToken = jwt.decode(token) as jwt.JwtPayload;
const username = decodedToken.sub;
if (!username) {
return null;
}
const users = await db.select()
.from(usersTable)
.where(eq(usersTable.username, username));
if (users.length === 0) {
return null;
}
return users[0];
}
export async function logOut() {
const cookieStore = await cookies();
cookieStore.delete("session");

View File

@ -1,42 +0,0 @@
"use server";
import { revalidatePath } from "next/cache";
import { eq, and } from "drizzle-orm";
import { blocksTable, IBlock, INote } from "@/lib/db/schema";
import { db } from "@/lib/db";
export async function createBlock(note: INote) {
await db
.insert(blocksTable)
.values({ noteId: note.id });
revalidatePath("/blocks/[id]");
}
export async function getBlocks(note: INote) {
return db.select()
.from(blocksTable)
.where(and(eq(blocksTable.noteId, note.id)));
}
export async function updateBlockTag(block: IBlock, newTag: string) {
await db
.update(blocksTable)
.set({ tag: newTag })
.where(eq(blocksTable.id, block.id));
}
export async function deleteBlock(block: IBlock) {
await db.delete(blocksTable)
.where(and(eq(blocksTable.id, block.id)));
revalidatePath("/blocks/[id]");
}
export async function switchLock(block: IBlock) {
await db.update(blocksTable)
.set({ isLocked: !block.isLocked })
.where(and(eq(blocksTable.id, block.id)));
revalidatePath("/blocks/[id]");
}

View File

@ -1,72 +0,0 @@
"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { validate as uuidValidate } from "uuid";
import { notesTable } from "@/lib/db/schema";
import { getAuth } from "@/lib/auth";
import { eq, and, desc } from "drizzle-orm";
import { db } from "@/lib/db";
export async function createNote() {
const auth = await getAuth();
if (!auth) {
redirect("/auth");
}
const notes = await db
.insert(notesTable)
.values({ authorId: auth.id })
.returning({ id: notesTable.id });
redirect(`/notes/${notes[0].id}`);
}
export async function getNotes(authorId: string) {
return db.select()
.from(notesTable)
.where(eq(notesTable.authorId, authorId))
.orderBy(desc(notesTable.lastEdited));
}
export async function getNote(noteId: string) {
if (!uuidValidate(noteId)) {
return null;
}
const auth = await getAuth();
if (!auth) {
return null;
}
const notes = await db.select()
.from(notesTable)
.where(and(eq(notesTable.id, noteId), eq(notesTable.authorId, auth.id)));
if (notes.length === 0) {
return null;
} else {
return notes[0];
}
}
export async function deleteNote(noteId: string) {
if (!uuidValidate(noteId)) {
return null;
}
const auth = await getAuth();
if (!auth) {
return null;
}
await db.delete(notesTable).where(eq(notesTable.id, noteId));
revalidatePath("/notes");
}
export async function updateTitle(noteId: string, title: string) {
await db
.update(notesTable)
.set({ title })
.where(eq(notesTable.id, noteId));
}

View File

@ -1,34 +1,18 @@
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { getBlocks } from "@/app/actions/blocks";
import { getNote } from "@/app/actions/notes";
import Editor from "@/components/editor/Editor";
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params;
const note = await getNote(id);
if (!note) {
notFound();
}
return { title: note.title };
return { title: id };
}
export default async function Note({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const note = await getNote(id);
if (!note) {
notFound();
}
const blocks = await getBlocks(note);
if (!blocks) {
notFound();
}
return (
<div className="flex justify-center">
<Editor note={note} blocks={blocks} />
{id}
<Editor />
</div>
);
}

View File

@ -1,8 +1,4 @@
import { Metadata } from "next";
import { redirect } from "next/navigation";
import { getNotes } from "@/app/actions/notes";
import { getAuth } from "@/lib/auth";
import NoteCard from "@/components/ui/NoteCard";
export const metadata: Metadata = {
title: "Notes - Rhyme",
@ -10,25 +6,10 @@ export const metadata: Metadata = {
};
export default async function Notes() {
const auth = await getAuth();
if (!auth) {
redirect("/auth");
}
const notes = await getNotes(auth.id);
return (
<>
<h1 className="font-bold text-xl mb-4">Notes of {auth.username}:</h1>
{notes ? (
notes.length > 0 ? (
notes.map((note) => <NoteCard key={note.id} note={note} />)
) : (
<span>You don&apos;t have any saved notes!</span>
)
) : (
<span>Failed to get notes! Please, try again.</span>
)}
<h1 className="font-bold text-xl mb-4">Notes of (username):</h1>
{/* notes */}
</>
);
}

View File

@ -1,11 +1,10 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { getAuth } from "@/lib/auth";
import { getAuth } from "@/app/actions/auth";
import Editor from "@/components/editor/Editor";
export default async function Home() {
const auth = await getAuth();
if (auth) {
redirect("/notes");
}

View File

@ -1,8 +1,6 @@
import Link from "next/link";
import { CircleQuestionMark, List, Plus, UserRound, UserRoundMinus } from "lucide-react";
import { createNote } from "@/app/actions/notes";
import { logOut } from "@/app/actions/auth";
import { getAuth } from "@/lib/auth";
import { getAuth } from "@/app/actions/auth";
import HeaderButton from "./ui/HeaderButton";
export default async function Header() {
@ -16,7 +14,7 @@ export default async function Header() {
{auth && (
<div className="flex gap-2">
<HeaderButton onClick={createNote} title="new" icon={<Plus size={20} />} />
<HeaderButton title="new" icon={<Plus size={20} />} />
<Link href="/notes">
<HeaderButton title="list" icon={<List size={20} />} />
</Link>
@ -28,7 +26,7 @@ export default async function Header() {
<HeaderButton title="about" icon={<CircleQuestionMark size={20} />} />
</Link>
{auth ? (
<HeaderButton onClick={logOut} title="log out" icon={<UserRoundMinus size={20} />} />
<HeaderButton title="log out" icon={<UserRoundMinus size={20} />} />
) : (
<Link href="/auth">
<HeaderButton title="login" icon={<UserRound size={20} />} />

View File

@ -1,37 +1,10 @@
"use client";
import { ChangeEvent } from "react";
import { ArrowDown, ArrowUp, Lock, LockOpen, Minus, Plus, X } from "lucide-react";
import { IBlock } from "@/lib/db/schema";
import { switchLock, deleteBlock } from "@/app/actions/blocks";
import { ArrowDown, ArrowUp, LockOpen, Minus, Plus, X } from "lucide-react";
import IconOnlyButton from "../ui/IconOnlyButton";
import LineInput from "./LineInput";
export default function Block({ block }: { block: IBlock }) {
const handleAddLine = () => {
// TODO
}
const handleDeleteLine = () => {
// TODO
}
const handleTagUpdate = (e: ChangeEvent<HTMLInputElement>) => {
// TODO
}
const handleLineUpdate = (e: ChangeEvent<HTMLInputElement>, line: string) => {
// TODO
}
const handleBlockUp = () => {
// TODO
}
const handleBlockDown = () => {
// TODO
}
export default function Block() {
return (
<div className="flex flex-col gap-2 w-full">
<div className="border-2 border-neutral-800 rounded-lg p-3 w-full">
@ -39,34 +12,20 @@ export default function Block({ block }: { block: IBlock }) {
type="text"
placeholder="enter tag..."
className="w-full focus:outline-none"
defaultValue={block.tag}
disabled={block.isLocked}
onChange={handleTagUpdate}
/>
{block.lines.map((line, i) => (
<LineInput
key={i}
defaultValue={line}
disabled={block.isLocked}
onChange={(e) => handleLineUpdate(e, line)}
/>
))}
<LineInput />
<div className="flex items-center mx-2 mt-2">
<div className="flex gap-1 mr-4">
<IconOnlyButton onClick={handleAddLine} icon={<Plus size={18} />} />
<IconOnlyButton onClick={handleDeleteLine} icon={<Minus size={18} />} />
<IconOnlyButton icon={<Plus size={18} />} />
<IconOnlyButton icon={<Minus size={18} />} />
</div>
<div className="flex gap-1">
<IconOnlyButton onClick={handleBlockUp} icon={<ArrowUp size={18} />} />
<IconOnlyButton onClick={handleBlockDown} icon={<ArrowDown size={18} />} />
<IconOnlyButton icon={<ArrowUp size={18} />} />
<IconOnlyButton icon={<ArrowDown size={18} />} />
</div>
<div className="flex gap-2 ml-auto">
<IconOnlyButton onClick={() => deleteBlock(block)} icon={<X size={18} />} />
<IconOnlyButton
onClick={() => switchLock(block)}
icon={block.isLocked ? <Lock size={18} /> : <LockOpen size={18} />}
alwaysOn={block.isLocked}
/>
<IconOnlyButton icon={<X size={18} />} />
<IconOnlyButton icon={<LockOpen size={18} />} />
</div>
</div>
</div>

View File

@ -1,70 +1,17 @@
"use client";
import { ChangeEvent, useState } from "react";
import { Copy, Plus } from "lucide-react";
import { v4 as uuidv4 } from "uuid";
import { createBlock } from "@/app/actions/blocks";
import { IBlock, INote } from "@/lib/db/schema";
import IconOnlyButton from "../ui/IconOnlyButton";
import Block from "./Block";
interface EditorProps {
note? : INote;
blocks? : IBlock[];
}
const defaultNoteId = uuidv4();
const defaultNote: INote = {
id: defaultNoteId,
title: "Untitled",
creationTime: new Date(),
lastEdited: new Date(),
authorId: uuidv4(),
};
const defaultBlocks: IBlock[] = [
{
id: uuidv4(),
tag: "",
order: 1,
isLocked: false,
noteId: defaultNoteId,
lines: ["", "", "", ""],
},
];
export default function Editor({ note = defaultNote, blocks = defaultBlocks }: EditorProps) {
const [title, setTitle] = useState(note.title);
const handleUpdateTitle = (e: ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
}
const handleCopy = () => {
let copyString = "";
blocks.forEach((block) => {
if (block.tag !== "") {
copyString += `[${block.tag}]`;
}
copyString += block.lines + "\n";
});
navigator.clipboard.writeText(copyString);
}
export default function Editor() {
return (
<div className="flex flex-col items-center max-w-2xl w-full gap-4">
<input
className="font-bold text-xl w-full text-center focus:outline-none"
value={title}
onChange={handleUpdateTitle}
/>
{blocks.map((block) => <Block key={block.id} block={block} /> )}
<input className="font-bold text-xl w-full text-center focus:outline-none" />
<Block />
<div className="flex gap-2">
<IconOnlyButton onClick={() => createBlock(note)} icon={<Plus size={24} />} />
<IconOnlyButton onClick={handleCopy} icon={<Copy size={24} />} title="Copy note to clipboard" />
<IconOnlyButton icon={<Plus size={24} />} />
<IconOnlyButton icon={<Copy size={24} />} title="Copy note to clipboard" />
</div>
</div>
);

View File

@ -1,7 +1,6 @@
import Link from "next/link";
import { X } from "lucide-react";
import { INote } from "@/lib/db/schema";
import { deleteNote } from "@/app/actions/notes";
import IconOnlyButton from "./IconOnlyButton";
function makeTimestamp(date: Date) {
@ -14,11 +13,6 @@ function makeTimestamp(date: Date) {
}
export default function NoteCard({ note }: { note: INote }) {
const handleDelete = async () => {
"use server";
await deleteNote(note.id);
}
return (
<div className="flex items-center mb-3 gap-4">
<Link href={`/notes/${note.id}`} className="flex flex-col border border-neutral-500 py-3 px-4 rounded-lg hover:bg-neutral-800 w-full">
@ -26,7 +20,7 @@ export default function NoteCard({ note }: { note: INote }) {
<i className="text-neutral-400 text-sm">Last time edited: {makeTimestamp(note.lastEdited)}</i>
<i className="text-neutral-400 text-sm">Creation date: {makeTimestamp(note.creationTime)}</i>
</Link>
<IconOnlyButton icon={<X size={24} />} onClick={handleDelete} />
<IconOnlyButton icon={<X size={24} />} />
</div>
);
}

View File

@ -1,28 +0,0 @@
import { cookies } from "next/headers";
import { eq } from "drizzle-orm";
import { usersTable } from "./db/schema";
import { db } from "./db";
import jwt from "jsonwebtoken";
export async function getAuth() {
const cookieStore = await cookies();
const token = cookieStore.get("session")?.value;
if (!token) {
return null;
}
const decodedToken = jwt.decode(token) as jwt.JwtPayload;
const username = decodedToken.sub;
if (!username) {
return null;
}
const users = await db.select()
.from(usersTable)
.where(eq(usersTable.username, username));
if (users.length === 0) {
return null;
}
return users[0];
}

View File

@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect } from "react";
// eslint-disable-next-line @typescript-eslint/no-explicit-any