diff --git a/.github/workflows/comment-on-issue.yml b/.github/workflows/comment-on-issue.yml index b54b4c5..387ff7a 100644 --- a/.github/workflows/comment-on-issue.yml +++ b/.github/workflows/comment-on-issue.yml @@ -1,4 +1,4 @@ -name: "Comment on New Issue" +name: "Comment on the Issue" on: issues: diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml deleted file mode 100644 index aef8b65..0000000 --- a/.github/workflows/echo.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Organization CI -on: - push: - branches: [$default-branch] - pull_request: - branches: [$default-branch] -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Run a one-line script - run: echo Hello from Action-Club diff --git a/.github/workflows/format-issue-title.yml b/.github/workflows/format-issue-title.yml index 603ef00..f797a3f 100644 --- a/.github/workflows/format-issue-title.yml +++ b/.github/workflows/format-issue-title.yml @@ -13,4 +13,4 @@ jobs: - name: Format issue title uses: recursivezero/action-club/.github/actions/format-issue-title@v0.2.57 with: - prefix: RTY + prefix: ${{ vars.PROJECT_PREFIX }} diff --git a/.github/workflows/issue-branch-sync.yml b/.github/workflows/issue-branch-sync.yml new file mode 100644 index 0000000..276b98d --- /dev/null +++ b/.github/workflows/issue-branch-sync.yml @@ -0,0 +1,24 @@ +name: Issue Sync with Branch +run-name: Notify whenever a branch created using issue title + +on: + create: + ref_type: branch # Ensure to trigger when a branch is created + +permissions: + issues: write # Required to comment and add labels + contents: read # Required to read the repo metadata + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Link Branch to Issue + # Point this to your centralized repository + uses: recursivezero/action-club/.github/actions/branch-notify@v0.2.57 + with: + issue_prefix: ${{ vars.PROJECT_PREFIX }} # e.g., "RTY" + branch_name: ${{ github.event.ref }} diff --git a/.gitignore b/.gitignore index 8a7644e..d0aec68 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__/ .venv* venv/ +.venv-dist/ env/ .venv* diff --git a/app/main.py b/app/main.py index 932e5e4..a1d2d60 100644 --- a/app/main.py +++ b/app/main.py @@ -3,18 +3,12 @@ from pathlib import Path import logging import asyncio - from fastapi import FastAPI, Request - 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.exceptions import HTTPException as FastAPIHTTPException from fastapi.templating import Jinja2Templates - from app.routes import ui_router from app.utils import db from app.utils.cache import cleanup_expired @@ -115,26 +109,6 @@ async def lifespan(app: FastAPI): 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(404) -# async def custom_404_handler(request: Request, exc): -# return templates.TemplateResponse( -# "404.html", -# {"request": request}, -# status_code=404, -# ) - @app.exception_handler(FastAPIHTTPException) async def http_exception_handler(request: Request, exc: FastAPIHTTPException): diff --git a/app/routes.py b/app/routes.py index ba5f165..034d54c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -34,11 +34,21 @@ remove_cache_key, rev_cache, ) -from app.utils.config import DOMAIN, MAX_RECENT_URLS, CACHE_PURGE_TOKEN, QR_DIR -from app.utils.helper import generate_code, is_valid_url, sanitize_url, format_date +from app.utils.config import ( + DOMAIN, + MAX_RECENT_URLS, + CACHE_PURGE_TOKEN, + QR_DIR, +) +from app.utils.helper import ( + generate_code, + sanitize_url, + is_valid_url, + authorize_url, + format_date, +) from app.utils.qr import generate_qr_with_logo -# templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) templates = Jinja2Templates(directory="app/templates") # Routers ui_router = APIRouter() @@ -98,12 +108,18 @@ async def create_short_url( qr_type: str = Form("short"), ): session = request.session - original_url = sanitize_url(original_url) + original_url = sanitize_url(original_url) # sanitize the URL input - if not original_url or not is_valid_url(original_url): + if not original_url or not is_valid_url(original_url): # validate the URL session["error"] = "Please enter a valid URL." return RedirectResponse("/", status_code=status.HTTP_303_SEE_OTHER) + if not authorize_url( + original_url + ): # authorize the URL based on whitelist/blacklist + session["error"] = "This domain is not allowed." + return RedirectResponse("/", status_code=status.HTTP_303_SEE_OTHER) + short_code: Optional[str] = get_short_from_cache(original_url) if not short_code and db.is_connected(): @@ -219,7 +235,6 @@ 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) raise HTTPException(status_code=404, detail="Page not found") @@ -331,9 +346,13 @@ class ShortenRequest(BaseModel): @api_v1.post("/shorten") def shorten_api(payload: ShortenRequest): original_url = sanitize_url(payload.url) + if not is_valid_url(original_url): return JSONResponse(status_code=400, content={"error": "INVALID_URL"}) + if not authorize_url(original_url): + return JSONResponse(status_code=400, content={"error": "DOMAIN_NOT_ALLOWED"}) + short_code = get_short_from_cache(original_url) if not short_code: short_code = generate_code() diff --git a/app/static/css/tiny.css b/app/static/css/tiny.css index daf3df1..9f50fb2 100644 --- a/app/static/css/tiny.css +++ b/app/static/css/tiny.css @@ -1,3 +1,17 @@ +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; +} + :root { --bg: #0a0a0c; --glass: rgba(255, 255, 255, 0.03); @@ -13,47 +27,35 @@ body { 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); - - /* main card + text */ --card: #ffffff; --text-primary: #111827; --text-secondary: #4b5563; --text-color: #111827; - - /* accent */ --accent: #2563eb; - - /* Remove the dark radial gradient */ background-image: none; } -/* Layout */ +.container { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100vh; +} + .main-layout { - max-width: 900px; - margin: 0 auto; - padding: 6rem 1rem 4rem; + width: 90%; + margin: 2rem auto; display: flex; flex-direction: column; - gap: 2rem; -} - -.page { - padding-top: 6rem; + gap: 1rem; } .app-header { - position: fixed; - top: 0; - left: 0; width: 100%; height: 55px; display: flex; @@ -69,9 +71,7 @@ body.light-theme { 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); } @@ -156,7 +156,6 @@ body.dark-theme .app-header { width: 100%; } - .dark-theme .nav-link.active::after { background: #f8fafc; } @@ -202,6 +201,13 @@ body.dark-theme .app-header { -webkit-text-fill-color: transparent; } +.alert-error { + text-align: center; + color: #ff6b6b; + margin-top: 8px; + font-size: 14px; +} + .input-wrapper { display: flex; gap: 1rem; @@ -215,7 +221,7 @@ body.dark-theme .app-header { border: 1px solid var(--glass-border); border-radius: 1.5rem; padding: 1rem; - font-size: 1rem; + font-size: 2rem; color: var(--text-primary); } @@ -228,6 +234,7 @@ body.dark-theme .app-header { border: none; cursor: pointer; transition: background 0.3s; + font-size: larger; } .btn-primary:hover { @@ -250,7 +257,6 @@ body.dark-theme .app-header { cursor: pointer; } -/* Result card */ .result-card { width: 100%; background: linear-gradient(145deg, rgba(99, 102, 241, 0.1), rgba(0, 0, 0, 0)); @@ -323,10 +329,10 @@ body.dark-theme .app-header { color: var(--accent); } -/* Recent tray */ .recent-tray { width: 100%; margin-top: 2rem; + overflow: visible; } .recent-header { @@ -341,7 +347,9 @@ body.dark-theme .app-header { display: flex; gap: 1rem; overflow-x: auto; - padding: 1rem 0; + padding: 1rem 16px; + scroll-padding-right: 16px; + box-sizing: border-box; } .scroll-container::-webkit-scrollbar { @@ -349,7 +357,8 @@ body.dark-theme .app-header { } .recent-item { - min-width: 220px; + width: 260px; + flex: 0 0 auto; background: var(--glass); border: 1px solid var(--glass-border); padding: 1rem; @@ -360,6 +369,10 @@ body.dark-theme .app-header { flex-shrink: 0; } +.recent-item:last-child { + margin-right: 16px; +} + .short-code { color: var(--accent); font-weight: bold; @@ -378,23 +391,16 @@ body.dark-theme .app-header { /* =============================== 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; } @@ -411,7 +417,6 @@ body.dark-theme .app-header { min-width: 800px; } -/* Header */ .recent-table thead { background: var(--glass); } @@ -428,7 +433,6 @@ body.dark-theme .app-header { white-space: nowrap; } -/* Body cells */ .recent-table td { padding: 14px; font-size: 14px; @@ -439,7 +443,6 @@ body.dark-theme .app-header { white-space: nowrap; } -/* Row hover */ .recent-table tbody tr:hover { background: rgba(255, 255, 255, 0.05); } @@ -448,7 +451,6 @@ body.dark-theme .app-header { COLUMN WIDTH CONTROL ================================= */ -/* # column */ .recent-table th:nth-child(1), .recent-table td:nth-child(1) { width: 45px; @@ -457,26 +459,22 @@ body.dark-theme .app-header { 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; @@ -485,7 +483,6 @@ body.dark-theme .app-header { color: var(--accent-2); } -/* Actions */ .recent-table th:nth-child(6), .recent-table td:nth-child(6) { width: 120px; @@ -506,7 +503,6 @@ body.dark-theme .app-header { text-decoration: underline; } -/* Original URL truncate */ .original-url { word-break: break-all; } @@ -523,7 +519,6 @@ body.dark-theme .app-header { color: var(--accent); } -/* Created time */ .created-time { font-size: 13px; color: var(--muted); @@ -572,21 +567,18 @@ body.dark-theme .app-header { 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; @@ -599,6 +591,9 @@ footer.big-footer { border-top: 1px solid var(--glass-border); padding: 4rem 1rem 2rem; margin-top: 4rem; + position: fixed; + bottom: 0; + width: 100%; } .footer-grid { @@ -739,31 +734,25 @@ body.dark-theme .footer-bottom a { } } +.recent-tray .recent-item .original-url { + max-width: 350px; + min-width: 0; +} - - -/* 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; - + -webkit-line-clamp: 2; overflow: hidden; text-overflow: ellipsis; - white-space: normal; - word-break: break-word; + word-break: break-all; overflow-wrap: anywhere; } -/* IMPORTANT — remove width restriction */ .recent-tray .recent-item { min-width: 0; - /* allows shrinking inside flex/grid */ max-width: 100%; } @@ -787,4 +776,4 @@ body.light-theme .history-link { .history-link:hover { opacity: 0.7; -} \ No newline at end of file +} diff --git a/app/static/og-image.png b/app/static/og-image.png new file mode 100644 index 0000000..96f3ccf Binary files /dev/null and b/app/static/og-image.png differ diff --git a/app/static/style.css b/app/static/style.css deleted file mode 100644 index 8dd7671..0000000 --- a/app/static/style.css +++ /dev/null @@ -1,1063 +0,0 @@ -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/footer.html b/app/templates/footer.html index 18064fc..e6599f5 100644 --- a/app/templates/footer.html +++ b/app/templates/footer.html @@ -1,41 +1,39 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/app/templates/header.html b/app/templates/header.html index 0fa001c..42599ee 100644 --- a/app/templates/header.html +++ b/app/templates/header.html @@ -1,19 +1,17 @@ -
-
- - RZRO.link -
- -
- -
-
+
+ + RZRO.link +
+ +
+ +