feat: add notes list
This commit is contained in:
parent
b1979aa8bb
commit
523f016a7c
735
package-lock.json
generated
735
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header showToolbar />
|
<Header allowExport />
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -13,12 +13,10 @@ export default async function Home() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-8">
|
<div className="flex flex-col items-center gap-8">
|
||||||
<Editor />
|
<Editor />
|
||||||
{!auth && (
|
<i className="text-center text-sm text-neutral-400">
|
||||||
<i className="text-center text-sm text-neutral-400">
|
Changes are not saved!<br />
|
||||||
Changes are not saved!<br />
|
<Link href="/login" className="font-bold hover:underline">Log in</Link> to save your notes.
|
||||||
<Link href="/login" className="font-bold hover:underline">Log in</Link> to save your notes.
|
</i>
|
||||||
</i>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export const metadata: Metadata = {
|
|||||||
export default function About() {
|
export default function About() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="m-4">
|
<div>
|
||||||
<h1 className="font-bold text-xl mb-2">Authenticate</h1>
|
<h1 className="font-bold text-xl mb-2">Authenticate</h1>
|
||||||
<div className="flex gap-4 flex-col md:flex-row">
|
<div className="flex gap-4 flex-col md:flex-row">
|
||||||
<AuthForm />
|
<AuthForm />
|
||||||
|
17
src/app/(noneditor)/notes/new/page.tsx
Normal file
17
src/app/(noneditor)/notes/new/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,14 +1,27 @@
|
|||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
import { getNotes } from "@/lib/notes";
|
||||||
|
import NoteCard from "@/components/NoteCard";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Notes - Rhyme",
|
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 (
|
return (
|
||||||
<div>
|
<>
|
||||||
<h1>Notes</h1>
|
<h1 className="font-bold text-xl mb-4">Notes</h1>
|
||||||
</div>
|
{notes ? (
|
||||||
|
notes.length > 0 ? (
|
||||||
|
notes.map((note) => <NoteCard key={note.id} note={note} />)
|
||||||
|
) : (
|
||||||
|
<span>You don't have any saved notes!</span>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span>Failed to get notes! Please, try again.</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { logOut } from "@/app/actions";
|
|||||||
import { getAuth } from "@/lib/auth";
|
import { getAuth } from "@/lib/auth";
|
||||||
import HeaderButton from "./HeaderButton";
|
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();
|
const auth = await getAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -24,7 +24,7 @@ export default async function Header({ showToolbar = false }: { showToolbar?: bo
|
|||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{showToolbar && <HeaderButton title="export" icon={<ArrowRightFromLine size={20} />} />}
|
{allowExport && <HeaderButton title="export" icon={<ArrowRightFromLine size={20} />} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 ml-auto">
|
<div className="flex gap-2 ml-auto">
|
||||||
|
25
src/components/NoteCard.tsx
Normal file
25
src/components/NoteCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { usersTable } from "./db/schema";
|
||||||
|
import { db } from "./db";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
export async function getAuth() {
|
export async function getAuth() {
|
||||||
@ -9,5 +12,17 @@ export async function getAuth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const decodedToken = jwt.decode(token) as jwt.JwtPayload;
|
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];
|
||||||
}
|
}
|
||||||
|
@ -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";
|
import { relations } from "drizzle-orm";
|
||||||
|
|
||||||
export const usersTable = pgTable("users", {
|
export const usersTable = pgTable("users", {
|
||||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
username: varchar({ length: 50 }).notNull().unique(),
|
username: varchar({ length: 50 }).notNull().unique(),
|
||||||
password: varchar().notNull(),
|
password: varchar().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const usersRelations = relations(usersTable, ({ many }) => ({
|
export const usersRelations = relations(usersTable, ({ many }) => ({
|
||||||
blocks: many(blocksTable),
|
notes: many(notesTable),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const blocksTable = pgTable("blocks", {
|
export const notesTable = pgTable("notes", {
|
||||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
tag: varchar({ length: 100 }).notNull().default(""),
|
name: varchar({ length: 50 }).notNull().default("Untitled"),
|
||||||
lines: json().notNull().default([]),
|
creationTime: timestamp().notNull().defaultNow(),
|
||||||
authorId: integer(),
|
lastEdited: timestamp().notNull().defaultNow(),
|
||||||
|
authorId: uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const blocksRelations = relations(blocksTable, ({ one }) => ({
|
export const notesRelations = relations(notesTable, ({ one }) => ({
|
||||||
author: one(usersTable, {
|
author: one(usersTable, {
|
||||||
fields: [blocksTable.authorId],
|
fields: [notesTable.authorId],
|
||||||
references: [usersTable.id],
|
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
38
src/lib/notes.ts
Normal 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));
|
||||||
|
}
|
@ -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*",
|
|
||||||
};
|
|
Loading…
x
Reference in New Issue
Block a user