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"]) {
|
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
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";
|
} 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
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 { 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>
|
||||||
|
|
@ -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,10 +19,16 @@ 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">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Account
|
||||||
|
</Link>
|
||||||
<Form action="/logout" method="post">
|
<Form action="/logout" method="post">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
@ -31,29 +37,30 @@ export default function NotesPage() {
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</Form>
|
</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>
|
||||||
))}
|
))}
|
||||||
Loading…
Reference in a new issue