feat: adjust routes for prj data and typings
This commit is contained in:
parent
efe8868c1d
commit
83300a4701
|
|
@ -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 },
|
||||
});
|
||||
}
|
||||
52
app/models/translation.server.ts
Normal file
52
app/models/translation.server.ts
Normal 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 },
|
||||
});
|
||||
}
|
||||
|
|
@ -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"]) {
|
||||
return prisma.user.delete({ where: { email } });
|
||||
}
|
||||
|
|
|
|||
0
app/routes/account.tsx
Normal file
0
app/routes/account.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,37 +8,42 @@ import {
|
|||
} from "@remix-run/react";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
import { deleteNote, getNote } from "~/models/note.server";
|
||||
import { deleteTranslation, getTranslation } from "~/models/translation.server";
|
||||
import { requireUserId } from "~/session.server";
|
||||
|
||||
export const loader = async ({ params, request }: LoaderArgs) => {
|
||||
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 });
|
||||
if (!note) {
|
||||
const translation = await getTranslation({
|
||||
id: params.translationId,
|
||||
userId,
|
||||
});
|
||||
if (!translation) {
|
||||
throw new Response("Not Found", { status: 404 });
|
||||
}
|
||||
return json({ note });
|
||||
return json({ translation });
|
||||
};
|
||||
|
||||
export const action = async ({ params, request }: ActionArgs) => {
|
||||
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>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold">{data.note.title}</h3>
|
||||
<p className="py-6">{data.note.body}</p>
|
||||
<p className="text-xl font-bold">To: {data.translation.lang}</p>
|
||||
<p className="py-6">{data.translation.text}</p>
|
||||
<hr className="my-4" />
|
||||
<p>Result:</p>
|
||||
<p className="py-6">{data.translation.result}</p>
|
||||
<Form method="post">
|
||||
<button
|
||||
type="submit"
|
||||
|
|
@ -63,7 +68,7 @@ export function ErrorBoundary() {
|
|||
}
|
||||
|
||||
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>;
|
||||
13
app/routes/t._index.tsx
Normal file
13
app/routes/t._index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,45 +3,47 @@ import { json, redirect } from "@remix-run/node";
|
|||
import { Form, useActionData } from "@remix-run/react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { createNote } from "~/models/note.server";
|
||||
import { createTranslation } from "~/models/translation.server";
|
||||
import { requireUserId } from "~/session.server";
|
||||
|
||||
export const action = async ({ request }: ActionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
const title = formData.get("title");
|
||||
const body = formData.get("body");
|
||||
const lang = formData.get("lang");
|
||||
const text = formData.get("text");
|
||||
|
||||
if (typeof title !== "string" || title.length === 0) {
|
||||
if (typeof lang !== "string" || lang.length === 0) {
|
||||
return json(
|
||||
{ errors: { body: null, title: "Title is required" } },
|
||||
{ errors: { text: null, lang: "Lang is required" } },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof body !== "string" || body.length === 0) {
|
||||
if (typeof text !== "string" || text.length === 0) {
|
||||
return json(
|
||||
{ errors: { body: "Body is required", title: null } },
|
||||
{ errors: { text: "Text is required", lang: null } },
|
||||
{ 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 titleRef = useRef<HTMLInputElement>(null);
|
||||
const bodyRef = useRef<HTMLTextAreaElement>(null);
|
||||
const langRef = useRef<HTMLInputElement>(null);
|
||||
const textRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (actionData?.errors?.title) {
|
||||
titleRef.current?.focus();
|
||||
} else if (actionData?.errors?.body) {
|
||||
bodyRef.current?.focus();
|
||||
if (actionData?.errors?.lang) {
|
||||
langRef.current?.focus();
|
||||
} else if (actionData?.errors?.text) {
|
||||
textRef.current?.focus();
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
|
|
@ -57,41 +59,41 @@ export default function NewNotePage() {
|
|||
>
|
||||
<div>
|
||||
<label className="flex w-full flex-col gap-1">
|
||||
<span>Title: </span>
|
||||
<span>Translate to: </span>
|
||||
<input
|
||||
ref={titleRef}
|
||||
name="title"
|
||||
ref={langRef}
|
||||
name="lang"
|
||||
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={
|
||||
actionData?.errors?.title ? "title-error" : undefined
|
||||
actionData?.errors?.lang ? "lang-error" : undefined
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{actionData?.errors?.title ? (
|
||||
<div className="pt-1 text-red-700" id="title-error">
|
||||
{actionData.errors.title}
|
||||
{actionData?.errors?.lang ? (
|
||||
<div className="pt-1 text-red-700" id="lang-error">
|
||||
{actionData.errors.lang}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex w-full flex-col gap-1">
|
||||
<span>Body: </span>
|
||||
<span>Text: </span>
|
||||
<textarea
|
||||
ref={bodyRef}
|
||||
name="body"
|
||||
ref={textRef}
|
||||
name="text"
|
||||
rows={8}
|
||||
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={
|
||||
actionData?.errors?.body ? "body-error" : undefined
|
||||
actionData?.errors?.text ? "text-error" : undefined
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{actionData?.errors?.body ? (
|
||||
<div className="pt-1 text-red-700" id="body-error">
|
||||
{actionData.errors.body}
|
||||
{actionData?.errors?.text ? (
|
||||
<div className="pt-1 text-red-700" id="text-error">
|
||||
{actionData.errors.text}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
@ -101,7 +103,7 @@ export default function NewNotePage() {
|
|||
type="submit"
|
||||
className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
|
||||
>
|
||||
Save
|
||||
Translate
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
@ -2,14 +2,14 @@ import type { LoaderArgs } from "@remix-run/node";
|
|||
import { json } from "@remix-run/node";
|
||||
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 { useUser } from "~/utils";
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const noteListItems = await getNoteListItems({ userId });
|
||||
return json({ noteListItems });
|
||||
const translations = await getTranslationsListItems({ userId });
|
||||
return json({ translations });
|
||||
};
|
||||
|
||||
export default function NotesPage() {
|
||||
|
|
@ -19,41 +19,48 @@ export default function NotesPage() {
|
|||
return (
|
||||
<div className="flex h-full min-h-screen flex-col">
|
||||
<header className="flex items-center justify-between bg-slate-800 p-4 text-white">
|
||||
<h1 className="text-3xl font-bold">
|
||||
<Link to=".">Notes</Link>
|
||||
<h1 className="mr-auto text-3xl font-bold">
|
||||
<Link to=".">Translations</Link>
|
||||
</h1>
|
||||
<p>{user.email}</p>
|
||||
<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"
|
||||
<div className="ml-auto flex items-center">
|
||||
<Link
|
||||
to="/account"
|
||||
className="mr-2 rounded bg-slate-600 px-4 py-2 text-blue-100 hover:bg-blue-500 active:bg-blue-600"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</Form>
|
||||
Account
|
||||
</Link>
|
||||
<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>
|
||||
|
||||
<main className="flex h-full bg-white">
|
||||
<div className="h-full w-80 border-r bg-gray-50">
|
||||
<Link to="new" className="block p-4 text-xl text-blue-500">
|
||||
+ New Note
|
||||
+ New Translation
|
||||
</Link>
|
||||
|
||||
<hr />
|
||||
|
||||
{data.noteListItems.length === 0 ? (
|
||||
<p className="p-4">No notes yet</p>
|
||||
{data.translations.length === 0 ? (
|
||||
<p className="p-4">No translations yet</p>
|
||||
) : (
|
||||
<ol>
|
||||
{data.noteListItems.map((note) => (
|
||||
<li key={note.id}>
|
||||
{data.translations.map((t) => (
|
||||
<li key={t.id}>
|
||||
<NavLink
|
||||
className={({ isActive }) =>
|
||||
`block border-b p-4 text-xl ${isActive ? "bg-white" : ""}`
|
||||
}
|
||||
to={note.id}
|
||||
to={t.id}
|
||||
>
|
||||
📝 {note.title}
|
||||
[{t.lang}] {t.text}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
Loading…
Reference in a new issue