feat: adjust routes for prj data and typings

This commit is contained in:
Nicola Zambello 2023-08-07 11:21:43 +02:00
parent efe8868c1d
commit 83300a4701
Signed by: nzambello
GPG key ID: 56E4A92C2C1E50BA
9 changed files with 172 additions and 129 deletions

View file

@ -1,52 +0,0 @@
import type { User, Note } from "@prisma/client";
import { prisma } from "~/db.server";
export function getNote({
id,
userId,
}: Pick<Note, "id"> & {
userId: User["id"];
}) {
return prisma.note.findFirst({
select: { id: true, body: true, title: true },
where: { id, userId },
});
}
export function getNoteListItems({ userId }: { userId: User["id"] }) {
return prisma.note.findMany({
where: { userId },
select: { id: true, title: true },
orderBy: { updatedAt: "desc" },
});
}
export function createNote({
body,
title,
userId,
}: Pick<Note, "body" | "title"> & {
userId: User["id"];
}) {
return prisma.note.create({
data: {
title,
body,
user: {
connect: {
id: userId,
},
},
},
});
}
export function deleteNote({
id,
userId,
}: Pick<Note, "id"> & { userId: User["id"] }) {
return prisma.note.deleteMany({
where: { id, userId },
});
}

View file

@ -0,0 +1,52 @@
import type { User, Translation } from "@prisma/client";
import { prisma } from "~/db.server";
export function getTranslation({
id,
userId,
}: Pick<Translation, "id"> & {
userId: User["id"];
}) {
return prisma.translation.findFirst({
where: { id, userId },
});
}
export function getTranslationsListItems({ userId }: { userId: User["id"] }) {
return prisma.translation.findMany({
where: { userId },
orderBy: { updatedAt: "desc" },
});
}
export function createTranslation({
lang,
text,
result,
userId,
}: Pick<Translation, "lang" | "text" | "result"> & {
userId: User["id"];
}) {
return prisma.translation.create({
data: {
lang,
text,
result,
user: {
connect: {
id: userId,
},
},
},
});
}
export function deleteTranslation({
id,
userId,
}: Pick<Translation, "id"> & { userId: User["id"] }) {
return prisma.translation.deleteMany({
where: { id, userId },
});
}

View file

@ -28,6 +28,34 @@ export async function createUser(email: User["email"], password: string) {
}); });
} }
export async function updateUser(
email: User["email"],
data: {
openAIKey?: User["openAIKey"];
password?: string;
}
) {
let userData: { [key: string]: any } = {};
if (data.openAIKey) {
userData.openAIKey = data.openAIKey;
}
if (data.password) {
const hashedPassword = await bcrypt.hash(data.password, 10);
userData.password = {
create: {
hash: hashedPassword,
},
};
}
if (Object.values(userData).length === 0) return;
return prisma.user.update({
where: { email },
data: userData,
});
}
export async function deleteUserByEmail(email: User["email"]) { export async function deleteUserByEmail(email: User["email"]) {
return prisma.user.delete({ where: { email } }); return prisma.user.delete({ where: { email } });
} }

0
app/routes/account.tsx Normal file
View file

View file

@ -1,12 +0,0 @@
import { Link } from "@remix-run/react";
export default function NoteIndexPage() {
return (
<p>
No note selected. Select a note on the left, or{" "}
<Link to="new" className="text-blue-500 underline">
create a new note.
</Link>
</p>
);
}

View file

