feat: add notes list

This commit is contained in:
Kirill Siukhin 2025-07-10 17:35:34 +05:00
parent b1979aa8bb
commit 523f016a7c
12 changed files with 535 additions and 389 deletions

735
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ export default function RootLayout({
}>) {
return (
<>
<Header showToolbar />
<Header allowExport />
{children}
</>
);

View File

@ -13,12 +13,10 @@ export default async function Home() {
return (
<div className="flex flex-col items-center gap-8">
<Editor />
{!auth && (
<i className="text-center text-sm text-neutral-400">
Changes are not saved!<br />
<Link href="/login" className="font-bold hover:underline">Log in</Link> to save your notes.
</i>
)}
<i className="text-center text-sm text-neutral-400">
Changes are not saved!<br />
<Link href="/login" className="font-bold hover:underline">Log in</Link> to save your notes.
</i>
</div>
);
}

View File

@ -9,7 +9,7 @@ export const metadata: Metadata = {
export default function About() {
return (
<>
<div className="m-4">
<div>
<h1 className="font-bold text-xl mb-2">Authenticate</h1>
<div className="flex gap-4 flex-col md:flex-row">
<AuthForm />

View File

@ -0,0 +1,17 @@
import { redirect } from "next/navigation";
import { createNote } from "@/lib/notes";
export default async function NewNote() {
const noteId = await createNote();
if (noteId) {
redirect(`/notes/${noteId}`);
}
return (
<div className="text-center mt-4">
Failed to create a new note!<br />
Please, try again.
</div>
);
}

View File

@ -1,14 +1,27 @@
import { Metadata } from "next";
import { getNotes } from "@/lib/notes";
import NoteCard from "@/components/NoteCard";
export const metadata: Metadata = {
title: "Notes - Rhyme",
description: "View, create and edit your notes",
description: "View and edit your notes",
};
export default function Notes() {
export default async function Notes() {
const notes = await getNotes();
return (
<div>
<h1>Notes</h1>
</div>
<>
<h1 className="font-bold text-xl mb-4">Notes</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>
)}
</>
);
}

View File

@ -4,7 +4,7 @@ import { logOut } from "@/app/actions";
import { getAuth } from "@/lib/auth";
import HeaderButton from "./HeaderButton";
export default async function Header({ showToolbar = false }: { showToolbar?: boolean }) {
export default async function Header({ allowExport = false }: { allowExport?: boolean }) {
const auth = await getAuth();
return (
@ -24,7 +24,7 @@ export default async function Header({ showToolbar = false }: { showToolbar?: bo
</Link>
</>
)}
{showToolbar && <HeaderButton title="export" icon={<ArrowRightFromLine size={20} />} />}
{allowExport && <HeaderButton title="export" icon={<ArrowRightFromLine size={20} />} />}
</div>
<div className="flex gap-2 ml-auto">

View File

@ -0,0 +1,25 @@
import Link from "next/link";
import { revalidatePath } from "next/cache";
import { X } from "lucide-react";
import { notesTable } from "@/lib/db/schema";
import { deleteNote } from "@/lib/notes";
import IconOnlyButton from "./ui/IconOnlyButton";
export default function NoteCard({ note }: { note: typeof notesTable.$inferSelect }) {
const handleDeleteNote = async () => {
"use server";
await deleteNote(note.id);
revalidatePath("/notes");
}
return (
<div className="flex items-center mb-2 gap-4">
<Link href={`/notes/${note.id}`} className="flex flex-col border-2 border-neutral-700 py-3 px-4 rounded-lg hover:bg-neutral-800 w-full">
<h1 className="font-bold">{note.name}</h1>
<i className="text-neutral-400 text-sm">Creation date: {note.creationTime.toString()}</i>
<i className="text-neutral-400 text-sm">Last time edited: {note.lastEdited.toString()}</i>
</Link>
<IconOnlyButton icon={<X size={24} />} onClick={handleDeleteNote} />
</div>
);
}

View File

@ -1,4 +1,7 @@
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() {
@ -9,5 +12,17 @@ export async function getAuth() {
}
const decodedToken = jwt.decode(token) as jwt.JwtPayload;
return { username: decodedToken.sub };
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,26 +1,41 @@
import { pgTable, integer, json, varchar } from "drizzle-orm/pg-core";
import { pgTable, json, varchar, timestamp, uuid } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
export const usersTable = pgTable("users", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
id: uuid().primaryKey().defaultRandom(),
username: varchar({ length: 50 }).notNull().unique(),
password: varchar().notNull(),
});
export const usersRelations = relations(usersTable, ({ many }) => ({
blocks: many(blocksTable),
notes: many(notesTable),
}));
export const blocksTable = pgTable("blocks", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
tag: varchar({ length: 100 }).notNull().default(""),
lines: json().notNull().default([]),
authorId: integer(),
export const notesTable = pgTable("notes", {
id: uuid().primaryKey().defaultRandom(),
name: varchar({ length: 50 }).notNull().default("Untitled"),
creationTime: timestamp().notNull().defaultNow(),
lastEdited: timestamp().notNull().defaultNow(),
authorId: uuid(),
});
export const blocksRelations = relations(blocksTable, ({ one }) => ({
export const notesRelations = relations(notesTable, ({ one }) => ({
author: one(usersTable, {
fields: [blocksTable.authorId],
fields: [notesTable.authorId],
references: [usersTable.id],
}),
}));
export const blocksTable = pgTable("blocks", {
id: uuid().primaryKey().defaultRandom(),
tag: varchar({ length: 100 }).notNull().default(""),
lines: json().notNull().default([]),
noteId: uuid(),
});
export const blocksRelations = relations(blocksTable, ({ one }) => ({
note: one(notesTable, {
fields: [blocksTable.noteId],
references: [notesTable.id],
}),
}));

38
src/lib/notes.ts Normal file
View File

@ -0,0 +1,38 @@
import { eq } from "drizzle-orm";
import { notesTable } from "./db/schema";
import { getAuth } from "./auth";
import { db } from "./db";
export async function getNotes() {
const auth = await getAuth();
if (!auth) {
return null;
}
return db.select()
.from(notesTable)
.where(eq(notesTable.authorId, auth.id));
}
export async function createNote() {
const auth = await getAuth();
if (!auth) {
return null;
}
const note = await db
.insert(notesTable)
.values({ authorId: auth.id })
.returning({ id: notesTable.id });
return note[0].id;
}
export async function deleteNote(noteId: string) {
const auth = await getAuth();
if (!auth) {
return null;
}
await db.delete(notesTable).where(eq(notesTable.id, noteId));
}

View File

@ -1,16 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { getAuth } from "./lib/auth";
export async function middleware(request: NextRequest) {
const auth = await getAuth();
if (auth) {
return NextResponse.next();
} else {
return NextResponse.redirect(new URL("/auth", request.url));
}
}
export const config = {
matcher: "/notes/:path*",
};