From 53ab98071ad62c8402e0db4f37d1aa095ec302f8 Mon Sep 17 00:00:00 2001 From: RecursiveZero Date: Mon, 16 Feb 2026 22:03:51 +0530 Subject: [PATCH 01/34] separate config env --- app/main.py | 5 +- app/utils/config.py | 23 ++----- app/utils/config_env.py | 15 +++++ group_shorten.ps1 | 134 ---------------------------------------- 4 files changed, 22 insertions(+), 155 deletions(-) create mode 100644 app/utils/config_env.py delete mode 100644 group_shorten.ps1 diff --git a/app/main.py b/app/main.py index cea15eb..09bb540 100644 --- a/app/main.py +++ b/app/main.py @@ -18,7 +18,7 @@ set_cache_pair, url_cache, ) -from app.utils.config import DOMAIN, MAX_RECENT_URLS, SESSION_SECRET, load_env +from app.utils.config import DOMAIN, MAX_RECENT_URLS, SESSION_SECRET from app.utils.helper import ( format_date, generate_code, @@ -27,7 +27,6 @@ ) from app.utils.qr import generate_qr_with_logo -load_env() # ----------------------------- @@ -151,7 +150,7 @@ async def create_short_url( return RedirectResponse("/", status_code=303) # Mypy now knows short_code is strictly 'str' - new_short_url = build_short_url(short_code, str(request.base_url)) + new_short_url = build_short_url(short_code, DOMAIN) session.update( { diff --git a/app/utils/config.py b/app/utils/config.py index c29ffec..0b9d73f 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -1,27 +1,14 @@ -import os - -from dotenv import load_dotenv - - -def load_env(): - env = os.getenv("ENV", "development") - file_map = { - "production": ".env", - "local": ".env.local", - "development": ".env.development", - } - load_dotenv(file_map.get(env, ".env.development"), override=True) - print(f"Environment selected: {env}") - print(f"MODE value: {os.getenv('MODE')}") - +import os # ------------------------- # Helpers # ------------------------- -def _get_bool(key: str, default: bool) -> bool: - return os.getenv(key, str(default)).lower() in ("1", "true", "yes", "on") +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/config_env.py b/app/utils/config_env.py new file mode 100644 index 0000000..049232d --- /dev/null +++ b/app/utils/config_env.py @@ -0,0 +1,15 @@ +import os + +from dotenv import load_dotenv + + +def load_env(): + env = os.getenv("ENV", "development") + file_map = { + "production": ".env", + "local": ".env.local", + "development": ".env.development", + } + load_dotenv(file_map.get(env, ".env.development"), override=True) + print(f"Environment selected: {env}") + print(f"MODE value: {os.getenv('MODE')}") diff --git a/group_shorten.ps1 b/group_shorten.ps1 deleted file mode 100644 index 3e52c10..0000000 --- a/group_shorten.ps1 +++ /dev/null @@ -1,134 +0,0 @@ -<# -========================================================= - PowerShell Script: group_shorten.ps1 ---------------------------------------------------------- - Description: - Bulk URL shortener client for Tiny API. - Reads a JSON file containing multiple URLs and sends - them one-by-one to the /api/shorten endpoint. - ---------------------------------------------------------- - HOW TO RUN (Windows PowerShell TERMINAL) ---------------------------------------------------------- - - 1. Open PowerShell as Administrator - - 2. Allow script execution (one-time): - Set-ExecutionPolicy RemoteSigned - - 3. Navigate to project root: - cd path\to\your\project - - 4. Run with JSON file: - .\group_shorten.ps1 -file ".\request\mixed_urls.json" - - 5. Or run without argument (interactive): - .\group_shorten.ps1 - - ---------------------------------------------------------- - HOW TO RUN (Windows PowerShell ISE) ---------------------------------------------------------- - - Method 1: Run with parameter - -------------------------------- - 1. Open Windows PowerShell ISE - 2. Open this script file (group_shorten.ps1) - 3. In the top menu, click: - File → New PowerShell Tab - 4. In the console pane (bottom), run: - .\group_shorten.ps1 -file ".\request\mixed_urls.json" - - Method 2: Run interactively - -------------------------------- - 1. Open Windows PowerShell ISE - 2. Open this script file - 3. Press the green ▶️ Run Script button - 4. When prompted, enter the JSON file path - ---------------------------------------------------------- - Expected JSON input format: - [ - { "url": "https://example.com" }, - { "url": "https://google.com" } - ] -========================================================= -#> - -param ( - [string]$file -) - -# Ask for file path if not provided -if (-not $file) { - $file = Read-Host "Enter path to JSON file" -} - -# Validate file -if (-not (Test-Path $file)) { - Write-Host "❌ File not found: $file" -ForegroundColor Red - exit 1 -} - -# Load JSON -try { - $data = Get-Content $file -Raw | ConvertFrom-Json -} -catch { - Write-Host "❌ Invalid JSON file" -ForegroundColor Red - exit 1 -} - -$apiUrl = "http://127.0.0.1:8001/api/v1/shorten" - -Write-Host "`n🚀 Processing URLs..." -ForegroundColor Cyan - -foreach ($item in $data) { - - # Support string + object format - $url = if ($item -is [string]) { $item } else { $item.url } - - if (-not $url) { - Write-Host "⚠️ Empty URL, skipped" -ForegroundColor Yellow - continue - } - - # -------- LOCAL URL VALIDATION -------- - if ($url -notmatch '^[a-zA-Z]+://') { - Write-Host "❌ ERROR:" $url "- Missing protocol (http/https)" -ForegroundColor yellow - continue - } - - if ($url -notmatch '^https?://') { - Write-Host "❌ ERROR:" $url "- Unsupported protocol" -ForegroundColor yellow - continue - } - - if ($url -match 'https?:/[^/]') { - Write-Host "❌ ERROR:" $url "- Malformed URL (missing /)" -ForegroundColor yellow - continue - } - - if ($url -match '\.\.') { - Write-Host "❌ ERROR:" $url "- Invalid domain format" -ForegroundColor yellow - continue - } - - # -------- API REQUEST -------- - $body = @{ url = $url } | ConvertTo-Json - - try { - $response = Invoke-RestMethod ` - -Uri $apiUrl ` - -Method POST ` - -ContentType "application/json" ` - -Body $body - - Write-Host "✅ SUCCESS:" $url "→" $response.short_code -ForegroundColor Green - } - catch { - Write-Host "❌ ERROR:" $url "- Rejected by API" -ForegroundColor Yellow - } -} - -Write-Host "`n🎉 Done!" -ForegroundColor Cyan From 2669d81b8fe5b8bbd98418c6972d561f6a969858 Mon Sep 17 00:00:00 2001 From: RecursiveZero Date: Tue, 17 Feb 2026 13:44:33 +0530 Subject: [PATCH 02/34] update theme based UI --- app/static/css/tiny.css | 343 ++++++++++++++++++++++++++++---------- app/templates/header.html | 11 ++ app/templates/index.html | 71 ++++---- app/templates/layout.html | 4 +- 4 files changed, 293 insertions(+), 136 deletions(-) create mode 100644 app/templates/header.html diff --git a/app/static/css/tiny.css b/app/static/css/tiny.css index db092a6..873f075 100644 --- a/app/static/css/tiny.css +++ b/app/static/css/tiny.css @@ -16,64 +16,158 @@ body { background-image: radial-gradient(circle at 50% -20%, #1e1e2e 0%, transparent 50%); } +body { + background: var(--bg); + color: var(--text-primary); + font-family: "Inter", system-ui, sans-serif; + margin: 0; + overflow-x: hidden; + background-image: radial-gradient(circle at 50% -20%, #1e1e2e 0%, transparent 50%); +} + +/* Light theme overrides */ +body.light-theme { + --bg: #f9fafb; + --glass: rgba(0, 0, 0, 0.03); + --glass-border: rgba(0, 0, 0, 0.07); + --accent: #2563eb; + --text-primary: #111827; + --text-secondary: #4b5563; + + /* Remove or soften the dark 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: 6rem 1.5rem; + padding: 4rem 1rem; display: flex; flex-direction: column; - align-items: center; gap: 2rem; - min-height: 52vh; } -/* --- LARGE HERO INPUT --- */ +.site-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + background: var(--glass); + border-bottom: 1px solid var(--glass-border); + backdrop-filter: blur(10px); +} + +.header-left, +.header-right { + display: flex; + gap: 1rem; + align-items: center; +} + +.header-center { + flex: 1; + text-align: center; +} + +.logo { + margin: 0; + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); +} + +.icon-btn { + background: none; + border: none; + color: var(--text-primary); + font-size: 1.2rem; + cursor: pointer; + transition: color 0.3s; +} + +.icon-btn:hover { + color: var(--accent); +} + +@media (max-width: 600px) { + .logo { + font-size: 1.2rem; + } +} + +/* Hero input */ .hero-input-card { width: 100%; background: var(--glass); backdrop-filter: blur(20px); border: 1px solid var(--glass-border); - border-radius: 2.5rem; - padding: 3rem; - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); + border-radius: 2rem; + padding: 2rem; } .hero-input-card h1 { - font-size: 3.5rem; + font-size: 2.5rem; font-weight: 800; - letter-spacing: -3px; - margin: 0 0 1.5rem 0; text-align: center; - background: linear-gradient(to right, #fff, #666); + margin-bottom: 1.5rem; + background: linear-gradient(to right, var(--text-primary), var(--text-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .input-wrapper { - position: relative; display: flex; gap: 1rem; + flex-wrap: wrap; } .input-wrapper input { flex: 1; + min-width: 200px; background: rgba(255, 255, 255, 0.05); border: 1px solid var(--glass-border); border-radius: 1.5rem; - padding: 1.5rem 2rem; - color: white; - font-size: 1.2rem; - outline: none; - transition: all 0.3s ease; + padding: 1rem; + font-size: 1rem; + color: var(--text-primary); } -.input-wrapper input:focus { - border-color: var(--accent); - background: rgba(255, 255, 255, 0.08); - box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); +.btn-primary { + padding: 0.8rem 2rem; + border-radius: 1.5rem; + background: var(--accent); + color: #fff; + font-weight: bold; + border: none; + cursor: pointer; + transition: background 0.3s; +} + +.btn-primary:hover { + background: #4f46e5; +} + +.options-row { + margin-top: 1rem; + display: flex; + flex-wrap: wrap; + gap: 1rem; + font-size: 0.85rem; + color: var(--text-secondary); +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; } -/* --- OUTPUT CARD (Immediate Result) --- */ +/* Result card */ .result-card { width: 100%; background: linear-gradient(145deg, rgba(99, 102, 241, 0.1), rgba(0, 0, 0, 0)); @@ -81,30 +175,89 @@ body { border-radius: 2rem; padding: 2rem; display: flex; - align-items: center; justify-content: space-between; - animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1); + align-items: center; + gap: 2rem; +} + +.result-content { + display: flex; + align-items: center; + gap: 1.5rem; } -/* --- SCROLLABLE RECENT TRAY --- */ +.qr-image { + width: 80px; + height: 80px; + border-radius: 1rem; + background: #fff; + padding: 0.5rem; +} + +.ready-label { + font-size: 0.8rem; + text-transform: uppercase; + color: var(--accent); + font-weight: 800; +} + +.short-url a { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + text-decoration: none; +} + +.result-actions { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.5rem; +} + +.btn-copy { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--glass-border); + color: var(--text-primary); + padding: 0.8rem 1.5rem; + border-radius: 1rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-copy:hover { + border-color: var(--accent); + color: var(--accent); +} + +.download-link { + font-size: 0.75rem; + color: var(--text-secondary); + text-decoration: none; +} + +.download-link:hover { + color: var(--accent); +} + +/* Recent tray */ .recent-tray { width: 100%; - margin-top: 4rem; + margin-top: 2rem; } .recent-header { display: flex; justify-content: space-between; margin-bottom: 1rem; - padding: 0 1rem; + padding: 0 0.5rem; } .scroll-container { display: flex; gap: 1rem; overflow-x: auto; - padding: 1rem; - mask-image: linear-gradient(to right, black 85%, transparent); + padding: 1rem 0; } .scroll-container::-webkit-scrollbar { @@ -112,51 +265,53 @@ body { } .recent-item { - min-width: 250px; + min-width: 220px; background: var(--glass); border: 1px solid var(--glass-border); - padding: 1.5rem; - border-radius: 1.5rem; - font-size: 0.9rem; + padding: 1rem; + border-radius: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + flex-shrink: 0; } -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px); - } +.short-code { + color: var(--accent); + font-weight: bold; +} - to { - opacity: 1; - transform: translateY(0); - } +.original-url { + color: var(--text-secondary); + font-size: 0.8rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -/* --- LARGE FOOTER --- */ +/* Footer */ .big-footer { background: rgba(255, 255, 255, 0.01); - border-top: 1px solid var(--border); - padding: 6rem 0 3rem 0; - margin-top: 8rem; + border-top: 1px solid var(--glass-border); + padding: 4rem 1rem 2rem; + margin-top: 4rem; } .footer-grid { max-width: 1200px; margin: 0 auto; - padding: 0 1.5rem; display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; - gap: 4rem; + gap: 2rem; } .footer-brand h3 { font-size: 1.5rem; - margin: 0 0 1rem 0; - letter-spacing: -1px; + margin-bottom: 1rem; } .footer-brand p { - color: var(--text-dim); + color: var(--text-secondary); line-height: 1.6; max-width: 320px; } @@ -165,8 +320,8 @@ body { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.1em; - margin-bottom: 1.5rem; - color: var(--text); + margin-bottom: 1rem; + color: var(--text-primary); } .footer-col ul { @@ -180,7 +335,7 @@ body { } .footer-col ul li a { - color: var(--text-dim); + color: var(--text-secondary); text-decoration: none; font-size: 0.9rem; transition: color 0.2s; @@ -192,65 +347,69 @@ body { .footer-bottom { max-width: 1200px; - margin: 4rem auto 0; - padding: 2rem 1.5rem 0; - border-top: 1px solid var(--border); + 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-dim); + 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; } } -/* The container for the icon/button */ -.copy-btn { - position: relative; - cursor: pointer; - transition: transform 0.1s ease; -} +@media (max-width: 700px) { + .result-card { + flex-direction: column; + align-items: flex-start; + } -.copy-btn:active { - transform: scale(0.9); -} + .result-actions { + align-items: flex-start; + } -/* The "Smart" Tooltip */ -.copy-btn.copy-success::after { - content: attr(data-tooltip); - position: absolute; - top: -30px; - left: 50%; - transform: translateX(-50%); - background: #333; - color: #fff; - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - white-space: nowrap; - animation: fadeInOut 2s forwards; + .recent-item { + min-width: 180px; + } } -@keyframes fadeInOut { - 0% { - opacity: 0; - transform: translate(-50%, 5px); +@media (max-width: 600px) { + .hero-input-card h1 { + font-size: 2rem; } - 15% { - opacity: 1; - transform: translate(-50%, 0); + .short-url a { + font-size: 1.2rem; } - 85% { - opacity: 1; + .footer-grid { + grid-template-columns: 1fr; } - 100% { - opacity: 0; + .footer-bottom { + flex-direction: column; + gap: 1rem; + text-align: center; } } diff --git a/app/templates/header.html b/app/templates/header.html new file mode 100644 index 0000000..c7b19b1 --- /dev/null +++ b/app/templates/header.html @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index eeadff1..7a5ecd4 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -8,47 +8,34 @@

tiny URL

- +
-
-
{% if new_short_url %}
-
+
{% if qr_image %} - + {% endif %}
- Ready -
-
- - +
+ {% if qr_image %} - - Download QR - + Download QR {% endif %}
@@ -56,16 +43,14 @@

tiny URL

-

Recently Shortened

- View History → +

Recently Shortened

+ View History →
{% for url in urls %}
-
/{{ url.short_code }}
-
- {{ url.original_url }}
+
/{{ url.short_code }}
+
{{ url.original_url }}
{% endfor %}
@@ -110,38 +95,38 @@

Legal

@@ -68,6 +70,7 @@

🔗 tiny URL