diff --git a/.env.sample b/.env.sample index bc9711c..24e1e91 100644 --- a/.env.sample +++ b/.env.sample @@ -2,5 +2,5 @@ MODE=local MONGO_URI=mongodb://:@127.0.0.1:27017/?authSource=admin&retryWrites=true&w=majority DOMAIN=https://localhost:8001 PORT=8001 -API_VERSION="" -APP_NAMe="LOCAL" \ No newline at end of file +API_VERSION="/api/v1" +APP_NAME="LOCAL" \ No newline at end of file diff --git a/.gitignore b/.gitignore index ae1cf18..8a7644e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ poetry.lock *.tmp *.temp *.bak + + +assets/images/qr/* \ No newline at end of file diff --git a/app/main.py b/app/main.py index c960eeb..81e3ece 100644 --- a/app/main.py +++ b/app/main.py @@ -2,14 +2,18 @@ from contextlib import asynccontextmanager from pathlib import Path import logging -import traceback import asyncio from fastapi import FastAPI, Request -from fastapi.responses import JSONResponse + +# from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware +# from fastapi.exceptions import RequestValidationError +# from starlette.exceptions import HTTPException as StarletteHTTPException +from fastapi.templating import Jinja2Templates + from app.routes import ui_router from app.utils import db from app.utils.cache import cleanup_expired @@ -90,22 +94,36 @@ async def lifespan(app: FastAPI): app = FastAPI(title="TinyURL", lifespan=lifespan) app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET) +templates = Jinja2Templates(directory="app/templates") BASE_DIR = Path(__file__).resolve().parent STATIC_DIR = BASE_DIR / "static" app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") +# QR codes are now served from /qr, which maps to assets/images/qr in the project root +PROJECT_ROOT = BASE_DIR.parent +QR_DIR = PROJECT_ROOT / "assets" / "images" / "qr" +app.mount("/qr", StaticFiles(directory=QR_DIR), name="qr") # ----------------------------- # Global error handler # ----------------------------- -@app.exception_handler(Exception) -async def global_exception_handler(request: Request, exc: Exception): - traceback.print_exc() - return JSONResponse( - status_code=500, - content={"success": False, "error": "INTERNAL_SERVER_ERROR"}, +# app.exception_handler(Exception) +# sync def global_exception_handler(request: Request, exc: Exception): +# traceback.print_exc() +# return JSONResponse( +# status_code=500, +# content={"success": False, "error": "INTERNAL_SERVER_ERROR"}, +# ) + + +@app.exception_handler(404) +async def custom_404_handler(request: Request, exc): + return templates.TemplateResponse( + "404.html", + {"request": request}, + status_code=404, ) diff --git a/app/routes.py b/app/routes.py index 004484f..0386f6b 100644 --- a/app/routes.py +++ b/app/routes.py @@ -22,6 +22,7 @@ from fastapi.templating import Jinja2Templates from pydantic import BaseModel, Field + from app import __version__ from app.utils import db from app.utils.cache import ( @@ -67,10 +68,11 @@ async def index(request: Request): if qr_enabled and new_short_url and short_code: qr_data = new_short_url qr_filename = f"{short_code}.png" - qr_dir = BASE_DIR / "static" / "qr" + PROJECT_ROOT = BASE_DIR.parent # go from app/ β†’ project root + qr_dir = PROJECT_ROOT / "assets" / "images" / "qr" qr_dir.mkdir(parents=True, exist_ok=True) generate_qr_with_logo(qr_data, str(qr_dir / qr_filename)) - qr_image = f"/static/qr/{qr_filename}" + qr_image = f"/qr/{qr_filename}" recent_urls = db.get_recent_urls(MAX_RECENT_URLS) or get_recent_from_cache( MAX_RECENT_URLS @@ -138,6 +140,7 @@ async def create_short_url( @ui_router.get("/recent", response_class=HTMLResponse) +@ui_router.get("/history", response_class=HTMLResponse) async def recent_urls(request: Request): recent_urls_list = db.get_recent_urls(MAX_RECENT_URLS) or get_recent_from_cache( MAX_RECENT_URLS @@ -222,7 +225,8 @@ def redirect_short_ui(short_code: str, background_tasks: BackgroundTasks): set_cache_pair(short_code, original_url) return RedirectResponse(original_url) - return PlainTextResponse("Invalid short URL", status_code=404) + # return PlainTextResponse("Invalid short URL", status_code=404) + raise HTTPException(status_code=404, detail="Page not found") @ui_router.delete("/recent/{short_code}") diff --git a/app/static/css/tiny.css b/app/static/css/tiny.css index 873f075..fa81205 100644 --- a/app/static/css/tiny.css +++ b/app/static/css/tiny.css @@ -16,80 +16,163 @@ body { background-image: radial-gradient(circle at 50% -20%, #1e1e2e 0%, transparent 50%); } -body { - background: var(--bg); - color: var(--text-primary); - font-family: "Inter", system-ui, sans-serif; - margin: 0; - overflow-x: hidden; - background-image: radial-gradient(circle at 50% -20%, #1e1e2e 0%, transparent 50%); -} - /* Light theme overrides */ body.light-theme { + /* background + glass */ --bg: #f9fafb; --glass: rgba(0, 0, 0, 0.03); --glass-border: rgba(0, 0, 0, 0.07); - --accent: #2563eb; + + /* main card + text */ + --card: #ffffff; --text-primary: #111827; --text-secondary: #4b5563; + --text-color: #111827; + + /* accent */ + --accent: #2563eb; - /* Remove or soften the dark gradient */ + /* Remove the dark radial gradient */ background-image: none; - /* clean white background */ - /* Or use a subtle light gradient if you prefer */ - /* background-image: radial-gradient(circle at 50% -20%, #e5e7eb 0%, transparent 50%); */ } /* Layout */ .main-layout { max-width: 900px; margin: 0 auto; - padding: 4rem 1rem; + padding: 6rem 1rem 4rem; display: flex; flex-direction: column; gap: 2rem; } -.site-header { +.page { + padding-top: 6rem; +} + +.app-header { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 55px; display: flex; - justify-content: space-between; align-items: center; - padding: 1rem 1.5rem; + justify-content: space-between; + padding: 0 10px; + box-sizing: border-box; + background: var(--glass); border-bottom: 1px solid var(--glass-border); - backdrop-filter: blur(10px); + z-index: 1000; +} + +body.light-theme .app-header { + background: #ffffff; + /* solid background */ + border-bottom: 1px solid #e5e7eb; + /* clear separation */ + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); } .header-left, .header-right { display: flex; - gap: 1rem; align-items: center; + gap: 12px; } -.header-center { - flex: 1; - text-align: center; +.app-logo { + width: 36px; + height: 36px; + background: linear-gradient(135deg, #2563eb, #5ab9ff); + color: #ffffff; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; } -.logo { - margin: 0; +.app-name { font-size: 1.5rem; font-weight: 700; color: var(--text-primary); } -.icon-btn { - background: none; - border: none; +.header-nav { + display: flex; + gap: 26px; + margin: 0 auto; +} + +.nav-link, +.nav-link:link, +.nav-link:visited { + text-decoration: none; color: var(--text-primary); - font-size: 1.2rem; + font-weight: 500; + position: relative; +} + +body.dark-theme .app-header { + background: linear-gradient(180deg, #0b1220, #050b14); +} + +.dark-theme .nav-link { + color: #e5e7eb; +} + +.nav-link:hover { + color: #2563eb; +} + +.nav-link.active::after { + content: ""; + position: absolute; + bottom: -6px; + left: 0; + width: 100%; + height: 2px; + background: #111827; +} + +.nav-link { + position: relative; +} + +.nav-link::after { + content: ""; + position: absolute; + left: 0; + bottom: -4px; + width: 0; + height: 2px; + background: var(--text-primary); + transition: width 0.3s; +} + +.nav-link.active::after { + width: 100%; +} + + +.dark-theme .nav-link.active::after { + background: #f8fafc; +} + +.theme-toggle { + background: transparent; + border: none; cursor: pointer; - transition: color 0.3s; + padding: 8px; + border-radius: 8px; + font-weight: 700; + background: var(--glass); + color: var(--text-primary); } -.icon-btn:hover { +.theme-toggle:hover { color: var(--accent); } @@ -281,17 +364,246 @@ body.light-theme { font-weight: bold; } -.original-url { +/*.original-url { color: var(--text-secondary); font-size: 0.8rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +}*/ + + +.hero { + text-align: center; + margin: 40px 0; +} + +.hero h1 { + font-size: 42px; + font-weight: 700; +} + +/* =============================== + MODERN GLASS RECENT TABLE +================================= */ +/* PAGE CONTAINER */ +.recent-page-container { + width: 100%; + max-width: 1200px; + /* controls table width */ + margin: 0 auto; + /* centers */ + padding: 0 24px; + /* space left & right */ + box-sizing: border-box; +} + +/* Wrapper */ +.recent-table-wrapper { + width: 100%; + /*margin-top: 20px; + margin-bottom: 20px;*/ + overflow-x: auto; +} + +/* =============================== + TABLE BASE +================================= */ + +.recent-table { + width: 100%; + border-collapse: collapse; + border-radius: 12px; + overflow: hidden; + table-layout: fixed; + min-width: 800px; +} + +/* Header */ +.recent-table thead { + background: var(--glass); +} + +.recent-table th { + padding: 10px 14px; + text-align: left; + font-size: 13px; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 700; + color: var(--muted); + border-bottom: 1px solid var(--glass-border); + white-space: nowrap; +} + +/* Body cells */ +.recent-table td { + padding: 14px; + font-size: 14px; + color: var(--text-primary); + border-bottom: 1px solid var(--glass-border); + vertical-align: middle; + transition: 0.25s ease; + white-space: nowrap; +} + +/* Row hover */ +.recent-table tbody tr:hover { + background: rgba(255, 255, 255, 0.05); +} + +/* =============================== + COLUMN WIDTH CONTROL +================================= */ + +/* # column */ +.recent-table th:nth-child(1), +.recent-table td:nth-child(1) { + width: 45px; + text-align: center; + padding-left: 6px; + padding-right: 6px; +} + +/* Short URL */ +.recent-table th:nth-child(2), +.recent-table td:nth-child(2) { + width: 170px; +} + +/* Original URL (main space owner) */ +.recent-table th:nth-child(3), +.recent-table td:nth-child(3) { + width: 45%; + min-width: 0; +} + +/* Created */ +.recent-table th:nth-child(4), +.recent-table td:nth-child(4) { + width: 170px; +} + +/* Visits */ +.recent-table th:nth-child(5), +.recent-table td:nth-child(5) { + width: 80px; + text-align: center; + font-weight: 700; + color: var(--accent-2); +} + +/* Actions */ +.recent-table th:nth-child(6), +.recent-table td:nth-child(6) { + width: 120px; +} + +/* =============================== + LINKS +================================= */ + +.short-code a { + color: var(--accent); + font-weight: 700; + text-decoration: none; +} + +.short-code a:hover { + color: var(--accent-2); + text-decoration: underline; +} + +/* Original URL truncate */ +.original-url { + word-break: break-all; +} + +.original-url a { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-secondary); +} + +.original-url a:hover { + color: var(--accent); +} + +/* Created time */ +.created-time { + font-size: 13px; + color: var(--muted); + white-space: nowrap; +} + +/* =============================== + ACTION BUTTONS +================================= */ + +.action-col { + display: flex; + gap: 10px; + justify-content: flex-start; +} + +.action-btn { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + font-size: 16px; + transition: 0.2s ease; +} + +.open-btn { + background: #3b82f6; + color: #fff; +} + +.delete-btn { + background: #ef4444; + color: #fff; +} + +/* =============================== + DARK MODE +================================= */ + +.dark-theme .recent-table th, +.dark-theme .recent-table td { + color: #e5e7eb; + border-bottom: 1px solid var(--glass-border); +} + +/* Tablet */ +@media (max-width: 1024px) { + .recent-page-container { + padding: 0 18px; + } +} + +/* Mobile */ +@media (max-width: 768px) { + .recent-page-container { + padding: 0 12px; + } +} + +/* Small phones */ +@media (max-width: 480px) { + .recent-page-container { + padding: 0 8px; + } } /* Footer */ -.big-footer { - background: rgba(255, 255, 255, 0.01); +footer.big-footer { + background: var(--bg); border-top: 1px solid var(--glass-border); padding: 4rem 1rem 2rem; margin-top: 4rem; @@ -372,6 +684,27 @@ body.light-theme { color: var(--accent); } +/* Dark mode footer adjustments */ +body.dark-theme footer.big-footer { + background: #020617 !important; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +body.dark-theme .footer-col h4 { + color: #f3f4f6; +} + +body.dark-theme .footer-col p, +body.dark-theme .footer-col ul li a, +body.dark-theme .footer-bottom { + color: #cbd5e1; +} + +body.dark-theme .footer-col ul li a:hover, +body.dark-theme .footer-bottom a { + color: #a5f3fc; +} + /* Responsive adjustments */ @media (max-width: 900px) { .footer-grid { @@ -413,3 +746,47 @@ body.light-theme { text-align: center; } } + + +/* =============================== + HOME PAGE FIX β€” LONG URL WRAP + (does NOT affect table) +================================= */ +/*.recent-tray .recent-item .original-url, +.recent-tray .recent-item .original-url a { + white-space: normal !important; + word-break: break-word !important; + overflow-wrap: anywhere !important; + line-break: anywhere !important; + display: block; +} + +.recent-tray .recent-item { + max-width: 20px; +}*/ + + +/* allow wrapping */ +.recent-tray .recent-item .original-url, +.recent-tray .recent-item .original-url a { + display: -webkit-box; + -webkit-box-orient: vertical; + + -webkit-line-clamp: 3; + /* ⭐ change 2 or 3 lines here */ + line-clamp: 3; + + overflow: hidden; + text-overflow: ellipsis; + + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; +} + +/* IMPORTANT β€” remove width restriction */ +.recent-tray .recent-item { + min-width: 0; + /* allows shrinking inside flex/grid */ + max-width: 100%; +} \ No newline at end of file diff --git a/app/static/style.css b/app/static/style.css index b754687..8dd7671 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -1,815 +1,1063 @@ -html, -body { - height: 100%; - margin: 0; - font-family: Arial; - padding: 0; - font-family: "Poppins", system-ui, Arial, sans-serif; - background: var(--bg); - background-size: cover; - background-position: center; - background-size: cover; - background-position: center; -} -input { - width: 70%; - margin-top: 2px; - margin-bottom: 2px; - font-size: 16px; -} -.admin-box { - margin: 120px auto 60px; - /* space from header + footer */ -} -.app-layout { - min-height: 100vh; - display: flex; - flex-direction: column; - margin-top: var(--header-height); -} -button { - padding: 8px; - margin: 5px; -} -.error-box { - margin-bottom: 15px; - padding: 10px; - color: #ff4d4d; - border-radius: 8px; - font-weight: 600; -} - -.dark-theme h1 { - background: linear-gradient(90deg, #ffffff, #dddddd, #ffffff); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - text-shadow: 0px 0px 10px rgba(255, 255, 255, 0.4); -} - -.dark-theme p { - background: linear-gradient(90deg, #ffffff, #dddddd, #ffffff); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - text-shadow: 0px 0px 10px rgba(255, 255, 255, 0.4); -} -.dark-theme { - --bg-overlay: rgba(0, 0, 0, 0.75); - --glass-bg: rgba(0, 0, 0, 0.4); - --text-color: #fff; - --input-bg: rgba(50, 50, 50, 0.8); - --input-text-color: #fff; -} - -@keyframes pop { - 0% { - transform: scale(0.7); - opacity: 0; - } - 100% { - transform: scale(1); - opacity: 1; - } -} -/* INPUT CONTAINER */ -.input-field { - flex: 1 1 700px; - display: flex; - align-items: center; - gap: 12px; - border-radius: 12px; - border: 2px solid rgb(6, 0, 0); - background: transparent; /* IMPORTANT */ - padding: 12px 12px; -} -.dark-theme .input-field { - border-color: #ffffff; -} -/* INPUT ITSELF */ -.input-field input[type="text"] { - width: 100%; - border: none; - outline: none; - background-color: transparent !important; - background-image: none !important; - box-shadow: none !important; - font-size: 23px; -} - -.input-field input { - color: #000 !important; -} - -.dark-theme .input-field input { - color: #fff !important; -} - -.input-field input:-webkit-autofill, -.input-field input:-webkit-autofill:hover, -.input-field input:-webkit-autofill:focus, -.input-field input:-webkit-autofill:active { - -webkit-box-shadow: 0 0 0 1000px transparent inset !important; - box-shadow: 0 0 0 1000px transparent inset !important; - background-color: transparent !important; - background-image: none !important; - transition: background-color 9999s ease-in-out 0s; -} - -.input-field input:-webkit-autofill { - -webkit-text-fill-color: #000 !important; -} - -.dark-theme .input-field input:-webkit-autofill { - -webkit-text-fill-color: #fff !important; -} -.input-field input::selection, -.input-field input::-moz-selection { - background: transparent; - color: inherit; -} -.short-code { - color: #0a0000; /* blue like links */ - font-weight: 700; -} - -.app-header { - position: fixed; - top: 0; - left: 0; - width: 97%; - height: 55px; - background: white; - display: flex; - align-items: center; - padding: 0 28px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); - z-index: 1000; -} - -/* Dark mode */ -.dark-theme .app-header { - background: linear-gradient(180deg, #0b1220, #050b14); -} - -footer { - margin-top: 0; -} - -body.dark-theme, -body.dark-theme .page, -body.dark-theme main, -body.dark-theme section { - background: #0f1720 !important; -} - -.header-left { - display: flex; - align-items: center; - gap: 12px; -} - -.app-logo { - width: 36px; - height: 36px; - background: linear-gradient(135deg, #2563eb, #5ab9ff); - color: white; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; -} - -.app-name { - font-size: 20px; - font-weight: 700; - color: #111827; -} - -.dark-theme .app-name { - color: #f8fafc; -} - -.header-nav { - position: absolute; - left: 50%; - transform: translateX(-50%); - display: flex; - gap: 26px; -} - -.nav-link { - text-decoration: none; - color: #111827; - font-weight: 500; - position: relative; -} - -.dark-theme .nav-link { - color: #e5e7eb; -} - -.nav-link:hover { - color: #2563eb; -} - -.nav-link.active::after { - content: ""; - position: absolute; - bottom: -6px; - left: 0; - width: 100%; - height: 2px; - background: #111827; -} - -.dark-theme .nav-link.active::after { - background: #f8fafc; -} - -.header-right { - margin-left: auto; - display: flex; - align-items: center; -} - -:root { - --header-height: 55px; - --bg: #eefaf8; - --card: rgba(255, 255, 255, 0.95); - --muted: #7b8b8a; - --accent-1: #5ab9ff; - --accent-2: #4cb39f; - --accent-grad: linear-gradient(90deg, #4cb39f, #5ab9ff); - --success: #2fb06e; - --glass: rgba(255, 255, 255, 0.85); -} - -.dark-theme { - --bg-overlay: rgba(0, 0, 0, 0.75); - --glass-bg: rgba(0, 0, 0, 0.4); - --text-color: #f3f3f3; - --input-bg: rgba(11, 10, 10, 0.8); - --button-bg: linear-gradient(90deg, #4444ff, #2266ff); - --recent-bg: rgba(255, 255, 255, 0.1); -} - -/* Preserve your dark theme variables too */ -body.dark-theme { - --bg: #0f1720; - --card: rgba(10, 14, 18, 0.92); - --muted: #9aa7a6; -} - -.page { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; - padding: 2rem; - min-height: 80vh; -} - -.theme-toggle { - background: transparent; - border: none; - cursor: pointer; - padding: 8px; - border-radius: 8px; - font-weight: 700; - background: var(--card); -} - -/* Hero */ -.hero { - width: 100%; - max-width: 1100px; - background: transparent; - text-align: center; - padding: 10px; -} - -.hero h1 { - margin: 10px 0 14px; - font-size: 36px; - line-height: 1.05; - color: #000606; -} - -.hero p { - margin: var(--bg-overlay); - color: var(--muted); - max-width: 820px; - margin-left: auto; - margin-right: auto; - color: #000606; -} - -/* Main card & input */ -.card { - width: 100%; - max-width: 1100px; - background: var(--card); - border-radius: 14px; - padding: 15px; - box-shadow: 0 18px 50px rgba(8, 24, 24, 0.06); -} - -.cta { - min-width: 220px; - padding: 14px 22px; - border-radius: 12px; - border: none; - color: rgb(12, 1, 1); - font-weight: 700; - cursor: pointer; - background: var(--accent-grad); - box-shadow: 0 12px 28px rgba(77, 163, 185, 0.12); -} - -.small-action { - display: flex; - align-items: center; - gap: 8px; - color: var(--muted); - margin-top: 10px; -} - -.result { - margin-top: 26px; - background: white; - border-radius: 12px; - padding: 20px; - border: 1px solid rgba(22, 60, 55, 0.03); - box-shadow: 0 8px 28px rgba(7, 20, 20, 0.03); -} - -.result-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 12px; -} - -.result-header .dot { - width: 30px; - height: 30px; - background: var(--success); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-weight: 700; -} - -.short-actions { - display: flex; - justify-content: center; - align-items: center; - gap: 8px; - padding: 10px 14px; - border-radius: 12px; - background: linear-gradient(180deg, rgba(75, 194, 176, 0.06), rgba(94, 207, 255, 0.04)); -} - -.short-box input { - align-items: center; - padding: 10px; - font-size: 15px; -} - -.btn-copy { - border: none; - padding: 10px 14px; - border-radius: 8px; - color: white; - font-weight: 700; - cursor: pointer; -} - -.btn-share { - background: #f2f5f5; - border: none; - padding: 10px 14px; - border-radius: 8px; - color: #0b2b2a; - font-weight: 700; - cursor: pointer; - margin-left: 6px; -} - -.meta-row { - align-items: center; - justify-content: center; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2px; - padding: 16px; - margin-top: 1px; - align-items: top; - color: black; -} -.result-body { - margin-top: 30px; - - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -} - -.qr-block { - text-align: center; - padding-top: 8px; -} - -.qr-block img { - height: 15rem; - align-items: center; - aspect-ratio: 1; - box-shadow: 0 10px 20px rgba(10, 20, 30, 0.06); - outline: 2px solid green; - outline-offset: 4px; -} - -.download-qr { - display: inline-block; - margin-top: 12px; - text-decoration: none; - color: var(--accent-1); - font-weight: 700; -} - -.action-row { - display: flex; - justify-content: right; - align-items: right; -} - -.action-secondary { - background: #f6fbfb; - border: 1px solid rgba(0, 0, 0, 0.03); - border-radius: 10px; - cursor: pointer; - font-weight: 700; -} - -/* Force Generate QR to stay on one line */ -.qr-inline { - display: inline-flex; - align-items: center; - gap: 8px; - white-space: nowrap; -} - -.qr-inline input { - margin: 0; -} - -/* Responsive */ -@media (max-width: 880px) { - .input-row { - flex-direction: column; - } - - .cta { - width: 100%; - } - - .meta-row { - grid-template-columns: 1fr; - } -} -.result-title { - font-weight: 700; - color: #0e34f6; -} - -.dark-theme .result-title { - color: #150cff; -} - -footer { - min-height: auto; -} - -.app-footer { - background: white; - color: #e5e7eb; - padding: 8px 10px; - margin-top: auto; - position: relative; -} -.dark-theme .app-footer { - background: linear-gradient(180deg, #0b1220, #050b14); -} - -.footer-container { - margin: auto; - display: flex; - gap: 60px; - justify-content: space-between; - flex-wrap: wrap; -} - -.footer-brand { - max-width: 420px; -} - -.footer-logo { - width: 42px; - height: 42px; - background: linear-gradient(135deg, #2563eb, #5ab9ff); - border-radius: 14px; - display: flex; - align-items: center; - justify-content: center; - font-size: 22px; -} -.dark-theme .footer-brand h3, -.dark-theme .footer-brand p, -.dark-theme .footer-col h4, -.dark-theme .app-footer a, -.dark-theme .footer-bottom { - color: #f8fafc; -} - -.footer-brand h3 { - margin: 0; - color: #000000; - font-size: 22px; - font-weight: 700; -} - -.footer-brand p { - margin-top: 8px; - color: #000000; - line-height: 1.6; - font-size: 14px; -} - -/* MAIN CONTENT */ - -/* FOOTER */ -.app-footer { - margin-top: auto; -} - -/* GitHub button */ -.github-btn { - display: inline-flex; - align-items: center; - gap: 2px; - margin-top: 1px; - padding: 10px 16px; - border-radius: 8px; - background: rgba(255, 255, 255, 0.06); - color: #000000; - text-decoration: none; - font-weight: 600; - transition: all 0.25s ease; -} - -.github-btn:hover { - background: black(11, 1, 1); - transform: translateY(-2px); -} - -.footer-links { - display: flex; - gap: 80px; - flex-wrap: wrap; -} - -.footer-col h4 { - margin-bottom: 14px; - font-size: 16px; - color: #000000; - font-weight: 700; -} - -.footer-col a { - display: block; - text-decoration: none; - color: #000000; - margin-bottom: 10px; - font-size: 14px; - transition: color 0.2s ease; -} - -.footer-col a:hover { - text-decoration: underline; -} - -/* Bottom */ -.footer-bottom { - margin-top: 10px; - border-top: 1px solid rgba(255, 255, 255, 0.153); - padding-top: 8px; - padding-bottom: 1px; - text-align: center; - font-size: 14px; - color: #080808; -} -.footer-bottom a { - color: #030000; - font-weight: 600; - text-decoration: none; -} - -.footer-bottom a:hover { - text-decoration: underline; -} - -/* Responsive */ -@media (max-width: 768px) { - .footer-container { - flex-direction: column; - gap: 40px; - } - - .footer-links { - gap: 40px; - } -} -/* REMOVE white line above footer in dark mode */ -footer { - margin-top: 0 !important; -} -.recent-table-wrapper { - margin-top: 20px; - width: 100%; - overflow-x: auto; -} - -.recent-table { - width: 100%; - border-collapse: collapse; - border-radius: 12px; - overflow: hidden; -} - -.recent-table thead { - background: rgb(0, 0, 0); -} -.recent-table th { - color: rgb(0, 0, 0); - padding: 8px 14px; - text-align: left; - font-size: 16px; -} -.short-code a { - color: #2563eb; - font-weight: 600; - text-decoration: none; -} - -.short-code a:hover { - color: #1d4ed8; - text-decoration: underline; -} -.recent-table td { - color: rgb(34, 48, 77); - padding: 10px 14px; - text-align: left; - font-size: 14px; -} - -.created-time { - font-size: 14px; - color: #374151; - white-space: nowrap; -} - -.time-ago { - color: #374151; - font-size: 13px; - margin-left: 2px; -} -.recent-table th { - font-weight: 700; -} - -.recent-table tbody tr, -th { - background: rgb(255, 255, 255); - border-bottom: 1px solid rgb(0, 0, 0); -} -.dark-theme.recent-table tbody tr, -td { - background: rgba(255, 255, 255, 0.04); - border-bottom: 1px solid rgb(0, 0, 0); -} -.recent-table tbody tr:hover { - background: rgb(196, 196, 196); -} - -/* Short code */ -.short-code { - font-weight: 700; -} - -.original-url { - color: #22c55e; - word-break: break-all; -} - -/* Action buttons */ -.action-col { - display: flex; - gap: 10px; -} - -.action-btn { - width: 36px; - height: 36px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; - font-size: 16px; - transition: 0.2s ease; -} - -.open-btn { - background: #3b82f6; - color: #fff; -} - -.delete-btn { - background: #ef4444; - color: #fff; -} - -.recent-table-wrapper { - margin-bottom: 20px; -} -/* ========================= - Coming Soon Page -========================= */ - -.coming-soon-page { - display: flex; - align-items: center; - justify-content: center; - padding: 120px 20px 60px; -} - -.coming-soon-card { - max-width: 520px; - width: 100%; - background: var(--card); - border-radius: 16px; - padding: 50px 40px; - text-align: center; - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.08); -} - -.coming-icon { - font-size: 48px; - margin-bottom: 18px; -} - -.coming-soon-card h1 { - font-size: 34px; - margin-bottom: 14px; - color: #000; -} - -.dark-theme .coming-soon-card h1 { - color: #fff; -} - -.coming-soon-card p { - font-size: 15px; - color: var(--muted); - line-height: 1.6; - margin-bottom: 28px; -} - -.coming-btn { - display: inline-block; - padding: 12px 22px; - border-radius: 10px; - background: var(--accent-grad); - color: #fff; - font-weight: 700; - text-decoration: none; - transition: 0.25s ease; -} - -.coming-btn:hover { - transform: scale(1.05); - box-shadow: 0 12px 28px rgba(77, 163, 185, 0.25); -} -.info-box { - margin-bottom: 15px; - padding: 10px; - color: #0e34f6; - border-radius: 8px; - font-weight: 700; -} +html, +body { + height: 100%; + margin: 0; + font-family: Arial; + padding: 0; + font-family: "Poppins", system-ui, Arial, sans-serif; + background: var(--bg); + background-size: cover; + background-position: center; + background-size: cover; + background-position: center; +} + +input { + width: 70%; + margin-top: 2px; + margin-bottom: 2px; + font-size: 16px; +} + +.admin-box { + margin: 120px auto 60px; + /* space from header + footer */ +} + +.app-layout { + min-height: 100vh; + display: flex; + flex-direction: column; + margin-top: var(--header-height); +} + +button { + padding: 8px; + margin: 5px; +} + +.error-box { + margin-bottom: 15px; + padding: 10px; + color: #ff4d4d; + border-radius: 8px; + font-weight: 600; +} + +.dark-theme h1 { + background: linear-gradient(90deg, #ffffff, #dddddd, #ffffff); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0px 0px 10px rgba(255, 255, 255, 0.4); +} + +.dark-theme p { + background: linear-gradient(90deg, #ffffff, #dddddd, #ffffff); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0px 0px 10px rgba(255, 255, 255, 0.4); +} + +.dark-theme { + --bg-overlay: rgba(0, 0, 0, 0.75); + --glass-bg: rgba(0, 0, 0, 0.4); + --text-color: #fff; + --input-bg: rgba(50, 50, 50, 0.8); + --input-text-color: #fff; +} + +@keyframes pop { + 0% { + transform: scale(0.7); + opacity: 0; + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + +/* INPUT CONTAINER */ +.input-field { + flex: 1 1 700px; + display: flex; + align-items: center; + gap: 12px; + border-radius: 12px; + border: 2px solid rgb(6, 0, 0); + background: transparent; + /* IMPORTANT */ + padding: 12px 12px; +} + +.dark-theme .input-field { + border-color: #ffffff; +} + +/* INPUT ITSELF */ +.input-field input[type="text"] { + width: 100%; + border: none; + outline: none; + background-color: transparent !important; + background-image: none !important; + box-shadow: none !important; + font-size: 23px; +} + +.input-field input { + color: #000 !important; +} + +.dark-theme .input-field input { + color: #fff !important; +} + +.input-field input:-webkit-autofill, +.input-field input:-webkit-autofill:hover, +.input-field input:-webkit-autofill:focus, +.input-field input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 1000px transparent inset !important; + box-shadow: 0 0 0 1000px transparent inset !important; + background-color: transparent !important; + background-image: none !important; + transition: background-color 9999s ease-in-out 0s; +} + +.input-field input:-webkit-autofill { + -webkit-text-fill-color: #000 !important; +} + +.dark-theme .input-field input:-webkit-autofill { + -webkit-text-fill-color: #fff !important; +} + +.input-field input::selection, +.input-field input::-moz-selection { + background: transparent; + color: inherit; +} + +.short-code { + color: #0a0000; + /* blue like links */ + font-weight: 700; +} + +footer { + margin-top: 0; +} + +body.dark-theme, +body.dark-theme .page, +body.dark-theme main, +body.dark-theme section { + background: #0f1720 !important; +} + +/*:root { + --header-height: 55px; + --bg: #eefaf8; + --card: rgba(255, 255, 255, 0.95); + --muted: #7b8b8a; + --accent-1: #5ab9ff; + --accent-2: #4cb39f; + --accent-grad: linear-gradient(90deg, #4cb39f, #5ab9ff); + --success: #2fb06e; + --glass: rgba(255, 255, 255, 0.85); +}*/ + +:root { + /* Background */ + --bg: #eefaf8; + --card: rgba(255, 255, 255, 0.85); + + /* Text */ + --text-color: #1f2937; + --text-muted: #6b7280; + + /* Borders */ + --glass-border: rgba(0, 0, 0, 0.08); + + /* Accent */ + --accent-1: #5ab9ff; + --accent-2: #4cb39f; + + /* Shadow */ + --card-shadow: 0 20px 60px rgba(0, 0, 0, 0.12); + --text-primary: var(--text-color); + --text-secondary: var(--text-muted); + --accent: var(--accent-1); +} + +.dark-theme { + --bg-overlay: rgba(0, 0, 0, 0.75); + --glass-bg: rgba(0, 0, 0, 0.4); + --text-color: #f3f3f3; + --input-bg: rgba(11, 10, 10, 0.8); + --button-bg: linear-gradient(90deg, #4444ff, #2266ff); + --recent-bg: rgba(255, 255, 255, 0.1); +} + +/* Preserve your dark theme variables too */ +body.dark-theme { + --bg: #0f1720; + --card: rgba(20, 25, 30, 0.75); + + --text-color: #e5e7eb; + --text-muted: #9aa7a6; + + --glass-border: rgba(255, 255, 255, 0.08); + + --card-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); +} + +.page { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 2rem; + min-height: 80vh; +} + +.theme-toggle { + background: transparent; + border: none; + cursor: pointer; + padding: 8px; + border-radius: 8px; + font-weight: 700; + background: var(--card); +} + +/* Hero */ +.hero { + width: 100%; + max-width: 1100px; + background: transparent; + text-align: center; + padding: 10px; +} + +.hero h1 { + margin: 10px 0 14px; + font-size: 36px; + line-height: 1.05; + color: #000606; +} + +.hero p { + margin: var(--bg-overlay); + color: var(--muted); + max-width: 820px; + margin-left: auto; + margin-right: auto; + color: #000606; +} + +/* Main card & input */ +.card { + width: 100%; + max-width: 1100px; + background: var(--card); + border-radius: 14px; + padding: 15px; + box-shadow: 0 18px 50px rgba(8, 24, 24, 0.06); +} + +.cta { + min-width: 220px; + padding: 14px 22px; + border-radius: 12px; + border: none; + color: rgb(12, 1, 1); + font-weight: 700; + cursor: pointer; + background: var(--accent-grad); + box-shadow: 0 12px 28px rgba(77, 163, 185, 0.12); +} + +.small-action { + display: flex; + align-items: center; + gap: 8px; + color: var(--muted); + margin-top: 10px; +} + +.result { + margin-top: 26px; + background: white; + border-radius: 12px; + padding: 20px; + border: 1px solid rgba(22, 60, 55, 0.03); + box-shadow: 0 8px 28px rgba(7, 20, 20, 0.03); +} + +.result-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.result-header .dot { + width: 30px; + height: 30px; + background: var(--success); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 700; +} + +.short-actions { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + padding: 10px 14px; + border-radius: 12px; + background: linear-gradient(180deg, rgba(75, 194, 176, 0.06), rgba(94, 207, 255, 0.04)); +} + +.short-box input { + align-items: center; + padding: 10px; + font-size: 15px; +} + +.btn-copy { + border: none; + padding: 10px 14px; + border-radius: 8px; + color: white; + font-weight: 700; + cursor: pointer; +} + +.btn-share { + background: #f2f5f5; + border: none; + padding: 10px 14px; + border-radius: 8px; + color: #0b2b2a; + font-weight: 700; + cursor: pointer; + margin-left: 6px; +} + +.meta-row { + align-items: center; + justify-content: center; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2px; + padding: 16px; + margin-top: 1px; + align-items: top; + color: black; +} + +.result-body { + margin-top: 30px; + + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.qr-block { + text-align: center; + padding-top: 8px; +} + +.qr-block img { + height: 15rem; + align-items: center; + aspect-ratio: 1; + box-shadow: 0 10px 20px rgba(10, 20, 30, 0.06); + outline: 2px solid green; + outline-offset: 4px; +} + +.download-qr { + display: inline-block; + margin-top: 12px; + text-decoration: none; + color: var(--accent-1); + font-weight: 700; +} + +.action-row { + display: flex; + justify-content: right; + align-items: right; +} + +.action-secondary { + background: #f6fbfb; + border: 1px solid rgba(0, 0, 0, 0.03); + border-radius: 10px; + cursor: pointer; + font-weight: 700; +} + +/* Force Generate QR to stay on one line */ +.qr-inline { + display: inline-flex; + align-items: center; + gap: 8px; + white-space: nowrap; +} + +.qr-inline input { + margin: 0; +} + +/* Responsive */ +@media (max-width: 880px) { + .input-row { + flex-direction: column; + } + + .cta { + width: 100%; + } + + .meta-row { + grid-template-columns: 1fr; + } +} + +.result-title { + font-weight: 700; + color: #0e34f6; +} + +.dark-theme .result-title { + color: #150cff; +} + +footer { + min-height: auto; +} + +/* /* =============================== + MODERN UI STYLE FOOTER (VISIT PAGE) +================================= */ + +.app-footer { + background: rgba(255, 255, 255, 0.02); + backdrop-filter: blur(16px); + border-top: 1px solid var(--glass-border); + padding: 2.5rem 1rem 1.2rem; + /* reduced space */ + margin-top: 40px; +} + +/* Container */ +.footer-container { + max-width: 1100px; + margin: 0 auto; + + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 1.8rem; +} + +/* Footer columns */ +.footer-col h4 { + font-size: 14px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 16px; + color: var(--text-primary); +} + +.footer-col p { + color: var(--text-secondary); + font-size: 14px; + line-height: 1.6; +} + +.footer-col ul { + list-style: none; + padding: 0; + margin: 0; +} + +.footer-col ul li { + margin-bottom: 10px; +} + +.footer-col ul li a { + color: var(--text-secondary); + text-decoration: none; + font-size: 14px; + transition: 0.2s ease; +} + +.footer-col ul li a:hover { + color: var(--accent); +} + +/* Footer bottom */ +.footer-bottom { + /* margin: 3rem auto 0; + padding-top: 20px; + + display: flex; + justify-content: space-between; + align-items: center; + + border-top: 1px solid var(--glass-border); + font-size: 14px; + color: var(--text-secondary); */ + + max-width: 1200px; + margin: 2rem auto 0; + padding-top: 1rem; + border-top: 1px solid var(--glass-border); + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + color: var(--text-secondary); + font-size: 0.8rem; + +} + +/* Version + GitHub area */ +.footer-bottom a { + color: var(--accent); + text-decoration: none; + transition: 0.2s ease; +} + +.footer-bottom a:hover { + opacity: 0.8; +} + +/* =============================== + DARK MODE SUPPORT +================================= */ + +.dark-theme .app-footer { + background: #0a0a0c; + backdrop-filter: blur(16px); + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.dark-theme .footer-col h4 { + color: #f3f4f6; +} + +.dark-theme .footer-col p, +.dark-theme .footer-col ul li a, +.dark-theme .footer-bottom { + color: #cbd5e1; +} + +.dark-theme .footer-col ul li a:hover, +.dark-theme .footer-bottom a { + color: var(--accent-2); +} + +/* =============================== + MOBILE RESPONSIVE +================================= */ + +@media (max-width: 900px) { + .footer-container { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 600px) { + .footer-container { + grid-template-columns: 1fr; + } + + .footer-bottom { + flex-direction: column; + gap: 10px; + text-align: center; + } +} + +/* REMOVE white line above footer in dark mode */ +footer { + margin-top: 0 !important; +} + +*/ +/* ===================================== + BIG FOOTER STYLE (FOR FIRST PAGE) + Using existing class names +===================================== */ + +/*Footer wrapper +.app-footer { + background: rgba(255, 255, 255, 0.01); + border-top: 1px solid var(--glass-border); + padding: 4rem 1rem 2rem; + margin-top: 4rem; +} + +/* Grid container */ +.footer-container { + max-width: 1200px; + margin: 0 auto; + + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 2rem; +} + +/* Brand column (first column) */ +.footer-container>div:first-child h4 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +.footer-container>div:first-child p { + color: var(--text-secondary); + line-height: 1.6; + max-width: 320px; +} + +.footer-brand h3 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +.footer-brand p { + color: var(--text-secondary); + line-height: 1.6; + max-width: 320px; +} + +/* Other footer columns */ +.footer-col h4 { + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.footer-col ul { + list-style: none; + padding: 0; + margin: 0; +} + +.footer-col ul li { + margin-bottom: 0.8rem; +} + +.footer-col ul li a { + color: var(--text-secondary); + text-decoration: none; + font-size: 0.9rem; + transition: color 0.2s ease; +} + +.footer-col ul li a:hover { + color: var(--accent); +} + +/* Bottom row */ +.footer-bottom { + max-width: 1200px; + margin: 2rem auto 0; + padding-top: 1rem; + border-top: 1px solid var(--glass-border); + + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + + color: var(--text-secondary); + font-size: 0.8rem; +} + +/* Footer links inside bottom */ +.footer-bottom a { + color: inherit; + text-decoration: none; + transition: color 0.2s ease; +} + +.footer-bottom a:hover { + color: var(--accent); +} + +/* Responsive */ +@media (max-width: 900px) { + .footer-container { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 600px) { + .footer-container { + grid-template-columns: 1fr; + } + + .footer-bottom { + flex-direction: column; + gap: 1rem; + text-align: center; + } +} + +*/ + +/* Footer */ +.big-footer { + background: rgba(255, 255, 255, 0.01); + border-top: 1px solid var(--glass-border); + padding: 4rem 1rem 2rem; + margin-top: 4rem; +} + +.footer-grid { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 2rem; +} + +.footer-brand h3 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +.footer-brand p { + color: var(--text-secondary); + line-height: 1.6; + max-width: 320px; +} + +.footer-col h4 { + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.footer-col ul { + list-style: none; + padding: 0; + margin: 0; +} + +.footer-col ul li { + margin-bottom: 0.8rem; +} + +.footer-col ul li a { + color: var(--text-secondary); + text-decoration: none; + font-size: 0.9rem; + transition: color 0.2s; +} + +.footer-col ul li a:hover { + color: var(--accent); +} + +.footer-bottom { + max-width: 1200px; + margin: 2rem auto 0; + padding-top: 1rem; + border-top: 1px solid var(--glass-border); + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + color: var(--text-secondary); + font-size: 0.8rem; +} + +.footer-meta { + display: flex; + gap: 1rem; +} + +.footer-meta a { + color: inherit; + text-decoration: none; +} + +.footer-meta a:hover { + color: var(--accent); +} + +/* Responsive adjustments */ +@media (max-width: 900px) { + .footer-grid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 700px) { + .result-card { + flex-direction: column; + align-items: flex-start; + } + + .result-actions { + align-items: flex-start; + } + + .recent-item { + min-width: 180px; + } +} + +@media (max-width: 600px) { + .hero-input-card h1 { + font-size: 2rem; + } + + .short-url a { + font-size: 1.2rem; + } + + .footer-grid { + grid-template-columns: 1fr; + } + + .footer-bottom { + flex-direction: column; + gap: 1rem; + text-align: center; + } +} + +/*=============================== + MODERN GLASS RECENT TABLE +================================ */ + +.recent-page-container { + width: 100%; + max-width: 1100px; + margin: 30px auto; + padding: 28px; + + background: var(--card); + backdrop-filter: blur(20px); + + border: 1px solid var(--glass-border); + border-radius: 20px; + + box-shadow: var(--card-shadow); + color: var(--text-color); + + transition: background 0.3s ease, border 0.3s ease; +} + +.recent-table-wrapper { + margin-top: 20px; + width: 100%; + overflow-x: auto; +} + +/* Table */ +.recent-table { + width: 100%; + border-collapse: collapse; + border-radius: 12px; + overflow: hidden; +} + +/* Header */ +.recent-table thead { + background: var(--glass); +} + +.recent-table th { + padding: 8px 14px; + text-align: left; + font-size: 13px; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 700; + color: var(--muted); + border-bottom: 1px solid var(--glass-border); +} + +/* Body cells */ +.recent-table td { + padding: 14px; + font-size: 14px; + color: var(--text-primary); + border-bottom: 1px solid var(--glass-border); + transition: 0.25s ease; +} + +/* Row hover */ +.recent-table tbody tr:hover { + background: rgba(255, 255, 255, 0.05); +} + +/* Short link */ +.short-code a { + color: var(--accent); + font-weight: 700; + text-decoration: none; +} + +.short-code a:hover { + color: var(--accent-2); + text-decoration: underline; +} + +/* Original URL */ +.original-url { + word-break: break-all; +} + +.original-url a { + color: var(--text-secondary); + text-decoration: none; +} + +.original-url a:hover { + color: var(--accent); +} + +/* Created time */ +.created-time { + font-size: 13px; + color: var(--muted); + white-space: nowrap; +} + +/* Visit count highlight */ +.recent-table td:nth-child(5) { + font-weight: 700; + color: var(--accent-2); +} + +/* Dark mode adjustments */ +.dark-theme .recent-table th, +.dark-theme .recent-table td { + color: #e5e7eb; + border-bottom: 1px solid var(--glass-border); +} + +/* Action buttons */ +.action-col { + display: flex; + gap: 10px; +} + +.action-btn { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + font-size: 16px; + transition: 0.2s ease; +} + +.open-btn { + background: #3b82f6; + color: #fff; +} + +.delete-btn { + background: #ef4444; + color: #fff; +} + +.recent-table-wrapper { + margin-bottom: 20px; +} + + +/* ========================= + Coming Soon Page +========================= */ + +.coming-soon-page { + display: flex; + align-items: center; + justify-content: center; + padding: 120px 20px 60px; +} + +.coming-soon-card { + max-width: 520px; + width: 100%; + background: var(--card); + border-radius: 16px; + padding: 50px 40px; + text-align: center; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.08); +} + +.coming-icon { + font-size: 48px; + margin-bottom: 18px; +} + +.coming-soon-card h1 { + font-size: 34px; + margin-bottom: 14px; + color: #000; +} + +.dark-theme .coming-soon-card h1 { + color: #fff; +} + +.coming-soon-card p { + font-size: 15px; + color: var(--muted); + line-height: 1.6; + margin-bottom: 28px; +} + +.coming-btn { + display: inline-block; + padding: 12px 22px; + border-radius: 10px; + background: var(--accent-grad); + color: #fff; + font-weight: 700; + text-decoration: none; + transition: 0.25s ease; +} + +.coming-btn:hover { + transform: scale(1.05); + box-shadow: 0 12px 28px rgba(77, 163, 185, 0.25); +} + +.info-box { + margin-bottom: 15px; + padding: 10px; + color: #0e34f6; + border-radius: 8px; + font-weight: 700; +} \ No newline at end of file diff --git a/app/templates/404.html b/app/templates/404.html new file mode 100644 index 0000000..0825534 --- /dev/null +++ b/app/templates/404.html @@ -0,0 +1,51 @@ +{% block content %} + + + + + Page Not Found + + + + +
+

