notamitgamer

It's my profile man, there is nothing to see.

git clone https://github.com/notamitgamer/notamitgamer.git
ยท rebuilt hourlyOpen on GitHub
commit 062f2690677442dda634f2d1b0bbe8687d507a62
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date:   Wed,  7 Oct 2026 03:56:32 +0000

chore: auto-update maintainer activity

Diffstat:
A.github/CODEOWNERS | 1+
A.github/scripts/update_commits.py | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.github/scripts/update_maintainers.py | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.github/workflows/job.yml | 37+++++++++++++++++++++++++++++++++++++
A.github/workflows/update_readme.yml | 33+++++++++++++++++++++++++++++++++
AREADME.md | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aa.cpp | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abanner-m.png | 0
Adiscord | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agiphy-unscreen.gif | 0
Agiphy-unscreen2.gif | 0
Amaintainers.json | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackage.json | 21+++++++++++++++++++++
Aserver.js | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astyle.css | 408+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
15 files changed, 1461 insertions(+), 0 deletions(-)

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @notamitgamer diff --git a/.github/scripts/update_commits.py b/.github/scripts/update_commits.py @@ -0,0 +1,91 @@ +import urllib.request +import json +import re +import os +from datetime import datetime + +USERNAME = "notamitgamer" + +def fetch_recent_commits(): + commits_data = [] + try: + search_url = f"https://api.github.com/search/commits?q=author:{USERNAME}&sort=author-date&order=desc&per_page=15" + req = urllib.request.Request(search_url) + req.add_header('User-Agent', f'{USERNAME}-readme-updater') + req.add_header('Accept', 'application/vnd.github+json') + + # Use GITHUB_TOKEN if available to avoid rate limits + token = os.environ.get('GITHUB_TOKEN') + if token: + req.add_header('Authorization', f'token {token}') + + with urllib.request.urlopen(req) as response: + data = json.loads(response.read().decode()) + + items = data.get('items', []) + print(f"๐Ÿ” Search API returned {len(items)} commits.") + + for item in items: + date_str = item['commit']['author']['date'] + date_obj = datetime.fromisoformat(date_str) + + msg = item['commit']['message'].split('\n')[0] + commit_url = item['html_url'] + + # Extract repo name from the commit URL + # e.g. https://github.com/owner/repo/commit/sha + parts = commit_url.split('/') + repo_name = f"{parts[3]}/{parts[4]}" + repo_url = f"https://github.com/{repo_name}" + + commits_data.append({ + 'date': date_obj, + 'markdown': f"- [{msg}]({commit_url}) in [{repo_name}]({repo_url})" + }) + + # Sort by date descending + commits_data.sort(key=lambda x: x['date'], reverse=True) + + # Deduplicate (search can return same commit across multiple branches) + seen = set() + unique_commits = [] + for c in commits_data: + if c['markdown'] not in seen: + seen.add(c['markdown']) + unique_commits.append(c) + + final_commits = [c['markdown'] for c in unique_commits[:5]] + print(f"โœ… Found {len(final_commits)} recent commits.") + return final_commits + + except Exception as e: + print(f"Error fetching data: {e}") + return [] + + +def update_readme(commits): + if not commits: + print("No commits found to update.") + return + + commit_list_md = "\n".join(commits) + + with open('README.md', 'r', encoding='utf-8') as f: + readme = f.read() + + updated_readme = re.sub( + r"<!-- START_RECENT_COMMITS -->.*?<!-- END_RECENT_COMMITS -->", + f"<!-- START_RECENT_COMMITS -->\n{commit_list_md}\n<!-- END_RECENT_COMMITS -->", + readme, + flags=re.DOTALL + ) + + with open('README.md', 'w', encoding='utf-8') as f: + f.write(updated_readme) + + print("โœ… README.md successfully updated!") + + +if __name__ == "__main__": + recent_commits = fetch_recent_commits() + update_readme(recent_commits) diff --git a/.github/scripts/update_maintainers.py b/.github/scripts/update_maintainers.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 + +import json +import os +import time +from datetime import datetime, timezone, timedelta + +import requests + +# โ”€โ”€ Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +REPO = "is-a-dev/register" +CACHE_FILE = "pr_cache.json" +OUTPUT_FILE = "maintainers.json" + +MAINTAINERS = [ + "Yunexiz", "wdhdev", "STICKnoLOGIC", "Stef-00012", + "satr14washere", "orangci", "omsenjalia", "notamitgamer", + "iostpa", "dragsbruh", "DEV-DIBSTER", +] + +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "") +HEADERS = { + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "Authorization": f"Bearer {GITHUB_TOKEN}", +} + +# โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def now_utc() -> datetime: + return datetime.now(timezone.utc) + + +def next_update_utc(now: datetime) -> datetime: + """ + Cron is '0 */1 * * *' โ€” fires at the top of every hour. + Next update = start of the next hour. + """ + next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + return next_hour + + +def fetch_contributor_stats() -> list: + """ + Fetch /stats/contributors. GitHub computes this asynchronously โ€” + if it returns 202, we wait and retry until we get 200. + """ + url = f"https://api.github.com/repos/{REPO}/stats/contributors" + for attempt in range(10): + r = requests.get(url, headers=HEADERS, timeout=30) + if r.status_code == 202: + print(f" GitHub is computing stats, waiting 5s โ€ฆ (attempt {attempt+1})", flush=True) + time.sleep(5) + continue + if r.status_code == 204: + print(" No stats available yet.", flush=True) + return [] + r.raise_for_status() + return r.json() + raise RuntimeError("GitHub stats endpoint kept returning 202 after 10 attempts") + + +def week_to_month_key(ts: int) -> str: + """Convert a Unix week timestamp to YYYY-MM key of the month it falls in.""" + dt = datetime.fromtimestamp(ts, tz=timezone.utc) + return f"{dt.year}-{dt.month:02d}" + + +def this_month_key() -> str: + now = now_utc() + return f"{now.year}-{now.month:02d}" + + +def last_month_key() -> str: + now = now_utc() + y, m = now.year, now.month + if m == 1: + y, m = y - 1, 12 + else: + m -= 1 + return f"{y}-{m:02d}" + +# โ”€โ”€ Cache โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def load_cache() -> dict: + if os.path.exists(CACHE_FILE): + with open(CACHE_FILE) as f: + return json.load(f) + return {} + + +def save_cache(cache: dict): + with open(CACHE_FILE, "w") as f: + json.dump(cache, f, indent=2) + +# โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +def main(): + if not GITHUB_TOKEN: + raise RuntimeError("GITHUB_TOKEN not set") + + cache = load_cache() + print("Fetching contributor stats (single API call) โ€ฆ", flush=True) + stats = fetch_contributor_stats() + + if not stats: + print("No stats returned, aborting.", flush=True) + return + + maintainers_lower = {m.lower(): m for m in MAINTAINERS} + this_key = this_month_key() + last_key = last_month_key() + + # Build monthly totals per maintainer from the weekly data + # weekly_data: { "username": { "YYYY-MM": total_commits } } + weekly_data = {} + for contributor in stats: + login = (contributor.get("author") or {}).get("login", "") + if not login or login.lower() not in maintainers_lower: + continue + + canonical = maintainers_lower[login.lower()] + monthly = {} + for week in contributor.get("weeks", []): + commits = week.get("c", 0) + if commits == 0: + continue + mk = week_to_month_key(week["w"]) + monthly[mk] = monthly.get(mk, 0) + commits + + weekly_data[canonical] = monthly + print(f" {canonical}: {sum(monthly.values())} total commits across {len(monthly)} active months", flush=True) + + # Merge into cache โ€” update only months that changed + for username, monthly in weekly_data.items(): + user_cache = cache.get(username, {}) + for mk, count in monthly.items(): + user_cache[mk] = count # overwrite (stats endpoint is authoritative) + cache[username] = user_cache + + save_cache(cache) + print(f"\nโœ“ Cache saved to {CACHE_FILE}", flush=True) + + # Build output + now = now_utc() + maintainers_output = [] + for username in MAINTAINERS: + user_cache = cache.get(username, {}) + all_time = sum(user_cache.values()) + this_month = user_cache.get(this_key, 0) + last_month = user_cache.get(last_key, 0) + + print(f" {username}: all={all_time} this_month={this_month} last_month={last_month}", flush=True) + maintainers_output.append({ + "username": username, + "stats": { + "all_time": all_time, + "this_month": this_month, + "last_month": last_month, + }, + }) + + output = { + "meta": { + "last_updated": now.isoformat(), + "next_update": next_update_utc(now).isoformat(), + }, + "maintainers": maintainers_output, + } + + with open(OUTPUT_FILE, "w") as f: + json.dump(output, f, indent=2) + + print(f"โœ“ Written to {OUTPUT_FILE}", flush=True) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/job.yml b/.github/workflows/job.yml @@ -0,0 +1,37 @@ +name: Auto-Update Maintainer Activity + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + update-maintainers: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install Dependencies + run: pip install requests + + - name: Run Maintainer Activity Script + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python .github/scripts/update_maintainers.py + + - name: Commit and Push maintainers.json + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add maintainers.json + git diff --quiet && git diff --staged --quiet || \ + (git commit -m "chore: auto-update maintainer activity" && git push) diff --git a/.github/workflows/update_readme.yml b/.github/workflows/update_readme.yml @@ -0,0 +1,33 @@ +name: Auto-Update Recent Commits + +on: + schedule: + - cron: '0 */1 * * *' # every hour + workflow_dispatch: + +jobs: + update-readme: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Run Python Script to update README + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python .github/scripts/update_commits.py + + - name: Commit and Push Changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add README.md + git diff --quiet && git diff --staged --quiet || (git commit -m "docs: auto-update recent commits" && git push) diff --git a/README.md b/README.md @@ -0,0 +1,92 @@ +<div align="center"> + +``` + โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— +โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ• +โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ +โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ +โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ•šโ•โ• โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ +โ•šโ•โ• โ•šโ•โ•โ•šโ•โ• โ•šโ•โ•โ•šโ•โ• โ•šโ•โ• +``` + +**CS Undergrad ยท Full Stack Developer (Maybe)**<br> +**Reviewer at <a href="https://github.com/is-a-dev">is-a.dev</a> and <a href="https://github.com/free-domains">free-domains</a> organization** โ€” *professional button-clicker* +<br><br>***Status: 404 - Girlfriend Not Found*** + +[![Website](https://img.shields.io/badge/amit.is--a.dev-000?style=flat-square&logo=google-chrome&logoColor=white)](https://amit.is-a.dev) +[![Email](https://img.shields.io/badge/Email-000?style=flat-square&logo=gmail&logoColor=white)](mailto:amitdutta4255@gmail.com) +[![Email](https://img.shields.io/badge/Email-000?style=flat-square&logo=gmail&logoColor=white)](mailto:mail@amit.is-a.dev) +[![LinkedIn](https://img.shields.io/badge/LinkedIn-000?style=flat-square&logo=linkedin&logoColor=white)](https://linkedin.com/in/notamitgamer) +[![X](https://img.shields.io/badge/X-000?style=flat-square&logo=x&logoColor=white)](https://x.com/notamitgamer) +[![Instagram](https://img.shields.io/badge/Instagram-000?style=flat-square&logo=instagram&logoColor=white)](https://instagram.com/notamitgamer) +[![ORCID](https://img.shields.io/badge/ORCID-000?style=flat-square&logo=orcid&logoColor=white)](https://orcid.org/0009-0006-8834-9881) +[![Crunchbase](https://img.shields.io/badge/Crunchbase-000?style=flat-square&logo=crunchbase&logoColor=white)](https://www.crunchbase.com/person/amit-dutta-d32d) +<br><br> +[![Discord Presence](https://lanyard.cnrad.dev/api/1274721760088166495?bg=00000000&idleMessage=Eating%20Burgers&animated=true&animatedDecoration=true&showDisplayName=true&hideActivity=true)](https://discord.com/users/1274721760088166495) + +</div> + +- **visit [git.amit.is-a.dev](https://git.amit.is-a.dev) instead of surfing through github, because it's better and faster.** +- **also visit [forgejo.amit.is-a.dev](https://forgejo.amit.is-a.dev) to visit my self-hosted git.** +--- + +### About + +B.Sc. Hons Computer Science student at **Acharya Prafulla Chandra College, Kolkata**, currently in semester 2 and still googling things I "*learned*" in semester 1. I build tools, libraries, and web utilities โ€” mostly things I wish existed, occasionally things nobody asked for. + +- 1st year ยท Semester 2 &nbsp;ยท&nbsp; *professional deadline-approacher* +- Currently working on โ€” **Sem 2 Prep** (a.k.a. panic) +- Freelance โ€” **Unavailable** *(see: Sem 2 Prep)* +- Status โ€” probably debugging something that worked five minutes ago +- [My website โ†—](https://amit.is-a.dev) ยท [More about me โ†—](https://amit.is-a.dev/about) ยท [Google Knowledge Graph โ†—](https://www.google.com/search?kgmid=/g/11yzcqs5ff) + +--- + +### Stack + +* **Primary:** `Python` โ€” my one true love (when coding, obviously), no jealousy issues +* **Web & Systems:** `HTML5`, `CSS3`, `JavaScript`, `Java`, `C`, `C++`, `Kotlin`, `Google Apps Script` โ€” mostly friendly, occasionally at war +* **Services:** `Firebase`, `Firestore`, `Gemini API`, `Render` โ€” the free-tier survival kit +* **Tooling:** `Git` โ€” my time machine and my regret log + +--- + +### Projects + +- [WhatsApp Logger โ†—](https://amit.is-a.dev/logger) ยท [GitHub โ†—](https://github.com/notamitgamer/WhatsApp-Logger-Self-Hosted-) โ€” Self-hosted archiver. Not spying, just... very thorough remembering. + +- [Veyrix IDE โ†—](https://veyrix.amit.is-a.dev) ยท [Docs โ†—](https://veyrix.amit.is-a.dev/docs) โ€” Lightweight, browser-based code editor. Built after one too many "*I forgot to bring the assignment*" moments โ€” write the code, download it in any extension, print, done. + +- [Githrun โ†—](https://amit.is-a.dev/githrun) ยท [PyPI โ†—](https://pypi.org/project/githrun) โ€” Run GitHub scripts without cloning, for people (me) too impatient to `git clone`. + +- [BSC Repository โ†—](https://code.amit.is-a.dev) ยท [GitHub โ†—](https://github.com/notamitgamer/bsc) โ€” Every B.Sc. practical I've ever suffered through, preserved for posterity. + +--- + +### Recent Commits +*(Or: what I was procrastinating on instead of Sem 2 Prep)* +<!-- START_RECENT_COMMITS --> +- [Merge pull request #31 from YOCRRZ224/patch-1](https://github.com/notamitgamer/cdnV2/commit/e93ad33e1df77d90d69ecbad41d345d8148b4bc1) in [notamitgamer/cdnV2](https://github.com/notamitgamer/cdnV2) +- [Merge pull request #29 from notamitgamer/migration](https://github.com/notamitgamer/cdnV2/commit/f5afc3459d8f26fe2a23df9421fb1229f25af35a) in [notamitgamer/cdnV2](https://github.com/notamitgamer/cdnV2) +- [Update upload guard to use bucket URL and auth headers](https://github.com/notamitgamer/cdnV2/commit/62321f14a3aade5cb5fbb73a417b753296b9a918) in [notamitgamer/cdnV2](https://github.com/notamitgamer/cdnV2) +- [Refactor URL shortening to use new storage methods](https://github.com/notamitgamer/cdnV2/commit/030954f698e256154339e2c4629261103cc06aa5) in [notamitgamer/cdnV2](https://github.com/notamitgamer/cdnV2) +- [Refactor Hugging Face URL handling and auth headers](https://github.com/notamitgamer/cdnV2/commit/7fa82dbd011a4379673c2795b4052239cd5945fd) in [notamitgamer/cdnV2](https://github.com/notamitgamer/cdnV2) +<!-- END_RECENT_COMMITS --> + +--- + +### GitHub Stats + +<div align="center"> + +![Streak](https://streak-stats.demolab.com/?user=notamitgamer&theme=ayu-mirage&hide_border=true&background=00000000) + +![Activity](https://github-readme-activity-graph.vercel.app/graph?username=notamitgamer&theme=tokyo-night&hide_border=true) + +</div> + +--- + +<div align="center"> + <img src="https://komarev.com/ghpvc/?username=notamitgamer&style=for-the-badge" alt="Profile Views"> +</div> diff --git a/a.cpp b/a.cpp @@ -0,0 +1,78 @@ +#include <iostream> +using namespace std; + +template<typename T> +class BinarySearch { +private: + T *arr; + int size; + +public: + BinarySearch() : arr(nullptr), size(0) {} + + ~BinarySearch() { + delete[] arr; + } + + T inputArray() { + cout << "How many elements do you want to enter: "; + cin >> size; + + arr = new T[size]; + + for (int i = 0; i < size; i++) { + cout << "Enter element for position " << i << ": "; + cin >> arr[i]; + } + + T target; + cout << "\nEnter the target element: "; + cin >> target; + + return target; + } + + void sortArray() { + for (int i = 0; i < size - 1; i++) { + for (int j = 0; j < size - i - 1; j++) { + if (arr[j] > arr[j + 1]) { + T temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + } + } + } + } + + int binarySearch(T target) { + int low = 0; + int high = size - 1; + + while (low <= high) { + int mid = low + ((high - low) / 2); + if (arr[mid] == target) { + return mid; + } else if (arr[mid] > target) { + high = mid - 1; + } else { + low = mid + 1; + } + } + return -1; + } +}; + +int main() { + BinarySearch<int> bs; + int target = bs.inputArray(); + bs.sortArray(); + int index = bs.binarySearch(target); + + if (index != -1) { + cout << "\nElement " << target << " is found at index " << index << "."; + } else { + cout << "\nElement " << target << " is not found."; + } + + return 0; +} +\ No newline at end of file diff --git a/banner-m.png b/banner-m.png Binary files differ. diff --git a/discord b/discord @@ -0,0 +1,166 @@ +{ + delete window.$; + let wpRequire = webpackChunkdiscord_app.push([[Symbol()], {}, r => r]); + webpackChunkdiscord_app.pop(); + + let ApplicationStreamingStore = Object.values(wpRequire.c).find(x => x?.exports?.A?.__proto__?.getStreamerActiveStreamMetadata).exports.A; + let RunningGameStore = Object.values(wpRequire.c).find(x => x?.exports?.Ay?.getRunningGames).exports.Ay; + let QuestsStore = Object.values(wpRequire.c).find(x => x?.exports?.A?.__proto__?.getQuest).exports.A; + let ChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.A?.__proto__?.getAllThreadsForParent).exports.A; + let GuildChannelStore = Object.values(wpRequire.c).find(x => x?.exports?.Ay?.getSFWDefaultChannel).exports.Ay; + let FluxDispatcher = Object.values(wpRequire.c).find(x => x?.exports?.h?.__proto__?.flushWaitQueue).exports.h; + let api = Object.values(wpRequire.c).find(x => x?.exports?.Bo?.get).exports.Bo; + + const supportedTasks = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY", "WATCH_VIDEO_ON_MOBILE"] + let quests = [...QuestsStore.quests.values()].filter(x => x.userStatus?.enrolledAt && !x.userStatus?.completedAt && new Date(x.config.expiresAt).getTime() > Date.now() && supportedTasks.find(y => Object.keys((x.config.taskConfig ?? x.config.taskConfigV2).tasks).includes(y))) + let isApp = typeof DiscordNative !== "undefined" + if(quests.length === 0) { + console.log("You don't have any uncompleted quests!") + } else { + let doJob = function() { + const quest = quests.pop() + if(!quest) return + + const pid = Math.floor(Math.random() * 30000) + 1000 + + const applicationId = quest.config.application.id + const applicationName = quest.config.application.name + const questName = quest.config.messages.questName + const taskConfig = quest.config.taskConfig ?? quest.config.taskConfigV2 + const taskName = supportedTasks.find(x => taskConfig.tasks[x] != null) + const secondsNeeded = taskConfig.tasks[taskName].target + let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0 + + if(taskName === "WATCH_VIDEO" || taskName === "WATCH_VIDEO_ON_MOBILE") { + const speed = 7 + const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime() + let completed = false + let fn = async () => { + while(true) { + const remaining = Math.min(speed, secondsNeeded - secondsDone) + await new Promise(resolve => setTimeout(resolve, remaining * 1000)) + + const timestamp = secondsDone + speed + const res = await api.post({url: `/quests/${quest.id}/video-progress`, body: {timestamp: Math.min(secondsNeeded, timestamp + Math.random())}}) + completed = res.body.completed_at != null + secondsDone = Math.min(secondsNeeded, timestamp) + + if(timestamp >= secondsNeeded) { + break + } + } + if(!completed) { + await api.post({url: `/quests/${quest.id}/video-progress`, body: {timestamp: secondsNeeded}}) + } + console.log("Quest completed!") + doJob() + } + fn() + console.log(`Spoofing video for ${questName}.`) + } else if(taskName === "PLAY_ON_DESKTOP") { + if(!isApp) { + console.log("This no longer works in browser for non-video quests. Use the discord desktop app to complete the", questName, "quest!") + } else { + api.get({url: `/applications/public?application_ids=${applicationId}`}).then(res => { + const appData = res.body[0] + const exeName = appData.executables?.find(x => x.os === "win32")?.name?.replace(">","") ?? appData.name.replace(/[\/\\:*?"<>|]/g, "") + + const fakeGame = { + cmdLine: `C:\\Program Files\\${appData.name}\\${exeName}`, + exeName, + exePath: `c:/program files/${appData.name.toLowerCase()}/${exeName}`, + hidden: false, + isLauncher: false, + id: applicationId, + name: appData.name, + pid: pid, + pidPath: [pid], + processName: appData.name, + start: Date.now(), + } + const realGames = RunningGameStore.getRunningGames() + const fakeGames = [fakeGame] + const realGetRunningGames = RunningGameStore.getRunningGames + const realGetGameForPID = RunningGameStore.getGameForPID + RunningGameStore.getRunningGames = () => fakeGames + RunningGameStore.getGameForPID = (pid) => fakeGames.find(x => x.pid === pid) + FluxDispatcher.dispatch({type: "RUNNING_GAMES_CHANGE", removed: realGames, added: [fakeGame], games: fakeGames}) + + let fn = data => { + let progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.PLAY_ON_DESKTOP.value) + console.log(`Quest progress: ${progress}/${secondsNeeded}`) + + if(progress >= secondsNeeded) { + console.log("Quest completed!") + + RunningGameStore.getRunningGames = realGetRunningGames + RunningGameStore.getGameForPID = realGetGameForPID + FluxDispatcher.dispatch({type: "RUNNING_GAMES_CHANGE", removed: [fakeGame], added: [], games: []}) + FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn) + + doJob() + } + } + FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn) + + console.log(`Spoofed your game to ${applicationName}. Wait for ${Math.ceil((secondsNeeded - secondsDone) / 60)} more minutes.`) + }) + } + } else if(taskName === "STREAM_ON_DESKTOP") { + if(!isApp) { + console.log("This no longer works in browser for non-video quests. Use the discord desktop app to complete the", questName, "quest!") + } else { + let realFunc = ApplicationStreamingStore.getStreamerActiveStreamMetadata + ApplicationStreamingStore.getStreamerActiveStreamMetadata = () => ({ + id: applicationId, + pid, + sourceName: null + }) + + let fn = data => { + let progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.STREAM_ON_DESKTOP.value) + console.log(`Quest progress: ${progress}/${secondsNeeded}`) + + if(progress >= secondsNeeded) { + console.log("Quest completed!") + + ApplicationStreamingStore.getStreamerActiveStreamMetadata = realFunc + FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn) + + doJob() + } + } + FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn) + + console.log(`Spoofed your stream to ${applicationName}. Stream any window in vc for ${Math.ceil((secondsNeeded - secondsDone) / 60)} more minutes.`) + console.log("Remember that you need at least 1 other person to be in the vc!") + } + } else if(taskName === "PLAY_ACTIVITY") { + const channelId = ChannelStore.getSortedPrivateChannels()[0]?.id ?? Object.values(GuildChannelStore.getAllGuilds()).find(x => x != null && x.VOCAL.length > 0).VOCAL[0].channel.id + const streamKey = `call:${channelId}:1` + + let fn = async () => { + console.log("Completing quest", questName, "-", quest.config.messages.questName) + + while(true) { + const res = await api.post({url: `/quests/${quest.id}/heartbeat`, body: {stream_key: streamKey, terminal: false}}) + const progress = res.body.progress.PLAY_ACTIVITY.value + console.log(`Quest progress: ${progress}/${secondsNeeded}`) + + await new Promise(resolve => setTimeout(resolve, 20 * 1000)) + + if(progress >= secondsNeeded) { + await api.post({url: `/quests/${quest.id}/heartbeat`, body: {stream_key: streamKey, terminal: true}}) + break + } + } + + console.log("Quest completed!") + doJob() + } + fn() + } + } + doJob() + } +} diff --git a/giphy-unscreen.gif b/giphy-unscreen.gif Binary files differ. diff --git a/giphy-unscreen2.gif b/giphy-unscreen2.gif Binary files differ. diff --git a/maintainers.json b/maintainers.json @@ -0,0 +1,96 @@ +{ + "meta": { + "last_updated": "2026-10-07T03:56:32.273086+00:00", + "next_update": "2026-10-07T04:00:00+00:00" + }, + "maintainers": [ + { + "username": "Yunexiz", + "stats": { + "all_time": 320, + "this_month": 0, + "last_month": 0 + } + }, + { + "username": "wdhdev", + "stats": { + "all_time": 2185, + "this_month": 0, + "last_month": 5 + } + }, + { + "username": "STICKnoLOGIC", + "stats": { + "all_time": 4249, + "this_month": 0, + "last_month": 0 + } + }, + { + "username": "Stef-00012", + "stats": { + "all_time": 0, + "this_month": 0, + "last_month": 0 + } + }, + { + "username": "satr14washere", + "stats": { + "all_time": 287, + "this_month": 0, + "last_month": 0 + } + }, + { + "username": "orangci", + "stats": { + "all_time": 2102, + "this_month": 1, + "last_month": 8 + } + }, + { + "username": "omsenjalia", + "stats": { + "all_time": 299, + "this_month": 0, + "last_month": 0 + } + }, + { + "username": "notamitgamer", + "stats": { + "all_time": 1196, + "this_month": 0, + "last_month": 85 + } + }, + { + "username": "iostpa", + "stats": { + "all_time": 4003, + "this_month": 3, + "last_month": 19 + } + }, + { + "username": "dragsbruh", + "stats": { + "all_time": 1299, + "this_month": 20, + "last_month": 2 + } + }, + { + "username": "DEV-DIBSTER", + "stats": { + "all_time": 5247, + "this_month": 0, + "last_month": 8 + } + } + ] +} +\ No newline at end of file diff --git a/package.json b/package.json @@ -0,0 +1,21 @@ +{ + "name": "email-router", + "version": "1.0.1", + "description": "Custom email routing webhook for Render", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.19.0", + "mongoose": "^8.3.0", + "resend": "^3.2.0", + "svix": "^1.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/server.js b/server.js @@ -0,0 +1,257 @@ +const express = require('express'); +const { Resend } = require('resend'); +const { Webhook } = require('svix'); + +const app = express(); +const port = process.env.PORT || 3000; + +const resend = new Resend(process.env.RESEND_API_KEY); +const webhookSecret = process.env.RESEND_WEBHOOK_SECRET; + +app.post('/api/incoming', express.text({ type: 'application/json' }), async (req, res) => { + try { + const rawBody = req.body; + const headers = { + 'svix-id': req.headers['svix-id'], + 'svix-timestamp': req.headers['svix-timestamp'], + 'svix-signature': req.headers['svix-signature'], + }; + + const wh = new Webhook(webhookSecret); + let event; + + try { + event = wh.verify(rawBody, headers); + } catch (err) { + console.error(err.message); + return res.status(400).json({ error: 'Invalid signature' }); + } + + if (event.type !== 'email.received') { + return res.status(200).json({ message: 'Not an email event' }); + } + + const emailData = event.data; + const originalSender = emailData.from; + const originalRecipient = emailData.to[0].toLowerCase(); + const subject = emailData.subject || 'No Subject'; + + const allowedString = process.env.ALLOWED_ALIASES || ''; + const allowedAliases = allowedString.split(',').map(alias => alias.trim().toLowerCase()); + + if (!allowedAliases.includes(originalRecipient)) { + return res.status(200).json({ success: true, message: 'Alias ignored' }); + } + + const personalGmail = process.env.PERSONAL_GMAIL; + const forwardingAddress = process.env.FORWARDING_BOT_ADDRESS; + + // Premium Adaptive Google-style HTML Template + const notificationHtml = ` +<!DOCTYPE html> +<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="color-scheme" content="light dark"> + <meta name="supported-color-schemes" content="light dark"> + <title>New Email Notification</title> + + <style> + @import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500&family=Roboto+Mono:wght@400;500&family=Roboto:wght@400;500&display=swap'); + + body { + margin: 0; + padding: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + .btn-pill:hover { + opacity: 0.9; + transform: translateY(-1px); + transition: all 0.2s ease; + } + + @media (prefers-color-scheme: dark) { + .bg-outer { background-color: #131314 !important; } + .bg-card { background-color: #1E1F20 !important; } + .bg-tint { background-color: #282A2C !important; } + .text-primary { color: #E3E3E3 !important; } + .text-secondary { color: #C4C7C5 !important; } + .text-accent { color: #A8C7FA !important; } + .border-card { border-color: #444746 !important; } + .divider { background-color: #444746 !important; } + .btn-pill { + background-color: #A8C7FA !important; + color: #000000 !important; + } + .icon-fill { fill: #8E918F !important; } + } + + /* Gmail Dark Mode Fixes */ + [data-ogsb] .bg-outer { background-color: #131314 !important; } + [data-ogsb] .bg-card { background-color: #1E1F20 !important; } + [data-ogsb] .bg-tint { background-color: #282A2C !important; } + [data-ogsc] .text-primary { color: #E3E3E3 !important; } + [data-ogsc] .text-secondary { color: #C4C7C5 !important; } + [data-ogsc] .text-accent { color: #A8C7FA !important; } + [data-ogsb] .border-card { border-color: #444746 !important; } + [data-ogsb] .divider { background-color: #444746 !important; } + [data-ogsb] .btn-pill { background-color: #A8C7FA !important; } + [data-ogsc] .btn-pill { color: #000000 !important; } + [data-ogsc] .icon-fill { fill: #8E918F !important; } + </style> +</head> + +<body class="bg-outer" style="margin: 0; padding: 0; background-color: #F8F9FA; font-family: 'Google Sans', Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased;"> + + <table width="100%" border="0" cellspacing="0" cellpadding="0" class="bg-outer" style="background-color: #F8F9FA; width: 100%; height: 100%;"> + <tr> + <td align="center" style="padding: 40px 16px;"> + + <table border="0" cellspacing="0" cellpadding="0" class="bg-card border-card" style="background-color: #FFFFFF; border-radius: 28px; max-width: 600px; width: 100%; overflow: hidden; border: 1px solid #C4C7C5;"> + + <tr> + <td style="padding: 40px 32px 16px 32px;"> + <h2 class="text-primary" style="margin: 0; font-family: 'Google Sans', Roboto, sans-serif; font-size: 22px; font-weight: 500; color: #1F1F1F; letter-spacing: -0.5px;">Incoming Message</h2> + <p class="text-secondary" style="margin: 8px 0 0 0; font-family: Roboto, Arial, sans-serif; font-size: 14px; font-weight: 400; color: #444746; line-height: 1.5;">A new email has been routed to your developer inbox.</p> + </td> + </tr> + + <tr> + <td style="padding: 0 32px;"> + <div class="divider" style="height: 1px; background-color: #E3E3E3; width: 100%; margin: 16px 0;"></div> + </td> + </tr> + + <tr> + <td style="padding: 16px 32px;"> + + <div style="margin-bottom: 24px;"> + <span class="text-accent" style="display: block; font-family: Roboto, Arial, sans-serif; font-size: 11px; font-weight: 500; color: #0B57D0; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px;">Sender Email ID</span> + <span class="text-primary" style="display: block; font-family: Roboto, Arial, sans-serif; font-size: 16px; font-weight: 400; color: #1F1F1F; word-break: break-all; line-height: 1.4;">${originalSender}</span> + </div> + + <div style="margin-bottom: 24px;"> + <span class="text-accent" style="display: block; font-family: Roboto, Arial, sans-serif; font-size: 11px; font-weight: 500; color: #0B57D0; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px;">Subject</span> + <span class="text-primary" style="display: block; font-family: 'Google Sans', Roboto, Arial, sans-serif; font-size: 18px; font-weight: 500; color: #1F1F1F; line-height: 1.4;">${subject}</span> + </div> + + <div style="margin-bottom: 32px;"> + <span class="text-accent" style="display: block; font-family: Roboto, Arial, sans-serif; font-size: 11px; font-weight: 500; color: #0B57D0; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px;">Email Tracking ID</span> + + <table border="0" cellspacing="0" cellpadding="0" style="width: 100%;"> + <tr> + <td class="bg-tint" style="background-color: #F0F4F9; border-radius: 8px; padding: 12px 16px;"> + <span class="text-secondary" style="display: block; font-family: 'Roboto Mono', monospace; font-size: 14px; font-weight: 500; color: #444746; word-break: break-all;"> + ${emailData.email_id || 'N/A'} + </span> + </td> + </tr> + </table> + </div> + + </td> + </tr> + + <tr> + <td style="padding: 8px 32px 40px 32px;"> + <table border="0" cellspacing="0" cellpadding="0"> + <tr> + <td align="center" style="border-radius: 100px;"> + <a href="https://resend.com/emails/${emailData.email_id}" target="_blank" class="btn-pill" style="display: inline-block; padding: 12px 32px; background-color: #0B57D0; font-family: 'Google Sans', Roboto, Arial, sans-serif; font-size: 14px; font-weight: 500; color: #FFFFFF; text-decoration: none; border-radius: 100px;"> + Open Resend Dashboard + </a> + </td> + </tr> + </table> + </td> + </tr> + + </table> + + <table border="0" cellspacing="0" cellpadding="0" style="max-width: 600px; width: 100%; margin-top: 32px;"> + <tr> + <td align="center" style="padding-bottom: 24px;"> + <p class="text-secondary" style="margin: 0; font-family: Roboto, Arial, sans-serif; font-size: 12px; color: #747775; line-height: 1.6;">Automated alert by Notifier Bot.<br>Please do not reply to this unmonitored email.</p> + </td> + </tr> + + <tr> + <td align="center" style="padding-bottom: 16px;"> + <table border="0" cellspacing="0" cellpadding="0"> + <tr> + <td style="padding: 0 12px;"> + <a href="https://amit.is-a.dev" target="_blank" style="text-decoration: none;" title="Website"> + <svg width="20" height="20" viewBox="0 0 24 24" fill="#747775" class="icon-fill" xmlns="http://www.w3.org/2000/svg" style="display: block; border: 0;"> + <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/> + </svg> + </a> + </td> + <td style="padding: 0 12px;"> + <a href="https://github.com/notamitgamer" target="_blank" style="text-decoration: none;" title="GitHub"> + <svg width="20" height="20" viewBox="0 0 16 16" fill="#747775" class="icon-fill" xmlns="http://www.w3.org/2000/svg" style="display: block; border: 0;"> + <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/> + </svg> + </a> + </td> + <td style="padding: 0 12px;"> + <a href="mailto:amitdutta4255@gmail.com" style="text-decoration: none;" title="Email"> + <svg width="20" height="20" viewBox="0 0 24 24" fill="#747775" class="icon-fill" xmlns="http://www.w3.org/2000/svg" style="display: block; border: 0;"> + <path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/> + </svg> + </a> + </td> + </tr> + </table> + </td> + </tr> + + <tr> + <td align="center"> + <p class="text-secondary" style="margin: 0; font-family: Roboto, Arial, sans-serif; font-size: 12px; color: #747775;"> + &copy; 2025-2026 Amit Dutta &bull; <a href="https://amit.is-a.dev" class="text-secondary" style="color: #747775; text-decoration: none;">amit.is-a.dev</a> + </p> + </td> + </tr> + </table> + + </td> + </tr> + </table> + +</body> +</html> + `; + + const payload = { + from: `Notifier Bot <${forwardingAddress}>`, + to: [personalGmail], + reply_to: originalSender, + subject: subject, + html: notificationHtml + }; + + const { data, error } = await resend.emails.send(payload); + + if (error) { + console.error(error); + return res.status(500).json({ error: error.message }); + } + + return res.status(200).json({ success: true, id: data?.id }); + + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +app.get('/ping', (req, res) => { + res.status(200).send('Server is awake!'); +}); + +app.listen(port, () => { + console.log(`Email router listening on port ${port}`); +}); diff --git a/style.css b/style.css @@ -0,0 +1,407 @@ +/* ===== Archie-style theme, no Hugo ===== */ + +/* Light theme (default) */ +:root { + --primary: #dc143c; + --primary-hover: #b91c3c; + --on-primary: #ffffff; + --border: #663399; + --text: #232333; + --heading: #232333; + --muted: #737373; + --meta: #767676; + --bg: #ffffff; + --code-bg: #f1f1f1; + --pre-bg: #ececec; + + --content-max-width: 800px; + --content-padding: 1ch; + --radius: 5px; +} + +/* Dark theme: follows the system unless the user chose light */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --primary: #50fa7b; + --primary-hover: #50fa7b; + --on-primary: #000000; + --border: #0066ff; + --text: #ffffff; + --heading: #dddddd; + --muted: #dddddd; + --meta: #b5b5b5; + --bg: #202124; + --code-bg: #3a3a3a; + --pre-bg: #272822; + } +} + +/* Dark theme: chosen manually with the toggle */ +:root[data-theme="dark"] { + --primary: #50fa7b; + --primary-hover: #50fa7b; + --on-primary: #000000; + --border: #0066ff; + --text: #ffffff; + --heading: #dddddd; + --muted: #dddddd; + --meta: #b5b5b5; + --bg: #202124; + --code-bg: #3a3a3a; + --pre-bg: #272822; +} + +/* ===== Reset / base ===== */ +*, *::before, *::after { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } + +html { + color: var(--text); + font-family: 'Roboto Mono', 'Courier New', monospace; + font-size: 15px; + line-height: 1.6; + scroll-behavior: smooth; +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } +} + +body { + margin: 8px; + background-color: var(--bg); + color: var(--text); + transition: background-color 0.2s ease, color 0.2s ease; +} + +::selection { background: var(--primary); color: var(--on-primary); } + +p { + font-family: 'Fira Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + line-height: 1.5; + margin-block: 0.5em; +} + +hr { + border: 0; + border-block-start: 3px dotted var(--border); + margin-block: 1em; +} + +blockquote { + border-inline-start: 3px solid var(--border); + padding-inline-start: 1em; + color: var(--muted); + margin: 0; + font-style: italic; +} + +/* ===== Links (thick underline, fill on hover) ===== */ +a { + border-block-end: 3px solid var(--primary); + color: inherit; + text-decoration: none; + transition: background-color 0.2s ease, color 0.2s ease; +} + +a:hover, +a:focus { + background-color: var(--primary-hover); + color: var(--on-primary); + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* small arrow on external links */ +a[target="_blank"]::after { + content: "โ†—"; + display: inline-block; + margin-left: 0.2em; + font-size: 0.8em; +} +/* but not on image/icon-only links or the site name */ +header .main a::after, +.no-arrow::after, +.readmore::after { content: none !important; } + +/* ===== Lists: "* " markers ===== */ +ul { + list-style: none; + padding-inline-start: 2ch; +} +ul li { text-indent: -2ch; } +ul > li::before { + content: '* '; + font-weight: bold; + color: var(--primary); +} + +/* ===== Images ===== */ +img { + border: 3px solid var(--pre-bg); + max-inline-size: 100%; + block-size: auto; + border-radius: var(--radius); +} + +/* ===== Code ===== */ +code { + background-color: var(--code-bg); + padding: 0.1em 0.2em; + border-radius: 3px; + font-size: 0.9em; +} +pre { + background-color: var(--pre-bg); + line-height: 1.4; + overflow-x: auto; + padding: 1em; + border-radius: var(--radius); + border: 1px solid var(--border); +} +pre code { background: transparent; padding: 0; font-size: 100%; } + +/* ===== Layout ===== */ +.content { + max-inline-size: var(--content-max-width); + margin-inline: auto; + margin-block-end: 4em; + padding-inline: var(--content-padding); + overflow-wrap: break-word; +} + +/* ===== Header ===== */ +header { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + margin-block: 1em; + line-height: 2.5em; + gap: 1rem; +} + +header .main { + font-size: 1.5rem; + font-weight: bold; + display: flex; + align-items: center; + gap: 0.75rem; +} + +header .main img.avatar { + width: 48px; + height: 48px; + object-fit: cover; + border-radius: 50%; +} + +nav { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +/* theme toggle (icon button) */ +.nav-toggle { + appearance: none; + -webkit-appearance: none; + border: none; + background: none; + margin: 0; + font: inherit; + cursor: pointer; + padding: 0.5rem; + border-radius: 4px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text); + transition: all 0.2s ease; +} +.nav-toggle:hover, +.nav-toggle:focus { + background-color: var(--primary); + color: var(--on-primary); + outline: 2px solid var(--primary); + outline-offset: 2px; +} +.nav-toggle:active { transform: scale(0.95); } + +.feather { + width: 1.5rem; + height: 1.5rem; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + fill: none; + display: inline-block; + vertical-align: middle; +} + +/* ===== Headings: markdown-style prefixes ===== */ +h1, h2, h3, h4, h5, h6 { + font-size: 1.2rem; + margin-block-start: 2em; + margin-block-end: 0.5em; + line-height: 1.2; + color: var(--heading); +} +h1::before { color: var(--primary); content: '# '; } +h2::before { color: var(--primary); content: '## '; } +h3::before { color: var(--primary); content: '### '; } +h4::before { color: var(--primary); content: '#### '; } + +/* ===== Home-page pieces ===== */ +.site-description { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; +} + +.list-item { margin-block-end: 1.5em; } +.list-item h3 { margin-block-start: 1.25em; margin-block-end: 0.25em; } + +.meta, time { + color: var(--meta); + font-size: 0.9em; + letter-spacing: -0.5px; +} + +.readmore { display: inline-block; margin-block-start: 0.25em; } + +/* tags (used for stack + project tech) */ +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + list-style: none; + padding: 0; + margin-block: 0.5rem; +} +.tags li { text-indent: 0; } +.tags li::before { content: none; } +.tag, +.tags a { + display: inline-block; + border-block-end: 3px solid var(--primary); + padding: 0.25rem 0.5rem; + border-radius: 3px; + font-size: 0.9em; + transition: all 0.2s ease; +} +/* non-link tags: no hover fill needed, but keep a subtle one */ +.tags a:hover, .tags a:focus { background-color: var(--primary-hover); color: var(--on-primary); } + +/* "Portfolio Graveyard" collapsible */ +details { + border: 1px dotted var(--border); + border-radius: var(--radius); + padding: 0.75rem 1rem; + margin-block: 1rem; +} +summary { + cursor: pointer; + font-weight: bold; + font-size: 1.2rem; + color: var(--heading); + list-style: none; +} +summary::-webkit-details-marker { display: none; } +summary::before { content: '## '; color: var(--primary); } +details[open] summary::after { content: ' โ–พ'; color: var(--primary); } +details:not([open]) summary::after { content: ' โ–ธ'; color: var(--primary); } + +/* contact / link rows */ +.link-row { display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem; margin-block: 0.75rem; } + +/* 88x31 web badges: wrap to fit the screen */ +.web-badges { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; + margin-block: 3rem 1.5rem; +} +.web-badges img { + width: 88px; + height: 31px; + border: 0; /* the border was shrinking the image and blurring it */ + outline: 1px solid var(--border); /* outline doesn't change the size */ + border-radius: 0; + image-rendering: pixelated; + transition: transform 150ms ease; +} +.web-badges img:hover { transform: scale(1.05); } + +.post-scroll { + margin-block-start: 1rem; + font-size: 0.8rem; + color: var(--meta); + text-align: center; +} + +/* ===== Footer ===== */ +footer { + display: flex; + align-items: center; + border-block-start: 0.4rem dotted var(--border); + padding-block: 2rem; + margin-block-start: 2rem; + gap: 0.5rem; + flex-wrap: wrap; +} +.soc { + display: inline-flex; + align-items: center; + border-block-end: none; + padding: 0 0.25rem; +} +.soc:hover, .soc:focus { color: var(--on-primary); } +.border { + display: inline-block; + margin-inline: 0.5rem; + border: 1px solid var(--border); + block-size: 1.2em; + padding: 0; +} +.footer-info { margin-inline-start: auto; font-size: 0.9em; } + +/* ===== Focus ring for keyboard users ===== */ +:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; } + +/* ===== Journal/component styles ===== */ +table { width:100%; border-collapse:collapse; margin-block:1rem; overflow:hidden; border-radius:var(--radius); } +th { padding:.75rem; border:1px solid var(--border); background:var(--pre-bg); text-align:start; font-weight:bold; } +td { padding:.75rem; border:1px solid var(--border); } +tr:nth-child(even) { background:var(--code-bg); } +.callout { background:var(--pre-bg); border:1px solid var(--border); border-radius:var(--radius); padding:1em; margin-block:1em; } +.table-wrapper { overflow-x: auto; } + +/* ===== Mobile ===== */ +@media (max-width: 37.4em) { + .content { padding-inline: 0.5rem; } + header { flex-direction: column; align-items: flex-start; gap: 0.5rem; } + .site-description { flex-direction: column; align-items: flex-start; } + .footer-info { margin-inline-start: 0; } +} + +/* non-clickable tag chips: label, not link */ +.tag { + border-block-end: 0; + background: var(--code-bg); + color: var(--muted); +} + +/* keep underline only on tags that are real links (Links section) */ +.tags a { border-block-end: 3px solid var(--primary); } + +/* slightly lighter underline on project titles */ +.list-item h3 a { border-block-end-width: 2px; } + +header .main img.avatar { border-width: 2px; } + +/* end */ +\ No newline at end of file