From 20cfa7979382125af3d6823da883ffe1710b330a Mon Sep 17 00:00:00 2001 From: Ravindrayadav04 Date: Tue, 24 Feb 2026 21:26:15 +0530 Subject: [PATCH 1/6] fix the bug related to visit count and change the ui also --- .env.sample | 4 +- app/main.py | 32 + app/static/style.css | 1715 +++++++++++++++++++------------------ app/templates/recent.html | 17 +- app/utils/cache.py | 21 +- app/utils/config.py | 5 +- app/utils/db.py | 40 +- 7 files changed, 987 insertions(+), 847 deletions(-) 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/app/main.py b/app/main.py index 8f393ba..0779b02 100644 --- a/app/main.py +++ b/app/main.py @@ -216,12 +216,44 @@ async def delete_url(request: Request, short_code: str): return PlainTextResponse("", status_code=204) +#@app.get("/{short_code}") +#async def redirect_short(request: Request, short_code: str): +# logger = logging.getLogger(__name__) +# # Try cache first +# cached_url = get_from_cache(short_code) +# if cached_url: +# return RedirectResponse(cached_url) + +# # Check if database is connected +# if not db.is_connected(): +# logger.warning(f"Database not connected, cannot redirect {short_code}") +# return PlainTextResponse( +# "Service temporarily unavailable. Please try again later.", +# status_code=503, +# headers={"Retry-After": "30"}, +# ) + +# # Try database +# doc = db.increment_visit(short_code) +# if doc: +# set_cache_pair(short_code, doc["original_url"]) +# return RedirectResponse(doc["original_url"]) + +# return PlainTextResponse("Invalid or expired short URL", status_code=404) + @app.get("/{short_code}") async def redirect_short(request: Request, short_code: str): logger = logging.getLogger(__name__) # Try cache first cached_url = get_from_cache(short_code) if cached_url: + + if db.is_connected(): + db.increment_visit(short_code) + else: + from app.utils.cache import increment_cache_visit + increment_cache_visit(short_code) + return RedirectResponse(cached_url) # Check if database is connected diff --git a/app/static/style.css b/app/static/style.css index b754687..d32e8d9 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -1,815 +1,900 @@ -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; +} + +.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); +}*/ + +: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); +} + +.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; +} + +.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; +} + + +/*=============================== + 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/recent.html b/app/templates/recent.html index 26c5d69..d20b35a 100644 --- a/app/templates/recent.html +++ b/app/templates/recent.html @@ -10,7 +10,7 @@ - +
@@ -205,6 +205,21 @@

Support