🚫 Page not found

+

The page you are looking for does not exist.

+ + + ← Go Back + +
+ + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/coming-soon.html b/app/templates/coming-soon.html deleted file mode 100644 index 1a5e5eb..0000000 --- a/app/templates/coming-soon.html +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - Coming-soon - - - - - - -
-
- - tiny URL -
- - -
- -
-
- - -
-
-
🚧
-

Coming Soon

-

- This feature is under active development. -

- - ← Back to Home -
-
- - - - - \ No newline at end of file diff --git a/app/templates/footer.html b/app/templates/footer.html new file mode 100644 index 0000000..07569aa --- /dev/null +++ b/app/templates/footer.html @@ -0,0 +1,42 @@ + \ No newline at end of file diff --git a/app/templates/header.html b/app/templates/header.html index c7b19b1..0fa001c 100644 --- a/app/templates/header.html +++ b/app/templates/header.html @@ -1,11 +1,28 @@ - + \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 5423df2..6a36ebf 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,21 +1,12 @@ -{% extends "layout.html" %} - -{% block content %} +{% extends "layout.html" %} {% block content %}
-

tiny URL

+

Shorten Your Links

-
- - -
-
- - Analytics Enabled -
+
+
Analytics Enabled
@@ -36,88 +27,29 @@

