Skip to content

Commit 5a38278

Browse files
[RTY-260028]: fix qr outside the app and add 404 page for non-defined urls
1 parent 6eb1057 commit 5a38278

File tree

13 files changed

+261
-224
lines changed

13 files changed

+261
-224
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ poetry.lock
5959
*.tmp
6060
*.temp
6161
*.bak
62+
63+
64+
assets/images/qr/*

app/main.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
from contextlib import asynccontextmanager
33
from pathlib import Path
44
import logging
5-
import traceback
65
import asyncio
76

87
from fastapi import FastAPI, Request
9-
from fastapi.responses import JSONResponse
8+
9+
# from fastapi.responses import JSONResponse
1010
from fastapi.staticfiles import StaticFiles
1111
from starlette.middleware.sessions import SessionMiddleware
1212

13+
# from fastapi.exceptions import RequestValidationError
14+
# from starlette.exceptions import HTTPException as StarletteHTTPException
15+
from fastapi.templating import Jinja2Templates
16+
1317
from app.routes import ui_router
1418
from app.utils import db
1519
from app.utils.cache import cleanup_expired
@@ -90,22 +94,36 @@ async def lifespan(app: FastAPI):
9094

9195
app = FastAPI(title="TinyURL", lifespan=lifespan)
9296
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET)
97+
templates = Jinja2Templates(directory="app/templates")
9398

9499
BASE_DIR = Path(__file__).resolve().parent
95100
STATIC_DIR = BASE_DIR / "static"
96101

97102
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
98103

104+
# QR codes are now served from /qr, which maps to assets/images/qr in the project root
105+
PROJECT_ROOT = BASE_DIR.parent
106+
QR_DIR = PROJECT_ROOT / "assets" / "images" / "qr"
107+
app.mount("/qr", StaticFiles(directory=QR_DIR), name="qr")
99108

100109
# -----------------------------
101110
# Global error handler
102111
# -----------------------------
103-
@app.exception_handler(Exception)
104-
async def global_exception_handler(request: Request, exc: Exception):
105-
traceback.print_exc()
106-
return JSONResponse(
107-
status_code=500,
108-
content={"success": False, "error": "INTERNAL_SERVER_ERROR"},
112+
# app.exception_handler(Exception)
113+
# sync def global_exception_handler(request: Request, exc: Exception):
114+
# traceback.print_exc()
115+
# return JSONResponse(
116+
# status_code=500,
117+
# content={"success": False, "error": "INTERNAL_SERVER_ERROR"},
118+
# )
119+
120+
121+
@app.exception_handler(404)
122+
async def custom_404_handler(request: Request, exc):
123+
return templates.TemplateResponse(
124+
"404.html",
125+
{"request": request},
126+
status_code=404,
109127
)
110128

111129

app/routes.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from fastapi.templating import Jinja2Templates
2323
from pydantic import BaseModel, Field
2424

25+
2526
from app import __version__
2627
from app.utils import db
2728
from app.utils.cache import (
@@ -68,10 +69,11 @@ async def index(request: Request):
6869
if qr_enabled and new_short_url and short_code:
6970
qr_data = new_short_url if qr_type == "short" else original_url
7071
qr_filename = f"{short_code}.png"
71-
qr_dir = BASE_DIR / "static" / "qr"
72+
PROJECT_ROOT = BASE_DIR.parent # go from app/ → project root
73+
qr_dir = PROJECT_ROOT / "assets" / "images" / "qr"
7274
qr_dir.mkdir(parents=True, exist_ok=True)
7375
generate_qr_with_logo(qr_data, str(qr_dir / qr_filename))
74-
qr_image = f"/static/qr/{qr_filename}"
76+
qr_image = f"/qr/{qr_filename}"
7577

7678
recent_urls = db.get_recent_urls(MAX_RECENT_URLS) or get_recent_from_cache(
7779
MAX_RECENT_URLS
@@ -139,6 +141,7 @@ async def create_short_url(
139141

140142

141143
@ui_router.get("/recent", response_class=HTMLResponse)
144+
@ui_router.get("/history", response_class=HTMLResponse)
142145
async def recent_urls(request: Request):
143146
recent_urls_list = db.get_recent_urls(MAX_RECENT_URLS) or get_recent_from_cache(
144147
MAX_RECENT_URLS
@@ -223,7 +226,8 @@ def redirect_short_ui(short_code: str, background_tasks: BackgroundTasks):
223226
set_cache_pair(short_code, original_url)
224227
return RedirectResponse(original_url)
225228

226-
return PlainTextResponse("Invalid short URL", status_code=404)
229+
# return PlainTextResponse("Invalid short URL", status_code=404)
230+
raise HTTPException(status_code=404, detail="Page not found")
227231

228232

229233
@ui_router.delete("/recent/{short_code}")

app/static/css/tiny.css

Lines changed: 137 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ body.light-theme {
6767
z-index: 1000;
6868
}
6969

70+
body.light-theme .app-header {
71+
background: #ffffff;
72+
/* solid background */
73+
border-bottom: 1px solid #e5e7eb;
74+
/* clear separation */
75+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
76+
}
77+
7078
.header-left,
7179
.header-right {
7280
display: flex;
@@ -129,6 +137,26 @@ body.dark-theme .app-header {
129137
background: #111827;
130138
}
131139

140+
.nav-link {
141+
position: relative;
142+
}
143+
144+
.nav-link::after {
145+
content: "";
146+
position: absolute;
147+
left: 0;
148+
bottom: -4px;
149+
width: 0;
150+
height: 2px;
151+
background: var(--text-primary);
152+
transition: width 0.3s;
153+
}
154+
155+
.nav-link.active::after {
156+
width: 100%;
157+
}
158+
159+
132160
.dark-theme .nav-link.active::after {
133161
background: #f8fafc;
134162
}
@@ -355,32 +383,40 @@ body.dark-theme .app-header {
355383
font-weight: 700;
356384
}
357385

358-
386+
/* ===============================
387+
MODERN GLASS RECENT TABLE
388+
================================= */
389+
/* PAGE CONTAINER */
359390
.recent-page-container {
360-
max-width: 1100px;
361-
margin: 30px auto;
362-
padding: 28px;
363-
background: var(--card);
364-
backdrop-filter: blur(20px);
365-
border: 1px solid var(--glass-border);
366-
border-radius: 20px;
367-
box-shadow: var(--card-shadow);
368-
color: var(--text-color);
369-
transition: background 0.3s ease, border 0.3s ease;
391+
width: 100%;
392+
max-width: 1200px;
393+
/* controls table width */
394+
margin: 0 auto;
395+
/* centers */
396+
padding: 0 24px;
397+
/* space left & right */
398+
box-sizing: border-box;
370399
}
371400

401+
/* Wrapper */
372402
.recent-table-wrapper {
373-
margin-top: 20px;
374403
width: 100%;
404+
/*margin-top: 20px;
405+
margin-bottom: 20px;*/
375406
overflow-x: auto;
376407
}
377408

409+
/* ===============================
410+
TABLE BASE
411+
================================= */
412+
378413
.recent-table {
379414
width: 100%;
380415
border-collapse: collapse;
381416
border-radius: 12px;
382417
overflow: hidden;
383418
table-layout: fixed;
419+
min-width: 800px;
384420
}
385421

386422
/* Header */
@@ -389,32 +425,84 @@ body.dark-theme .app-header {
389425
}
390426

391427
.recent-table th {
392-
padding: 8px 14px;
428+
padding: 10px 14px;
393429
text-align: left;
394430
font-size: 13px;
395431
letter-spacing: 0.08em;
396432
text-transform: uppercase;
397433
font-weight: 700;
398434
color: var(--muted);
399435
border-bottom: 1px solid var(--glass-border);
436+
white-space: nowrap;
400437
}
401438

402439
/* Body cells */
403440
.recent-table td {
404-
vertical-align: middle;
405441
padding: 14px;
406442
font-size: 14px;
407443
color: var(--text-primary);
408444
border-bottom: 1px solid var(--glass-border);
445+
vertical-align: middle;
409446
transition: 0.25s ease;
447+
white-space: nowrap;
410448
}
411449

412450
/* Row hover */
413451
.recent-table tbody tr:hover {
414452
background: rgba(255, 255, 255, 0.05);
415453
}
416454

417-
/* Short link */
455+
/* ===============================
456+
COLUMN WIDTH CONTROL
457+
================================= */
458+
459+
/* # column */
460+
.recent-table th:nth-child(1),
461+
.recent-table td:nth-child(1) {
462+
width: 45px;
463+
text-align: center;
464+
padding-left: 6px;
465+
padding-right: 6px;
466+
}
467+
468+
/* Short URL */
469+
.recent-table th:nth-child(2),
470+
.recent-table td:nth-child(2) {
471+
width: 170px;
472+
}
473+
474+
/* Original URL (main space owner) */
475+
.recent-table th:nth-child(3),
476+
.recent-table td:nth-child(3) {
477+
width: 45%;
478+
min-width: 0;
479+
}
480+
481+
/* Created */
482+
.recent-table th:nth-child(4),
483+
.recent-table td:nth-child(4) {
484+
width: 170px;
485+
}
486+
487+
/* Visits */
488+
.recent-table th:nth-child(5),
489+
.recent-table td:nth-child(5) {
490+
width: 80px;
491+
text-align: center;
492+
font-weight: 700;
493+
color: var(--accent-2);
494+
}
495+
496+
/* Actions */
497+
.recent-table th:nth-child(6),
498+
.recent-table td:nth-child(6) {
499+
width: 120px;
500+
}
501+
502+
/* ===============================
503+
LINKS
504+
================================= */
505+
418506
.short-code a {
419507
color: var(--accent);
420508
font-weight: 700;
@@ -426,22 +514,17 @@ body.dark-theme .app-header {
426514
text-decoration: underline;
427515
}
428516

429-
/*.original-url,
430-
.original-url a {
431-
white-space: normal;
432-
word-break: break-word;
433-
overflow-wrap: break-word;
434-
}*/
435-
517+
/* Original URL truncate */
436518
.original-url {
437-
max-width: 100%;
519+
word-break: break-all;
438520
}
439521

440522
.original-url a {
441523
display: block;
442524
overflow: hidden;
443525
text-overflow: ellipsis;
444526
white-space: nowrap;
527+
color: var(--text-secondary);
445528
}
446529

447530
.original-url a:hover {
@@ -455,23 +538,14 @@ body.dark-theme .app-header {
455538
white-space: nowrap;
456539
}
457540

458-
/* Visit count highlight */
459-
.recent-table td:nth-child(5) {
460-
font-weight: 700;
461-
color: var(--accent-2);
462-
}
541+
/* ===============================
542+
ACTION BUTTONS
543+
================================= */
463544

464-
/* Dark mode adjustments */
465-
.dark-theme .recent-table th,
466-
.dark-theme .recent-table td {
467-
color: #e5e7eb;
468-
border-bottom: 1px solid var(--glass-border);
469-
}
470-
471-
/* Action buttons */
472545
.action-col {
473546
display: flex;
474547
gap: 10px;
548+
justify-content: flex-start;
475549
}
476550

477551
.action-btn {
@@ -496,11 +570,36 @@ body.dark-theme .app-header {
496570
color: #fff;
497571
}
498572

499-
.recent-table-wrapper {
500-
margin-bottom: 20px;
573+
/* ===============================
574+
DARK MODE
575+
================================= */
576+
577+
.dark-theme .recent-table th,
578+
.dark-theme .recent-table td {
579+
color: #e5e7eb;
580+
border-bottom: 1px solid var(--glass-border);
501581
}
502582

583+
/* Tablet */
584+
@media (max-width: 1024px) {
585+
.recent-page-container {
586+
padding: 0 18px;
587+
}
588+
}
503589

590+
/* Mobile */
591+
@media (max-width: 768px) {
592+
.recent-page-container {
593+
padding: 0 12px;
594+
}
595+
}
596+
597+
/* Small phones */
598+
@media (max-width: 480px) {
599+
.recent-page-container {
600+
padding: 0 8px;
601+
}
602+
}
504603

505604
/* Footer */
506605
footer.big-footer {

0 commit comments

Comments
 (0)