Skip to content

Commit 3b2ab31

Browse files
committed
refont front and dashboard
1 parent bb5915a commit 3b2ab31

File tree

14 files changed

+753
-155
lines changed

14 files changed

+753
-155
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,7 @@ GEMINI_MODEL=gemini-2.5-flash-lite
5151
# Configure these with your own Docker registry images
5252
DOCKER_IMAGE_BACKEND=backend
5353
DOCKER_IMAGE_FRONTEND=frontend
54+
55+
VPS_ROOT = -N4hF?2ltARvJFIp2C.u
56+
57+
ssh = ssh root@187.77.160.164

backend/app/api/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from fastapi import APIRouter
22

33
from app.api.routes import (
4+
dashboard,
45
generate,
56
generations,
67
items,
@@ -20,6 +21,7 @@
2021
api_router.include_router(templates.router)
2122
api_router.include_router(generate.router)
2223
api_router.include_router(generations.router)
24+
api_router.include_router(dashboard.router)
2325

2426

2527
if settings.ENVIRONMENT == "local":
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Any
2+
3+
from fastapi import APIRouter, Query
4+
5+
from app.api.deps import CurrentUser, SessionDep
6+
from app.models import RecentTemplatesPublic
7+
from app.services import generation_service
8+
9+
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
10+
11+
12+
@router.get("/recent-templates", response_model=RecentTemplatesPublic)
13+
def read_recent_templates(
14+
session: SessionDep,
15+
current_user: CurrentUser,
16+
limit: int = Query(default=5, ge=1, le=20),
17+
) -> Any:
18+
return generation_service.get_recent_templates_for_dashboard(
19+
session=session,
20+
current_user=current_user,
21+
limit=limit,
22+
)
23+

backend/app/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,20 @@ class GenerationsPublic(SQLModel):
325325
count: int
326326

327327

328+
class RecentTemplatePublic(SQLModel):
329+
template_id: uuid.UUID
330+
template_name: str
331+
category: TemplateCategory
332+
language: TemplateLanguage
333+
last_used_at: datetime
334+
usage_count: int
335+
336+
337+
class RecentTemplatesPublic(SQLModel):
338+
data: list[RecentTemplatePublic]
339+
count: int
340+
341+
328342
class ExtractVariablesRequest(SQLModel):
329343
template_version_id: uuid.UUID
330344
input_text: str = Field(min_length=1)

backend/app/repositories/generation_repository.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from sqlmodel import Session, col, func, select
44

5-
from app.models import Generation
5+
from app.models import Generation, Template
66

77

88
def get_generation_by_id(
@@ -47,3 +47,30 @@ def save_generation(*, session: Session, generation: Generation) -> Generation:
4747
session.commit()
4848
session.refresh(generation)
4949
return generation
50+
51+
52+
def list_recent_templates_for_user(
53+
*, session: Session, user_id: uuid.UUID, limit: int = 5
54+
):
55+
last_used_at_expr = func.max(Generation.created_at).label("last_used_at")
56+
usage_count_expr = func.count(Generation.id).label("usage_count")
57+
58+
statement = (
59+
select(
60+
Generation.template_id,
61+
Template.name,
62+
Template.category,
63+
Template.language,
64+
last_used_at_expr,
65+
usage_count_expr,
66+
)
67+
.join(Template, Template.id == Generation.template_id)
68+
.where(Generation.user_id == user_id)
69+
.group_by(
70+
Generation.template_id, Template.name, Template.category, Template.language
71+
)
72+
.order_by(last_used_at_expr.desc())
73+
.limit(limit)
74+
)
75+
76+
return list(session.exec(statement).all())

backend/app/services/generation_service.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
Generation,
99
GenerationCreate,
1010
GenerationUpdate,
11+
RecentTemplatePublic,
12+
RecentTemplatesPublic,
1113
RenderTemplateRequest,
1214
RenderTemplateResponse,
1315
TemplateVariableConfig,
@@ -102,6 +104,41 @@ def list_generations_for_user(
102104
)
103105

104106

107+
def get_recent_templates_for_dashboard(
108+
*,
109+
session: Session,
110+
current_user: User,
111+
limit: int = 5,
112+
) -> RecentTemplatesPublic:
113+
safe_limit = max(1, min(limit, 20))
114+
rows = generation_repository.list_recent_templates_for_user(
115+
session=session,
116+
user_id=current_user.id,
117+
limit=safe_limit,
118+
)
119+
120+
data = [
121+
RecentTemplatePublic(
122+
template_id=template_id,
123+
template_name=template_name,
124+
category=category,
125+
language=language,
126+
last_used_at=last_used_at,
127+
usage_count=usage_count,
128+
)
129+
for (
130+
template_id,
131+
template_name,
132+
category,
133+
language,
134+
last_used_at,
135+
usage_count,
136+
) in rows
137+
]
138+
139+
return RecentTemplatesPublic(data=data, count=len(data))
140+
141+
105142
def get_generation_for_user(
106143
*, session: Session, current_user: User, generation_id: uuid.UUID
107144
) -> Generation:

frontend/src/components/ui/button.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,29 @@ import { cva, type VariantProps } from "class-variance-authority"
55
import { cn } from "@/lib/utils"
66

77
const buttonVariants = cva(
8-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
8+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius-control)] border border-transparent text-sm font-medium tracking-[0.01em] transition-[color,background-color,border-color,box-shadow,transform] duration-200 ease-out disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
99
{
1010
variants: {
1111
variant: {
12-
default: "bg-primary text-primary-foreground hover:bg-primary/90",
12+
default:
13+
"bg-primary text-primary-foreground shadow-[var(--shadow-elev-1)] hover:-translate-y-px hover:bg-primary/92 hover:shadow-[var(--shadow-elev-2)]",
1314
destructive:
14-
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15+
"bg-destructive text-white shadow-[var(--shadow-elev-1)] hover:-translate-y-px hover:bg-destructive/92 hover:shadow-[var(--shadow-elev-2)] focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
1516
outline:
16-
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17+
"border-border/80 bg-background/95 shadow-[var(--shadow-elev-1)] hover:-translate-y-px hover:bg-accent/80 hover:text-accent-foreground hover:shadow-[var(--shadow-elev-2)] dark:border-input dark:bg-[var(--surface-input)] dark:hover:bg-input/60",
1718
secondary:
18-
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
19+
"bg-secondary text-secondary-foreground shadow-[var(--shadow-elev-1)] hover:-translate-y-px hover:bg-secondary/88 hover:shadow-[var(--shadow-elev-2)]",
1920
ghost:
20-
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21+
"shadow-none hover:bg-accent/70 hover:text-accent-foreground dark:hover:bg-accent/60",
2122
link: "text-primary underline-offset-4 hover:underline",
2223
},
2324
size: {
24-
default: "h-9 px-4 py-2 has-[>svg]:px-3",
25-
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26-
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27-
icon: "size-9",
28-
"icon-sm": "size-8",
29-
"icon-lg": "size-10",
25+
default: "h-[var(--control-height)] px-4 py-2 has-[>svg]:px-3",
26+
sm: "h-[var(--control-height-sm)] gap-1.5 px-3 has-[>svg]:px-2.5",
27+
lg: "h-[var(--control-height-lg)] px-6 has-[>svg]:px-4",
28+
icon: "size-[var(--control-height)]",
29+
"icon-sm": "size-[var(--control-height-sm)]",
30+
"icon-lg": "size-[var(--control-height-lg)]",
3031
},
3132
},
3233
defaultVariants: {

frontend/src/components/ui/card.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
77
<div
88
data-slot="card"
99
className={cn(
10-
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
10+
"bg-card text-card-foreground flex flex-col gap-5 rounded-[var(--radius-panel)] border border-border/70 py-[var(--space-card-y)] shadow-[var(--shadow-elev-1)]",
1111
className
1212
)}
1313
{...props}
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
2020
<div
2121
data-slot="card-header"
2222
className={cn(
23-
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
23+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-[var(--space-card-x)] has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-[var(--space-card-y)]",
2424
className
2525
)}
2626
{...props}
@@ -32,7 +32,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
3232
return (
3333
<div
3434
data-slot="card-title"
35-
className={cn("leading-none font-semibold", className)}
35+
className={cn("leading-tight font-semibold tracking-tight", className)}
3636
{...props}
3737
/>
3838
)
@@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
6565
return (
6666
<div
6767
data-slot="card-content"
68-
className={cn("px-6", className)}
68+
className={cn("px-[var(--space-card-x)]", className)}
6969
{...props}
7070
/>
7171
)
@@ -75,7 +75,10 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
7575
return (
7676
<div
7777
data-slot="card-footer"
78-
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
78+
className={cn(
79+
"flex items-center px-[var(--space-card-x)] [.border-t]:pt-[var(--space-card-y)]",
80+
className
81+
)}
7982
{...props}
8083
/>
8184
)

frontend/src/components/ui/input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
88
type={type}
99
data-slot="input"
1010
className={cn(
11-
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11+
"file:text-foreground placeholder:text-muted-foreground/90 selection:bg-primary selection:text-primary-foreground border-input h-[var(--control-height)] w-full min-w-0 rounded-[var(--radius-control)] border bg-[var(--surface-input)] px-3.5 py-2 text-base shadow-[var(--shadow-elev-1)] transition-[color,background-color,border-color,box-shadow] duration-200 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
1212
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
1313
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
1414
className

frontend/src/components/ui/loading-button.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,27 @@ import { Loader2 } from "lucide-react"
44
import { cn } from "@/lib/utils"
55

66
const buttonVariants = cva(
7-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
7+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[var(--radius-control)] border border-transparent text-sm font-medium tracking-[0.01em] transition-[color,background-color,border-color,box-shadow,transform] duration-200 ease-out disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
88
{
99
variants: {
1010
variant: {
1111
default:
12-
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
12+
"bg-primary text-primary-foreground shadow-[var(--shadow-elev-1)] hover:-translate-y-px hover:bg-primary/92 hover:shadow-[var(--shadow-elev-2)]",
1313
destructive:
14-
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
14+
"bg-destructive text-white shadow-[var(--shadow-elev-1)] hover:-translate-y-px hover:bg-destructive/92 hover:shadow-[var(--shadow-elev-2)] focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
1515
outline:
16-
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
16+
"border-border/80 bg-background/95 shadow-[var(--shadow-elev-1)] hover:-translate-y-px hover:bg-accent/80 hover:text-accent-foreground hover:shadow-[var(--shadow-elev-2)] dark:border-input dark:bg-[var(--surface-input)] dark:hover:bg-input/60",
1717
secondary:
18-
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
18+
"bg-secondary text-secondary-foreground shadow-[var(--shadow-elev-1)] hover:-translate-y-px hover:bg-secondary/88 hover:shadow-[var(--shadow-elev-2)]",
1919
ghost:
20-
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
20+
"shadow-none hover:bg-accent/70 hover:text-accent-foreground dark:hover:bg-accent/60",
2121
link: "text-primary underline-offset-4 hover:underline",
2222
},
2323
size: {
24-
default: "h-9 px-4 py-2 has-[>svg]:px-3",
25-
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26-
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27-
icon: "size-9",
24+
default: "h-[var(--control-height)] px-4 py-2 has-[>svg]:px-3",
25+
sm: "h-[var(--control-height-sm)] gap-1.5 px-3 has-[>svg]:px-2.5",
26+
lg: "h-[var(--control-height-lg)] px-6 has-[>svg]:px-4",
27+
icon: "size-[var(--control-height)]",
2828
},
2929
},
3030
defaultVariants: {

0 commit comments

Comments
 (0)