tiny URL

-
- - {% if qr_image %} - Download QR - {% endif %} -
- - {% endif %} - -
+
{% if qr_image %} Download QR {% endif + %}
+
{% endif %}

Recently Shortened

View History β†’
-
- {% for url in urls %} -
+
{% for url in urls %}
/{{ url.short_code }}
{{ url.original_url }}
-
- {% endfor %} -
+
{% endfor %}
- - {% endblock %} \ No newline at end of file diff --git a/app/templates/layout.html b/app/templates/layout.html index e96619f..23d8a6f 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -27,12 +27,25 @@ {% block content %}{% endblock %} + {% include "footer.html" %} + diff --git a/app/templates/recent.html b/app/templates/recent.html index 5d89d19..ff32076 100644 --- a/app/templates/recent.html +++ b/app/templates/recent.html @@ -1,218 +1,82 @@ - - - - - - - Recent URLs - - - - - - -
-
- - tiny URL -
- - -
- -
-
- - - -
-
- -
- - - -
-

Recent Shortened URLs

-
- - -
- -
- - - - - - - - - - - - - - {% for url in urls %} - - - - - - - - - - - - - - - {% else %} - - - - {% endfor %} - -
#Short URLOriginal URLCreatedVisitsActions
{{ loop.index }} - - {{ request.host_url }}{{ url.short_code }} - - - - {{ url.original_url }} - - - {{ format_date(url.created_at) }} - {{ url.visit_count }} - - β†— - - - - πŸ—‘ - -
- No URLs found. -
-
- -
- -
- - - - - - - \ No newline at end of file +{% extends "layout.html" %} +{% block title %}Recent URLs{% endblock %} +{% block content %} +
+
+
+

