feat(cover): Display generated cover letters

This commit is contained in:
Leons Aleksandrovs
2025-07-12 17:11:11 +03:00
parent ad822f3abc
commit d0de05e0a2
8 changed files with 120 additions and 4 deletions

View File

@@ -29,7 +29,7 @@ func Get(c *gin.Context) {
return return
} }
covers, err := cover.Get("user_id = $1", user.Id) covers, err := cover.Get("user_id = $1 ORDER BY created_at DESC", user.Id)
if err != nil { if err != nil {
res.Error(c, err.Error(), http.StatusInternalServerError) res.Error(c, err.Error(), http.StatusInternalServerError)
return return

View File

@@ -69,7 +69,7 @@ func Get(c *gin.Context) {
} }
// Get all user templates // Get all user templates
templates, err := template.Get("user_id = $1", user.Id) templates, err := template.Get("user_id = $1 ORDER BY created_at DESC", user.Id)
if err != nil { if err != nil {
res.Error(c, err.Error(), http.StatusInternalServerError) res.Error(c, err.Error(), http.StatusInternalServerError)
return return

View File

@@ -1,3 +1,3 @@
export default function Container({ children, className = "" }: React.ComponentProps<"div">) { export default function Container({ children, className = "" }: React.ComponentProps<"div">) {
return <div className={`container mx-auto max-w-6xl px-4 ${className}`}>{children}</div>; return <div className={`container mx-auto max-w-6xl px-4 mb-16 ${className}`}>{children}</div>;
} }

View File

@@ -24,6 +24,11 @@
margin-top: 0; margin-top: 0;
} }
/* Text */
p {
margin-top: 0.5rem;
}
/* Links */ /* Links */
a { a {
color: var(--editor-accent); color: var(--editor-accent);

View File

@@ -15,6 +15,7 @@ import { Route as IndexRouteImport } from './routes/index'
import { Route as TemplatesIndexRouteImport } from './routes/templates/index' import { Route as TemplatesIndexRouteImport } from './routes/templates/index'
import { Route as TemplatesCreateRouteImport } from './routes/templates/create' import { Route as TemplatesCreateRouteImport } from './routes/templates/create'
import { Route as CoverCreateRouteImport } from './routes/cover/create' import { Route as CoverCreateRouteImport } from './routes/cover/create'
import { Route as CoverCoverIdRouteImport } from './routes/cover/$coverId'
const RegisterRoute = RegisterRouteImport.update({ const RegisterRoute = RegisterRouteImport.update({
id: '/register', id: '/register',
@@ -46,11 +47,17 @@ const CoverCreateRoute = CoverCreateRouteImport.update({
path: '/cover/create', path: '/cover/create',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const CoverCoverIdRoute = CoverCoverIdRouteImport.update({
id: '/cover/$coverId',
path: '/cover/$coverId',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/register': typeof RegisterRoute '/register': typeof RegisterRoute
'/cover/$coverId': typeof CoverCoverIdRoute
'/cover/create': typeof CoverCreateRoute '/cover/create': typeof CoverCreateRoute
'/templates/create': typeof TemplatesCreateRoute '/templates/create': typeof TemplatesCreateRoute
'/templates': typeof TemplatesIndexRoute '/templates': typeof TemplatesIndexRoute
@@ -59,6 +66,7 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/register': typeof RegisterRoute '/register': typeof RegisterRoute
'/cover/$coverId': typeof CoverCoverIdRoute
'/cover/create': typeof CoverCreateRoute '/cover/create': typeof CoverCreateRoute
'/templates/create': typeof TemplatesCreateRoute '/templates/create': typeof TemplatesCreateRoute
'/templates': typeof TemplatesIndexRoute '/templates': typeof TemplatesIndexRoute
@@ -68,6 +76,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
'/register': typeof RegisterRoute '/register': typeof RegisterRoute
'/cover/$coverId': typeof CoverCoverIdRoute
'/cover/create': typeof CoverCreateRoute '/cover/create': typeof CoverCreateRoute
'/templates/create': typeof TemplatesCreateRoute '/templates/create': typeof TemplatesCreateRoute
'/templates/': typeof TemplatesIndexRoute '/templates/': typeof TemplatesIndexRoute
@@ -78,6 +87,7 @@ export interface FileRouteTypes {
| '/' | '/'
| '/login' | '/login'
| '/register' | '/register'
| '/cover/$coverId'
| '/cover/create' | '/cover/create'
| '/templates/create' | '/templates/create'
| '/templates' | '/templates'
@@ -86,6 +96,7 @@ export interface FileRouteTypes {
| '/' | '/'
| '/login' | '/login'
| '/register' | '/register'
| '/cover/$coverId'
| '/cover/create' | '/cover/create'
| '/templates/create' | '/templates/create'
| '/templates' | '/templates'
@@ -94,6 +105,7 @@ export interface FileRouteTypes {
| '/' | '/'
| '/login' | '/login'
| '/register' | '/register'
| '/cover/$coverId'
| '/cover/create' | '/cover/create'
| '/templates/create' | '/templates/create'
| '/templates/' | '/templates/'
@@ -103,6 +115,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
LoginRoute: typeof LoginRoute LoginRoute: typeof LoginRoute
RegisterRoute: typeof RegisterRoute RegisterRoute: typeof RegisterRoute
CoverCoverIdRoute: typeof CoverCoverIdRoute
CoverCreateRoute: typeof CoverCreateRoute CoverCreateRoute: typeof CoverCreateRoute
TemplatesCreateRoute: typeof TemplatesCreateRoute TemplatesCreateRoute: typeof TemplatesCreateRoute
TemplatesIndexRoute: typeof TemplatesIndexRoute TemplatesIndexRoute: typeof TemplatesIndexRoute
@@ -152,6 +165,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof CoverCreateRouteImport preLoaderRoute: typeof CoverCreateRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/cover/$coverId': {
id: '/cover/$coverId'
path: '/cover/$coverId'
fullPath: '/cover/$coverId'
preLoaderRoute: typeof CoverCoverIdRouteImport
parentRoute: typeof rootRouteImport
}
} }
} }
@@ -159,6 +179,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
LoginRoute: LoginRoute, LoginRoute: LoginRoute,
RegisterRoute: RegisterRoute, RegisterRoute: RegisterRoute,
CoverCoverIdRoute: CoverCoverIdRoute,
CoverCreateRoute: CoverCreateRoute, CoverCreateRoute: CoverCreateRoute,
TemplatesCreateRoute: TemplatesCreateRoute, TemplatesCreateRoute: TemplatesCreateRoute,
TemplatesIndexRoute: TemplatesIndexRoute, TemplatesIndexRoute: TemplatesIndexRoute,

View File

@@ -0,0 +1,48 @@
import renderQueryState from "@/components/RenderQueryState";
import Authorised from "@/layouts/Authorised";
import requests from "@/lib/requests";
import type { CoverLetter } from "@/types/api";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
import "../../editor.css";
export const Route = createFileRoute("/cover/$coverId")({
component: RouteComponent,
});
function RouteComponent() {
const { coverId } = Route.useParams();
const cover = useQuery({
queryKey: ["cover", coverId],
queryFn: () => requests.get<{ cover: CoverLetter }>(`/cover/${coverId}`, {}),
});
const coverState = renderQueryState({
query: cover,
noFound: "cover letter",
skeleton: {
count: 1,
className: "h-[400px]",
},
});
return (
<Authorised>
<div>
<h1 className="text-2xl font-semibold">{cover.data?.cover.name || "Loading..."}</h1>
{/* edit buttons */}
</div>
<div className="mt-8 p-4 border rounded-md">
{coverState !== null ? (
coverState
) : (
<div
className="tiptap"
dangerouslySetInnerHTML={{ __html: cover.data?.cover.letter || "" }}
/>
)}
</div>
</Authorised>
);
}

View File

@@ -2,16 +2,31 @@ import { createFileRoute, Link } from "@tanstack/react-router";
import Authorised from "@/layouts/Authorised"; import Authorised from "@/layouts/Authorised";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import requests from "@/lib/requests";
import type { CoverLetterPreview } from "@/types/api";
import renderQueryState from "@/components/RenderQueryState";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: App, component: App,
}); });
function App() { function App() {
const letters = useQuery({
queryKey: ["cover_letters"],
queryFn: () => requests.get<{ covers: CoverLetterPreview[] }>("/cover", {}),
});
const lettersState = renderQueryState({
query: letters,
noFound: "cover letters",
});
return ( return (
<Authorised> <Authorised>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h1 className="text-2xl font-bold text-primary">0 Cover letters</h1> <h1 className="text-2xl font-bold text-primary">
{letters.data?.covers.length} Cover letters
</h1>
<Link to="/cover/create"> <Link to="/cover/create">
<Button icon={<Plus />} variant="secondary"> <Button icon={<Plus />} variant="secondary">
@@ -19,6 +34,21 @@ function App() {
</Button> </Button>
</Link> </Link>
</div> </div>
<div className="flex flex-col gap-2 mt-4">
{lettersState !== null
? lettersState
: letters.data?.covers.map((l) => (
<Link
className="px-3 py-2 cursor-pointer rounded hover:bg-secondary"
to={"/cover/$coverId"}
params={{ coverId: l.id.toString() }}
key={l.id}
>
<p>{l.name}</p>
</Link>
))}
</div>
</Authorised> </Authorised>
); );
} }

View File

@@ -27,3 +27,15 @@ export interface Template {
template: string; template: string;
created_at: string; created_at: string;
} }
// -------- Cover letters --------
export interface CoverLetterPreview {
id: number;
name: string;
}
export interface CoverLetter extends CoverLetterPreview {
user_id: number;
letter: string;
created_at: string;
}