@ -8,37 +8,42 @@ import {
} from "@remix-run/react"; } from "@remix-run/react";
import invariant from "tiny-invariant"; import invariant from "tiny-invariant";
import { deleteNote, getNote } from "~/models/note.server"; import { deleteTranslation, getTranslation } from "~/models/translation.server";
import { requireUserId } from "~/session.server"; import { requireUserId } from "~/session.server";
export const loader = async ({ params, request }: LoaderArgs) => { export const loader = async ({ params, request }: LoaderArgs) => {
const userId = await requireUserId(request); const userId = await requireUserId(request);
invariant(params.noteId, "noteId not found"); invariant(params.translationId, "translationId not found");
const note = await getNote({ id: params.noteId, userId }); const translation = await getTranslation({
if (!note) { id: params.translationId,
userId,
});
if (!translation) {
throw new Response("Not Found", { status: 404 }); throw new Response("Not Found", { status: 404 });
} }
return json({ note }); return json({ translation });
}; };
export const action = async ({ params, request }: ActionArgs) => { export const action = async ({ params, request }: ActionArgs) => {
const userId = await requireUserId(request); const userId = await requireUserId(request);
invariant(params.noteId, "noteId not found"); invariant(params.translationId, "translationId not found");
await deleteNote({ id: params.noteId, userId }); await deleteTranslation({ id: params.translationId, userId });
return redirect("/notes"); return redirect("/t");
}; };
export default function NoteDetailsPage() { export default function TranslationDetailsPage() {
const data = useLoaderData<typeof loader>(); const data = useLoaderData<typeof loader>();
return ( return (
<div> <div>
<h3 className="text-2xl font-bold">{data.note.title}</h3> <p className="text-xl font-bold">To: {data.translation.lang}</p>
<p className="py-6">{data.note.body}</p> <p className="py-6">{data.translation.text}</p>
<hr className="my-4" /> <hr className="my-4" />
<p>Result:</p>
<p className="py-6">{data.translation.result}</p>
<Form method="post"> <Form method="post">
<button <button
type="submit" type="submit"
@ -63,7 +68,7 @@ export function ErrorBoundary() {
} }
if (error.status === 404) { if (error.status === 404) {
return <div>Note not found</div>; return <div>Translation not found</div>;
} }
return <div>An unexpected error occurred: {error.statusText}</div>; return <div>An unexpected error occurred: {error.statusText}</div>;

13
app/routes/t._index.tsx Normal file
View file

@ -0,0 +1,13 @@
import { Link } from "@remix-run/react";
export default function TranslationsIndexPage() {
return (
<p>
No translation selected. Select one the left, or{" "}
<Link to="new" className="text-blue-500 underline">
start a new translation
</Link>
.
</p>
);
}

View file

@ -3,45 +3,47 @@ import { json, redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react"; import { Form, useActionData } from "@remix-run/react";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { createNote } from "~/models/note.server"; import { createTranslation } from "~/models/translation.server";
import { requireUserId } from "~/session.server"; import { requireUserId } from "~/session.server";
export const action = async ({ request }: ActionArgs) => { export const action = async ({ request }: ActionArgs) => {
const userId = await requireUserId(request); const userId = await requireUserId(request);
const formData = await request.formData(); const formData = await request.formData();
const title = formData.get("title"); const lang = formData.get("lang");
const body = formData.get("body"); const text = formData.get("text");
if (typeof title !== "string" || title.length === 0) { if (typeof lang !== "string" || lang.length === 0) {
return json( return json(
{ errors: { body: null, title: "Title is required" } }, { errors: { text: null, lang: "Lang is required" } },
{ status: 400 } { status: 400 }
); );
} }
if (typeof body !== "string" || body.length === 0) { if (typeof text !== "string" || text.length === 0) {
return json( return json(
{ errors: { body: "Body is required", title: null } }, { errors: { text: "Text is required", lang: null } },
{ status: 400 } { status: 400 }
); );
} }
const note = await createNote({ body, title, userId }); const result = "test";
return redirect(`/notes/${note.id}`); const t = await createTranslation({ lang, text, result, userId });
return redirect(`/t/${t.id}`);
}; };
export default function NewNotePage() { export default function NewTranslationPage() {
const actionData = useActionData<typeof action>(); const actionData = useActionData<typeof action>();
const titleRef = useRef<HTMLInputElement>(null); const langRef = useRef<HTMLInputElement>(null);
const bodyRef = useRef<HTMLTextAreaElement>(null); const textRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => { useEffect(() => {
if (actionData?.errors?.title) { if (actionData?.errors?.lang) {
titleRef.current?.focus(); langRef.current?.focus();
} else if (actionData?.errors?.body) { } else if (actionData?.errors?.text) {
bodyRef.current?.focus(); textRef.current?.focus();
} }
}, [actionData]); }, [actionData]);
@ -57,41 +59,41 @@ export default function NewNotePage() {
> >
<div> <div>
<label className="flex w-full flex-col gap-1"> <label className="flex w-full flex-col gap-1">
<span>Title: </span> <span>Translate to: </span>
<input <input
ref={titleRef} ref={langRef}
name="title" name="lang"
className="flex-1 rounded-md border-2 border-blue-500 px-3 text-lg leading-loose" className="flex-1 rounded-md border-2 border-blue-500 px-3 text-lg leading-loose"
aria-invalid={actionData?.errors?.title ? true : undefined} aria-invalid={actionData?.errors?.lang ? true : undefined}
aria-errormessage={ aria-errormessage={
actionData?.errors?.title ? "title-error" : undefined actionData?.errors?.lang ? "lang-error" : undefined
} }
/> />
</label> </label>
{actionData?.errors?.title ? ( {actionData?.errors?.lang ? (
<div className="pt-1 text-red-700" id="title-error"> <div className="pt-1 text-red-700" id="lang-error">
{actionData.errors.title} {actionData.errors.lang}
</div> </div>
) : null} ) : null}
</div> </div>
<div> <div>
<label className="flex w-full flex-col gap-1"> <label className="flex w-full flex-col gap-1">
<span>Body: </span> <span>Text: </span>
<textarea <textarea
ref={bodyRef} ref={textRef}
name="body" name="text"
rows={8} rows={8}
className="w-full flex-1 rounded-md border-2 border-blue-500 px-3 py-2 text-lg leading-6" className="w-full flex-1 rounded-md border-2 border-blue-500 px-3 py-2 text-lg leading-6"
aria-invalid={actionData?.errors?.body ? true : undefined} aria-invalid={actionData?.errors?.text ? true : undefined}
aria-errormessage={ aria-errormessage={
actionData?.errors?.body ? "body-error" : undefined actionData?.errors?.text ? "text-error" : undefined
} }
/> />
</label> </label>
{actionData?.errors?.body ? ( {actionData?.errors?.text ? (
<div className="pt-1 text-red-700" id="body-error"> <div className="pt-1 text-red-700" id="text-error">
{actionData.errors.body} {actionData.errors.text}
</div> </div>
) : null} ) : null}
</div> </div>
@ -101,7 +103,7 @@ export default function NewNotePage() {
type="submit" type="submit"
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400" className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
> >
Save Translate
</button> </button>
</div> </div>
</Form> </Form>

View file

@ -2,14 +2,14 @@ import type { LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node"; import { json } from "@remix-run/node";
import { Form, Link, NavLink, Outlet, useLoaderData } from "@remix-run/react"; import { Form, Link, NavLink, Outlet, useLoaderData } from "@remix-run/react";
import { getNoteListItems } from "~/models/note.server"; import { getTranslationsListItems } from "~/models/translation.server";
import { requireUserId } from "~/session.server"; import { requireUserId } from "~/session.server";
import { useUser } from "~/utils"; import { useUser } from "~/utils";
export const loader = async ({ request }: LoaderArgs) => { export const loader = async ({ request }: LoaderArgs) => {
const userId = await requireUserId(request); const userId = await requireUserId(request);
const noteListItems = await getNoteListItems({ userId }); const translations = await getTranslationsListItems({ userId });
return json({ noteListItems }); return json({ translations });
}; };
export default function NotesPage() { export default function NotesPage() {
@ -19,41 +19,48 @@ export default function NotesPage() {
return ( return (
<div className="flex h-full min-h-screen flex-col"> <div className="flex h-full min-h-screen flex-col">
<header className="flex items-center justify-between bg-slate-800 p-4 text-white"> <header className="flex items-center justify-between bg-slate-800 p-4 text-white">
<h1 className="text-3xl font-bold"> <h1 className="mr-auto text-3xl font-bold">
<Link to=".">Notes</Link> <Link to=".">Translations</Link>
</h1> </h1>
<p>{user.email}</p> <div className="ml-auto flex items-center">
<Form action="/logout" method="post"> <Link
<button to="/account"
type="submit" className="mr-2 rounded bg-slate-600 px-4 py-2 text-blue-100 hover:bg-blue-500 active:bg-blue-600"
className="rounded bg-slate-600 px-4 py-2 text-blue-100 hover:bg-blue-500 active:bg-blue-600"
> >
Logout Account
</button> </Link>
</Form> <Form action="/logout" method="post">
<button
type="submit"
className="rounded bg-slate-600 px-4 py-2 text-blue-100 hover:bg-blue-500 active:bg-blue-600"
>
Logout
</button>
</Form>
</div>
</header> </header>
<main className="flex h-full bg-white"> <main className="flex h-full bg-white">
<div className="h-full w-80 border-r bg-gray-50"> <div className="h-full w-80 border-r bg-gray-50">
<Link to="new" className="block p-4 text-xl text-blue-500"> <Link to="new" className="block p-4 text-xl text-blue-500">
+ New Note + New Translation
</Link> </Link>
<hr /> <hr />
{data.noteListItems.length === 0 ? ( {data.translations.length === 0 ? (
<p className="p-4">No notes yet</p> <p className="p-4">No translations yet</p>
) : ( ) : (
<ol> <ol>
{data.noteListItems.map((note) => ( {data.translations.map((t) => (
<li key={note.id}> <li key={t.id}>
<NavLink <NavLink
className={({ isActive }) => className={({ isActive }) =>
`block border-b p-4 text-xl ${isActive ? "bg-white" : ""}` `block border-b p-4 text-xl ${isActive ? "bg-white" : ""}`
} }
to={note.id} to={t.id}
> >
📝 {note.title} [{t.lang}] {t.text}
</NavLink> </NavLink>
</li> </li>
))} ))}