Recent Shortened URLs

+
+
+
+ + + + + + + + + + + + {% for url in urls %} + + + + + + + + {% else %} + + {% endfor %} +
#Short URLOriginal URLCreatedVisitsActions
{{ loop.index }} {{ + request.host_url }}{{ url.short_code }} {{ + url.original_url }} {{ format_date(url.created_at) }} {{ url.visit_count }} β†— πŸ—‘
No URLs found.
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/utils/cache.py b/app/utils/cache.py index a9ac95a..a5e66ee 100644 --- a/app/utils/cache.py +++ b/app/utils/cache.py @@ -8,6 +8,7 @@ class UrlCacheItem(TypedDict): url: str expires_at: float + visit_count: int class RevCacheItem(TypedDict): @@ -72,6 +73,7 @@ def set_cache_pair(short_code: str, original_url: str) -> None: url_cache[short_code] = { "url": original_url, "expires_at": expires_at, + "visit_count": 0, } rev_cache[original_url] = { diff --git a/poetry.lock b/poetry.lock index 6ddbe34..963bebc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "annotated-doc" @@ -549,6 +549,7 @@ files = [ {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, ] +markers = {main = "extra == \"mongodb\""} [package.extras] dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] @@ -1873,6 +1874,7 @@ files = [ {file = "pymongo-4.16.0-cp39-cp39-win_arm64.whl", hash = "sha256:2a3ba6be3d8acf64b77cdcd4e36f0e4a8e87965f14a8b09b90ca86f10a1dd2f2"}, {file = "pymongo-4.16.0.tar.gz", hash = "sha256:8ba8405065f6e258a6f872fe62d797a28f383a12178c7153c01ed04e845c600c"}, ] +markers = {main = "extra == \"mongodb\""} [package.dependencies] dnspython = ">=2.6.1,<3.0.0" @@ -2353,6 +2355,18 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" +[[package]] +name = "tzdata" +version = "2025.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, + {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -2448,4 +2462,4 @@ mongodb = ["pymongo"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "349ea21b64217dca053564867ff9f7cf5375fe7c8dee163a5689d9f3a25fd371" +content-hash = "751a72153c538c8a27da935e798ed6ef5a365074e75da2a296a8ec499494a838" diff --git a/pyproject.toml b/pyproject.toml index 28417f7..6562170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "itsdangerous>=2.2.0,<3.0.0", "python-multipart>=0.0.22,<0.0.23", "jinja2>=3.1.2", + "tzdata (>=2025.3,<2026.0)", ] [project.optional-dependencies] mongodb = ["pymongo>=4.16.0,<5.0.0"] diff --git a/requirements.txt b/requirements.txt index 25863a5..a420fad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ anyio==4.12.1 ; python_version >= "3.10" and python_version < "3.13" async-timeout==5.0.1 ; python_version >= "3.10" and python_full_version < "3.11.3" click==8.3.1 ; python_version >= "3.10" and python_version < "3.13" colorama==0.4.6 ; python_version >= "3.10" and python_version < "3.13" and (sys_platform == "win32" or platform_system == "Windows") -dnspython==2.8.0 ; python_version >= "3.10" and python_version < "3.13" exceptiongroup==1.3.1 ; python_version == "3.10" fastapi==0.128.8 ; python_version >= "3.10" and python_version < "3.13" h11==0.16.0 ; python_version >= "3.10" and python_version < "3.13" @@ -15,7 +14,6 @@ markupsafe==3.0.3 ; python_version >= "3.10" and python_version < "3.13" pillow==12.1.1 ; python_version >= "3.10" and python_version < "3.13" pydantic-core==2.41.5 ; python_version >= "3.10" and python_version < "3.13" pydantic==2.12.5 ; python_version >= "3.10" and python_version < "3.13" -pymongo==4.16.0 ; python_version >= "3.10" and python_version < "3.13" python-dotenv==1.2.1 ; python_version >= "3.10" and python_version < "3.13" python-multipart==0.0.22 ; python_version >= "3.10" and python_version < "3.13" qrcode==8.2 ; python_version >= "3.10" and python_version < "3.13" @@ -23,5 +21,6 @@ redis==7.2.0 ; python_version >= "3.10" and python_version < "3.13" starlette==0.52.1 ; python_version >= "3.10" and python_version < "3.13" typing-extensions==4.15.0 ; python_version >= "3.10" and python_version < "3.13" typing-inspection==0.4.2 ; python_version >= "3.10" and python_version < "3.13" +tzdata==2025.3 ; python_version >= "3.10" and python_version < "3.13" uvicorn==0.40.0 ; python_version >= "3.10" and python_version < "3.13" validators==0.35.0 ; python_version >= "3.10" and python_version < "3.13"