+ + \ No newline at end of file diff --git a/app/utils/cache.py b/app/utils/cache.py index a6f1c6a..9150f4a 100644 --- a/app/utils/cache.py +++ b/app/utils/cache.py @@ -7,6 +7,7 @@ class UrlCacheItem(TypedDict): url: str expires_at: float + visit_count: int class RevCacheItem(TypedDict): @@ -60,8 +61,9 @@ def set_cache_pair(short_code: str, original_url: str) -> None: expires_at = now + CACHE_TTL url_cache[short_code] = { - "url": original_url, - "expires_at": expires_at, + "url": original_url, + "expires_at": expires_at, + "visit_count": 0, } rev_cache[original_url] = { @@ -112,13 +114,14 @@ def get_recent_from_cache(limit: int = MAX_RECENT_URLS) -> list[dict]: now = _now() items = [ - { - "short_code": data["short_code"], - "original_url": original_url, - } - for original_url, data in rev_cache.items() - if data["expires_at"] >= now - ] + { + "short_code": data["short_code"], + "original_url": original_url, + "visit_count": url_cache.get(data["short_code"], {}).get("visit_count", 0), + } + for original_url, data in rev_cache.items() + if data["expires_at"] >= now + ] items.sort( key=lambda x: rev_cache[x["original_url"]]["last_accessed"], reverse=True diff --git a/app/utils/config.py b/app/utils/config.py index af78124..7bd3cec 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -1,14 +1,15 @@ - import os + # ------------------------- # Helpers # ------------------------- -from app.utils.config_env import load_env # noqa: F401 +from app.utils.config_env import load_env # noqa: F401 load_env() + def _get_int(key: str, default: int) -> int: try: return int(os.getenv(key, default)) diff --git a/app/utils/db.py b/app/utils/db.py index be7ed0c..eb9453d 100644 --- a/app/utils/db.py +++ b/app/utils/db.py @@ -34,10 +34,10 @@ def connect_db(max_retries: Optional[int] = None) -> bool: """ Connect to MongoDB with retry logic and exponential backoff. - + Args: max_retries: Maximum number of retry attempts (defaults to config value) - + Returns: True if connection successful, False otherwise """ @@ -68,8 +68,10 @@ def connect_db(max_retries: Optional[int] = None) -> bool: for attempt in range(1, max_retries + 1): connection_state = "CONNECTING" last_connection_attempt = datetime.utcnow() - - logger.info(f"Attempting to connect to MongoDB (attempt {attempt}/{max_retries})...") + + logger.info( + f"Attempting to connect to MongoDB (attempt {attempt}/{max_retries})..." + ) try: # Create MongoClient with timeout and pool settings @@ -80,17 +82,17 @@ def connect_db(max_retries: Optional[int] = None) -> bool: minPoolSize=MONGO_MIN_POOL_SIZE, maxPoolSize=MONGO_MAX_POOL_SIZE, ) - + # Validate connection with ping new_client.admin.command("ping") - + # Connection successful client = new_client db = new_client[MONGO_DB_NAME] collection = db[MONGO_COLLECTION] connection_state = "CONNECTED" connection_error = None - + logger.info("Successfully connected to MongoDB") return True @@ -98,7 +100,7 @@ def connect_db(max_retries: Optional[int] = None) -> bool: error_msg = f"Connection attempt {attempt} failed: {str(e)}" logger.warning(error_msg) connection_error = str(e) - + if attempt < max_retries: logger.info(f"Retrying in {retry_delay:.1f} seconds...") time.sleep(retry_delay) @@ -120,7 +122,9 @@ def get_connection_state() -> dict[str, Any]: """Return current connection state information.""" return { "state": connection_state, - "last_attempt": last_connection_attempt.isoformat() if last_connection_attempt else None, + "last_attempt": ( + last_connection_attempt.isoformat() if last_connection_attempt else None + ), "error": connection_error, "connected": is_connected(), } @@ -227,23 +231,23 @@ def increment_visit(short_code: str) -> Optional[dict]: async def health_check_loop() -> None: """Background task that periodically checks database connection health.""" global connection_state, connection_error - + from app.utils.config import HEALTH_CHECK_INTERVAL_SECONDS - + logger.info("Health check loop started") - + try: while True: await asyncio.sleep(HEALTH_CHECK_INTERVAL_SECONDS) - + logger.debug("Running health check...") - + # If disconnected, try to reconnect if not is_connected(): logger.info("Database disconnected, attempting reconnection...") connect_db() continue - + # Validate active connection with ping try: if client is not None: @@ -253,7 +257,7 @@ async def health_check_loop() -> None: logger.error(f"Health check failed: {str(e)}") connection_state = "FAILED" connection_error = str(e) - + except asyncio.CancelledError: logger.info("Health check loop cancelled") raise @@ -262,7 +266,7 @@ async def health_check_loop() -> None: def start_health_check() -> Any: """Start the background health check task.""" global health_check_task - + health_check_task = asyncio.create_task(health_check_loop()) logger.info("Health check task started") return health_check_task @@ -271,7 +275,7 @@ def start_health_check() -> Any: async def stop_health_check() -> None: """Stop the background health check task.""" global health_check_task - + if health_check_task is not None: logger.info("Stopping health check task...") health_check_task.cancel() From f2bb9f534ea2e6489d167c32d89f9329d0f04be4 Mon Sep 17 00:00:00 2001 From: Ravindrayadav04 Date: Thu, 26 Feb 2026 21:37:05 +0530 Subject: [PATCH 2/6] [RTY-260028]: fiexed the whole ui header and footer align --- app/main.py | 51 ++-- app/static/css/tiny.css | 337 +++++++++++++++++++++++-- app/static/style.css | 519 +++++++++++++++++++++++++------------- app/templates/footer.html | 41 +++ app/templates/header.html | 19 +- app/templates/index.html | 117 ++------- app/templates/layout.html | 27 +- app/templates/recent.html | 286 +++++---------------- app/utils/cache.py | 22 +- 9 files changed, 849 insertions(+), 570 deletions(-) create mode 100644 app/templates/footer.html diff --git a/app/main.py b/app/main.py index 0779b02..8eeba0f 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,12 @@ import logging from fastapi import FastAPI, Form, Request, status -from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse, JSONResponse +from fastapi.responses import ( + HTMLResponse, + PlainTextResponse, + RedirectResponse, + JSONResponse, +) from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from starlette.middleware.sessions import SessionMiddleware @@ -39,12 +44,12 @@ async def lifespan(app: FastAPI): db.connect_db() db.start_health_check() logger.info("Application startup complete") - + yield - + logger.info("Application shutdown: Cleaning up...") await db.stop_health_check() - + # Close MongoDB client gracefully try: if db.client is not None: @@ -52,7 +57,7 @@ async def lifespan(app: FastAPI): logger.info("MongoDB client closed") except Exception as e: logger.error(f"Error closing MongoDB client: {str(e)}") - + logger.info("Application shutdown complete") @@ -123,7 +128,7 @@ async def create_short_url( qr_type: str = Form("short"), ) -> RedirectResponse: logger = logging.getLogger(__name__) - + session = request.session qr_enabled = bool(generate_qr) original_url = sanitize_url(original_url) @@ -149,13 +154,17 @@ async def create_short_url( if not short_code: short_code = generate_code() set_cache_pair(short_code, original_url) - + # Only write to database if connected if db.is_connected(): db.insert_url(short_code, original_url) else: - logger.warning(f"Database not connected, URL {short_code} created in cache only") - session["info_message"] = "URL created (database temporarily unavailable)" + logger.warning( + f"Database not connected, URL {short_code} created in cache only" + ) + session["info_message"] = ( + "URL created (database temporarily unavailable)" + ) # --- TYPE GUARD FOR MYPY --- if not isinstance(short_code, str): @@ -180,9 +189,9 @@ async def create_short_url( @app.get("/recent", response_class=HTMLResponse) async def recent_urls(request: Request): - recent_urls_list = db.get_recent_urls( + recent_urls_list = db.get_recent_urls(MAX_RECENT_URLS) or get_recent_from_cache( MAX_RECENT_URLS - ) or get_recent_from_cache(MAX_RECENT_URLS) + ) normalized = [] for item in recent_urls_list: @@ -216,8 +225,8 @@ async def delete_url(request: Request, short_code: str): return PlainTextResponse("", status_code=204) -#@app.get("/{short_code}") -#async def redirect_short(request: Request, short_code: str): +# @app.get("/{short_code}") +# async def redirect_short(request: Request, short_code: str): # logger = logging.getLogger(__name__) # # Try cache first # cached_url = get_from_cache(short_code) @@ -241,6 +250,7 @@ async def delete_url(request: Request, short_code: str): # return PlainTextResponse("Invalid or expired short URL", status_code=404) + @app.get("/{short_code}") async def redirect_short(request: Request, short_code: str): logger = logging.getLogger(__name__) @@ -252,25 +262,26 @@ async def redirect_short(request: Request, short_code: str): db.increment_visit(short_code) else: from app.utils.cache import increment_cache_visit + increment_cache_visit(short_code) return RedirectResponse(cached_url) - + # Check if database is connected if not db.is_connected(): logger.warning(f"Database not connected, cannot redirect {short_code}") return PlainTextResponse( "Service temporarily unavailable. Please try again later.", status_code=503, - headers={"Retry-After": "30"} + headers={"Retry-After": "30"}, ) - + # Try database doc = db.increment_visit(short_code) if doc: set_cache_pair(short_code, doc["original_url"]) return RedirectResponse(doc["original_url"]) - + return PlainTextResponse("Invalid or expired short URL", status_code=404) @@ -283,15 +294,15 @@ async def coming_soon(request: Request): async def health_check(): """Health check endpoint showing database and cache status.""" state = db.get_connection_state() - + response_data = { "database": state, "cache": { "enabled": True, "size": len(url_cache), - } + }, } - + status_code = 200 if state["connected"] else 503 return JSONResponse(content=response_data, status_code=status_code) diff --git a/app/static/css/tiny.css b/app/static/css/tiny.css index 873f075..1224c96 100644 --- a/app/static/css/tiny.css +++ b/app/static/css/tiny.css @@ -27,69 +27,138 @@ body { /* 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; - /* Remove or soften the dark gradient */ + /* accent */ + --accent: #2563eb; + + /* 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 { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.5rem; +.page { + padding-top: 6rem; +} + +/* Top navigation header */ +.app-header { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 55px; background: var(--glass); border-bottom: 1px solid var(--glass-border); - backdrop-filter: blur(10px); + display: flex; + align-items: center; + padding: 0 28px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); + z-index: 1000; } .header-left, .header-right { display: flex; - gap: 1rem; align-items: center; + gap: 12px; } -.header-center { - flex: 1; - text-align: center; +.header-right { + margin-left: auto; } -.logo { - margin: 0; +.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; +} + +.app-name { font-size: 1.5rem; font-weight: 700; color: var(--text-primary); } -.icon-btn { - background: none; - border: none; +.header-nav { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 26px; +} + +.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; +} + +.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); } @@ -289,9 +358,202 @@ body.light-theme { text-overflow: ellipsis; } + +.hero { + text-align: center; + margin: 40px 0; +} + +.hero h1 { + font-size: 42px; + font-weight: 700; +} + +/* Recent page full table layout (visit page) */ +.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: hidden; +} + +.recent-table { + width: 100%; + border-collapse: collapse; + border-radius: 12px; + overflow: hidden; + table-layout: fixed; +} + +.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); +} + +.recent-table td { + padding: 14px; + font-size: 14px; + color: var(--text-primary); + border-bottom: 1px solid var(--glass-border); + transition: 0.25s ease; +} + +/* Stable column widths so important columns stay visible */ +.recent-table th:nth-child(1), +.recent-table td:nth-child(1) { + width: 6%; +} + +.recent-table th:nth-child(2), +.recent-table td:nth-child(2) { + width: 18%; +} + +.recent-table th:nth-child(3), +.recent-table td:nth-child(3) { + width: 38%; +} + +.recent-table th:nth-child(4), +.recent-table td:nth-child(4) { + width: 18%; +} + +.recent-table th:nth-child(5), +.recent-table td:nth-child(5) { + width: 8%; +} + +.recent-table th:nth-child(6), +.recent-table td:nth-child(6) { + width: 12%; +} + +.recent-table tbody tr:hover { + background: rgba(255, 255, 255, 0.05); +} + +.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 a { + color: var(--text-secondary); + text-decoration: none; +} + +.original-url a:hover { + color: var(--accent); +} + +.created-time { + font-size: 13px; + color: var(--muted); + white-space: nowrap; +} + +.recent-table td:nth-child(5) { + font-weight: 700; + color: var(--accent-2); +} + +.dark-theme .recent-table th, +.dark-theme .recent-table td { + color: #e5e7eb; + border-bottom: 1px solid var(--glass-border); +} + +.action-col { + display: flex; + gap: 12px; + justify-content: flex-end; + align-items: center; +} + +.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; +} + + +/* 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; +} + /* 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 +634,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 { @@ -412,4 +695,4 @@ body.light-theme { gap: 1rem; text-align: center; } -} +} \ No newline at end of file diff --git a/app/static/style.css b/app/static/style.css index d32e8d9..8dd7671 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -147,25 +147,6 @@ button { 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; } @@ -177,77 +158,6 @@ 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; @@ -278,6 +188,9 @@ body.dark-theme section { /* 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 { @@ -538,154 +451,404 @@ footer { min-height: auto; } +/* /* =============================== + MODERN UI STYLE FOOTER (VISIT PAGE) +================================= */ + .app-footer { - background: white; - color: #e5e7eb; - padding: 8px 10px; - margin-top: auto; - position: relative; + 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; } -.dark-theme .app-footer { - background: linear-gradient(180deg, #0b1220, #050b14); +/* Container */ +.footer-container { + max-width: 1100px; + margin: 0 auto; + + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 1.8rem; } -.footer-container { - margin: auto; +/* 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; - gap: 60px; 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-brand { - max-width: 420px; +.footer-bottom a:hover { + opacity: 0.8; } -.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 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-brand h3, -.dark-theme .footer-brand p, -.dark-theme .footer-col h4, -.dark-theme .app-footer a, +.dark-theme .footer-col h4 { + color: #f3f4f6; +} + +.dark-theme .footer-col p, +.dark-theme .footer-col ul li a, .dark-theme .footer-bottom { - color: #f8fafc; + color: #cbd5e1; } -.footer-brand h3 { - margin: 0; - color: #000000; - font-size: 22px; - font-weight: 700; +.dark-theme .footer-col ul li a:hover, +.dark-theme .footer-bottom a { + color: var(--accent-2); } -.footer-brand p { - margin-top: 8px; - color: #000000; - line-height: 1.6; - font-size: 14px; +/* =============================== + 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; } -/* MAIN CONTENT */ +*/ +/* ===================================== + BIG FOOTER STYLE (FOR FIRST PAGE) + Using existing class names +===================================== */ -/* FOOTER */ +/*Footer wrapper .app-footer { - margin-top: auto; + background: rgba(255, 255, 255, 0.01); + border-top: 1px solid var(--glass-border); + padding: 4rem 1rem 2rem; + margin-top: 4rem; } -/* 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; +/* Grid container */ +.footer-container { + max-width: 1200px; + margin: 0 auto; + + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 2rem; } -.github-btn:hover { - background: black(11, 1, 1); - transform: translateY(-2px); +/* Brand column (first column) */ +.footer-container>div:first-child h4 { + font-size: 1.5rem; + margin-bottom: 1rem; } -.footer-links { - display: flex; - gap: 80px; - flex-wrap: wrap; +.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 { - margin-bottom: 14px; - font-size: 16px; - color: #000000; - font-weight: 700; + 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 a { - display: block; +.footer-col ul li a { + color: var(--text-secondary); text-decoration: none; - color: #000000; - margin-bottom: 10px; - font-size: 14px; + font-size: 0.9rem; transition: color 0.2s ease; } -.footer-col a:hover { - text-decoration: underline; +.footer-col ul li a:hover { + color: var(--accent); } -/* Bottom */ +/* Bottom row */ .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; + 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: #030000; - font-weight: 600; + color: inherit; text-decoration: none; + transition: color 0.2s ease; } .footer-bottom a:hover { - text-decoration: underline; + color: var(--accent); } /* Responsive */ -@media (max-width: 768px) { +@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: 40px; + gap: 1rem; + text-align: center; } +} - .footer-links { - gap: 40px; +*/ + +/* 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; } } -/* REMOVE white line above footer in dark mode */ -footer { - margin-top: 0 !important; +@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 diff --git a/app/templates/footer.html b/app/templates/footer.html new file mode 100644 index 0000000..84e04aa --- /dev/null +++ b/app/templates/footer.html @@ -0,0 +1,41 @@ + + diff --git a/app/templates/header.html b/app/templates/header.html index c7b19b1..88c06db 100644 --- a/app/templates/header.html +++ b/app/templates/header.html @@ -1,11 +1,16 @@ -