From 852397bb564cb843c524405175add6a67c5339cb Mon Sep 17 00:00:00 2001 From: Anurag Verma <78868769+anurag629@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:29:56 +0530 Subject: [PATCH 1/3] =?UTF-8?q?ci:=20enforce=20dev=20=E2=86=92=20main=20PR?= =?UTF-8?q?=20flow=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: improve README hero image, add contributors section (#1) Switch hero from blog-minimal-dark to github-readme-hero template with brand accent color. Add contrib.rocks contributors grid. Condense contributing section to avoid redundancy with CONTRIBUTING.md. * ci: add workflow to restrict PRs targeting main to dev branch only PRs targeting main from any branch other than dev will fail the PR Target Check. This enforces the flow: feature branches → dev → main. * ci: retrigger checks after base branch change * ci: run CI on PRs targeting dev branch too --- .github/workflows/ci.yml | 4 ++-- .github/workflows/pr-target-check.yml | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pr-target-check.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbf7594..17c905d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main] + branches: [main, dev] pull_request: - branches: [main] + branches: [main, dev] jobs: build: diff --git a/.github/workflows/pr-target-check.yml b/.github/workflows/pr-target-check.yml new file mode 100644 index 0000000..3ef2504 --- /dev/null +++ b/.github/workflows/pr-target-check.yml @@ -0,0 +1,22 @@ +name: PR Target Check + +on: + pull_request: + branches: [main] + +jobs: + check-source-branch: + runs-on: ubuntu-latest + steps: + - name: Only allow PRs from dev to main + if: github.head_ref != 'dev' + run: | + echo "::error::PRs targeting 'main' are only allowed from the 'dev' branch." + echo "Please target 'dev' instead, or merge your branch into 'dev' first." + echo "" + echo " Source: ${{ github.head_ref }}" + echo " Target: ${{ github.base_ref }}" + exit 1 + - name: PR source branch is valid + if: github.head_ref == 'dev' + run: echo "PR from 'dev' to 'main' — allowed." From 6b0373e3b50bebd742555b83037c435554b3f0f6 Mon Sep 17 00:00:00 2001 From: Anurag Verma <78868769+anurag629@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:16:42 +0530 Subject: [PATCH 2/3] feat: Raycast-inspired UI/UX redesign (#3) * feat: Raycast-inspired UI/UX redesign across all pages Redesign the entire site with a polished, premium feel inspired by Raycast: - Design system: enhanced shadows, gradients, glassmorphism, animations - Header: frosted glass, gradient border, nav hover pills - Homepage: hero glow, equal-width bento category grid, feature cards, vertical timeline, terminal chrome API teaser, OSS banner - Templates gallery: search bar, filter pills, card hover glow - Editor: dot-grid canvas, refined panels, gradient export button - API docs: sidebar icons, terminal chrome code blocks, copy buttons - 404 page: SVG illustration, floating shapes, gradient text - Footer: 3-column grid layout with gradient top border - Layout: Astro View Transitions for smooth page navigation - Fix thumbnail API rendering templates at 600x315 viewport (cropping) instead of 1200x630 with resvg scale-down (showing full template) * feat: add GitHub stars and visitor counter utilities Add supporting libs for GitHub star count, Upstash Redis visitor counter, and the /api/visitors endpoint. Include .env.example for required environment variables. --- .env.example | 4 + src/components/BackToTop.astro | 82 +++ src/components/Footer.astro | 171 +++++-- src/components/Header.astro | 123 ++++- src/components/StarBanner.astro | 205 ++++++++ src/components/Toast.astro | 83 ++++ src/components/editor/ExportBar.tsx | 91 +++- src/components/editor/TemplateThumbnail.tsx | 4 +- src/layouts/Layout.astro | 17 +- src/lib/github-stars.ts | 31 ++ src/lib/og-engine.ts | 7 +- src/lib/upstash.ts | 60 +++ src/pages/404.astro | 102 +++- src/pages/api-docs.astro | 108 +++- src/pages/api/templates/[id]/thumbnail.png.ts | 3 +- src/pages/api/visitors.ts | 26 + src/pages/index.astro | 466 ++++++++++++++---- src/pages/templates.astro | 225 +++++++-- src/styles/api-docs.css | 154 +++++- src/styles/editor.css | 65 ++- src/styles/global.css | 253 ++++++++-- 21 files changed, 1970 insertions(+), 310 deletions(-) create mode 100644 .env.example create mode 100644 src/components/BackToTop.astro create mode 100644 src/components/StarBanner.astro create mode 100644 src/components/Toast.astro create mode 100644 src/lib/github-stars.ts create mode 100644 src/lib/upstash.ts create mode 100644 src/pages/api/visitors.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..259f995 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Upstash Redis (visitor counter) +# Get these from https://console.upstash.com → Redis → your database → REST API +UPSTASH_REDIS_REST_URL=https://your-endpoint.upstash.io +UPSTASH_REDIS_REST_TOKEN=your-token-here diff --git a/src/components/BackToTop.astro b/src/components/BackToTop.astro new file mode 100644 index 0000000..c49130b --- /dev/null +++ b/src/components/BackToTop.astro @@ -0,0 +1,82 @@ + + + + + + diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 79f7740..8459d93 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -4,45 +4,78 @@ const year = new Date().getFullYear(); + + diff --git a/src/components/Header.astro b/src/components/Header.astro index dc0ff00..086b61a 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -1,5 +1,8 @@ --- +import { getStarCount, formatStarCount } from '../lib/github-stars'; + const currentPath = Astro.url.pathname; +const starCount = await getStarCount(); const navItems = [ { href: '/', label: 'Home' }, @@ -15,6 +18,7 @@ const navItems = [ @@ -40,10 +45,14 @@ const navItems = [
- {navItems.map((item) => ( - {item.label} + {navItems.map((item, i) => ( + {item.label} ))} - GitHub + + + Star on GitHub + {starCount !== null && {formatStarCount(starCount)}} +
@@ -62,10 +71,19 @@ const navItems = [ position: sticky; top: 0; z-index: 100; - background: var(--bg); - border-bottom: 1px solid var(--border); - backdrop-filter: blur(12px); - background: rgba(var(--bg), 0.8); + background: var(--bg-frosted); + backdrop-filter: saturate(180%) blur(20px); + -webkit-backdrop-filter: saturate(180%) blur(20px); + } + + .header::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent 0%, var(--border) 20%, var(--accent-primary-muted) 50%, var(--border) 80%, transparent 100%); } .header-nav { @@ -80,11 +98,12 @@ const navItems = [ flex-shrink: 0; display: flex; align-items: center; + gap: var(--space-sm); } .logo-img { display: block; - height: 26px; + height: 28px; width: auto; } @@ -93,10 +112,25 @@ const navItems = [ .logo-text { color: var(--text); } .logo-accent { color: var(--accent-primary); } + .oss-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.03em; + color: var(--accent-primary); + background: var(--accent-primary-subtle, rgba(224, 122, 95, 0.1)); + border: 1px solid rgba(224, 122, 95, 0.2); + border-radius: var(--radius-full, 999px); + white-space: nowrap; + line-height: 1.4; + } + .header-links { display: flex; align-items: center; - gap: var(--space-lg); + gap: 4px; margin-left: auto; } @@ -104,24 +138,49 @@ const navItems = [ font-size: var(--text-sm); font-weight: 500; color: var(--text-muted); - transition: color var(--transition); + padding: 6px 12px; + border-radius: var(--radius); + transition: color var(--transition), background-color var(--transition); white-space: nowrap; } - .nav-link:hover, .nav-link.active { + .nav-link:hover { + color: var(--text); + background-color: var(--bg-surface); + } + + .nav-link.active { color: var(--text); + background-color: var(--bg-surface); } .nav-github { - padding: var(--space-xs) var(--space-md); + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: 8px; + padding: 6px 12px; border: 1px solid var(--border); border-radius: var(--radius); font-size: var(--text-xs); + background: var(--bg-elevated); + box-shadow: var(--shadow-xs); } .nav-github:hover { border-color: var(--border-strong); background: var(--bg-surface); + box-shadow: var(--shadow-md), 0 0 12px rgba(224, 122, 95, 0.08); + } + + .github-icon { + flex-shrink: 0; + } + + .star-count { + font-weight: 600; + font-size: var(--text-xs); + font-family: var(--font-mono, 'JetBrains Mono', monospace); } .mobile-menu-btn { @@ -154,14 +213,20 @@ const navItems = [ display: none; position: fixed; inset: var(--nav-height) 0 0 0; - background: var(--bg); + background: var(--bg-frosted); + backdrop-filter: saturate(180%) blur(20px); + -webkit-backdrop-filter: saturate(180%) blur(20px); z-index: 99; padding: var(--space-xl); - transform: translateY(-100%); - transition: transform var(--transition); + opacity: 0; + transform: translateY(-8px); + transition: opacity var(--transition), transform var(--transition); } - .mobile-menu.open { transform: translateY(0); } + .mobile-menu.open { + opacity: 1; + transform: translateY(0); + } .mobile-menu-link { display: block; @@ -170,11 +235,35 @@ const navItems = [ font-weight: 500; color: var(--text); border-bottom: 1px solid var(--border); + opacity: 0; + transform: translateY(-4px); + transition: opacity 0.3s ease var(--delay, 0ms), transform 0.3s ease var(--delay, 0ms); + } + + .mobile-menu.open .mobile-menu-link { + opacity: 1; + transform: translateY(0); + } + + .mobile-github-link { + display: flex; + align-items: center; + gap: var(--space-sm); + } + + .mobile-github-link .star-count { + margin-left: auto; + padding: 2px 8px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: var(--text-xs); } @media (max-width: 768px) { .header-links { display: none; } .mobile-menu-btn { display: flex; } .mobile-menu { display: block; } + .oss-badge { display: none; } } diff --git a/src/components/StarBanner.astro b/src/components/StarBanner.astro new file mode 100644 index 0000000..c7b76d9 --- /dev/null +++ b/src/components/StarBanner.astro @@ -0,0 +1,205 @@ + + + + + + diff --git a/src/components/Toast.astro b/src/components/Toast.astro new file mode 100644 index 0000000..1576e44 --- /dev/null +++ b/src/components/Toast.astro @@ -0,0 +1,83 @@ + +
+ + + + diff --git a/src/components/editor/ExportBar.tsx b/src/components/editor/ExportBar.tsx index a181d80..f96c399 100644 --- a/src/components/editor/ExportBar.tsx +++ b/src/components/editor/ExportBar.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; interface ExportBarProps { apiUrl: string; @@ -7,14 +7,25 @@ interface ExportBarProps { templateId: string; } +declare global { + interface Window { + showToast?: (message: string, type?: string, duration?: number) => void; + } +} + export function ExportBar({ apiUrl, downloadUrl, params, templateId }: ExportBarProps) { const [downloading, setDownloading] = useState(false); const [copied, setCopied] = useState(null); + const downloadRef = useRef(downloadUrl); + const apiUrlRef = useRef(apiUrl); + + downloadRef.current = downloadUrl; + apiUrlRef.current = apiUrl; const handleDownload = useCallback(async () => { setDownloading(true); try { - const res = await fetch(downloadUrl); + const res = await fetch(downloadRef.current); if (!res.ok) throw new Error('Download failed'); const blob = await res.blob(); const url = URL.createObjectURL(blob); @@ -25,17 +36,20 @@ export function ExportBar({ apiUrl, downloadUrl, params, templateId }: ExportBar a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); + window.showToast?.('Downloaded!', 'success'); } catch (err) { console.error('Download error:', err); + window.showToast?.('Download failed', 'error'); } finally { setDownloading(false); } - }, [apiUrl, templateId]); + }, [templateId]); const copyToClipboard = useCallback(async (text: string, label: string) => { try { await navigator.clipboard.writeText(text); setCopied(label); + window.showToast?.(label === 'url' ? 'URL copied to clipboard!' : 'Meta tags copied!', 'success'); setTimeout(() => setCopied(null), 2000); } catch { // Fallback @@ -46,10 +60,34 @@ export function ExportBar({ apiUrl, downloadUrl, params, templateId }: ExportBar document.execCommand('copy'); document.body.removeChild(ta); setCopied(label); + window.showToast?.(label === 'url' ? 'URL copied to clipboard!' : 'Meta tags copied!', 'success'); setTimeout(() => setCopied(null), 2000); } }, []); + // Keyboard shortcuts + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + const mod = e.metaKey || e.ctrlKey; + if (!mod) return; + + // Ctrl/Cmd+S → Download PNG + if (e.key === 's' && !e.shiftKey) { + e.preventDefault(); + handleDownload(); + } + + // Ctrl/Cmd+Shift+C → Copy API URL + if (e.key === 'C' && e.shiftKey) { + e.preventDefault(); + copyToClipboard(apiUrlRef.current, 'url'); + } + } + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [handleDownload, copyToClipboard]); + const metaTags = [ ``, ``, @@ -64,28 +102,31 @@ export function ExportBar({ apiUrl, downloadUrl, params, templateId }: ExportBar return (
- - - +
+ + + +
); } diff --git a/src/components/editor/TemplateThumbnail.tsx b/src/components/editor/TemplateThumbnail.tsx index 5037356..8090ecd 100644 --- a/src/components/editor/TemplateThumbnail.tsx +++ b/src/components/editor/TemplateThumbnail.tsx @@ -18,8 +18,8 @@ export function TemplateThumbnail({ id, name, isActive, onClick }: TemplateThumb src={`/api/templates/${id}/thumbnail.png`} alt={name} loading="lazy" - width={280} - height={147} + width={600} + height={315} /> {name} diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 3ba0d60..0cb1229 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,6 +1,10 @@ --- +import { ClientRouter } from 'astro:transitions'; import Header from '../components/Header.astro'; import Footer from '../components/Footer.astro'; +import Toast from '../components/Toast.astro'; +import StarBanner from '../components/StarBanner.astro'; +import BackToTop from '../components/BackToTop.astro'; import '../styles/global.css'; interface Props { @@ -38,8 +42,7 @@ const fullTitle = title === 'OGCOPS' ? title : `${title} | ${siteName}`; - - + @@ -64,6 +67,9 @@ const fullTitle = title === 'OGCOPS' ? title : `${title} | ${siteName}`; + + + diff --git a/src/pages/api/templates/[id]/thumbnail.png.ts b/src/pages/api/templates/[id]/thumbnail.png.ts index 511c116..3187fab 100644 --- a/src/pages/api/templates/[id]/thumbnail.png.ts +++ b/src/pages/api/templates/[id]/thumbnail.png.ts @@ -20,7 +20,8 @@ export const GET: APIRoute = async ({ params }) => { let png = thumbnailCache.get(id!); if (!png) { const element = template.render(template.defaults); - png = await renderToPng(element, { width: 600, height: 315 }); + // Render at full 1200x630 so satori lays out correctly, then scale down via resvg + png = await renderToPng(element, { width: 1200, height: 630, scaleDown: 600 }); thumbnailCache.set(id!, png); } diff --git a/src/pages/api/visitors.ts b/src/pages/api/visitors.ts new file mode 100644 index 0000000..b547c07 --- /dev/null +++ b/src/pages/api/visitors.ts @@ -0,0 +1,26 @@ +import type { APIRoute } from 'astro'; +import { incrementVisitors, getVisitors } from '../../lib/upstash'; + +const SITE = 'ogcops'; +const HEADERS = { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', +}; + +export const POST: APIRoute = async () => { + try { + const counts = await incrementVisitors(SITE); + return new Response(JSON.stringify(counts), { headers: HEADERS }); + } catch { + return new Response(JSON.stringify({ today: 0, total: 0 }), { status: 500, headers: HEADERS }); + } +}; + +export const GET: APIRoute = async () => { + try { + const counts = await getVisitors(SITE); + return new Response(JSON.stringify(counts), { headers: HEADERS }); + } catch { + return new Response(JSON.stringify({ today: 0, total: 0 }), { status: 500, headers: HEADERS }); + } +}; diff --git a/src/pages/index.astro b/src/pages/index.astro index 8d19b95..30f0d23 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -11,22 +11,27 @@ const categoryCounts = getCategoryCounts();
+
+
-

Free OG Image
Generator

+

Free OG Image
Generator

{templateCount}+ templates. 8 platform previews. Free API. No login required.

- +
@@ -34,16 +39,17 @@ const categoryCounts = getCategoryCounts();

12 Categories, {templateCount}+ Templates

Beautiful, professional OG images for every use case.

-
+
{ALL_CATEGORIES.map((cat, i) => { const meta = CATEGORY_META[cat]; const count = categoryCounts[cat] || 0; return ( - - {meta.icon} -

{meta.label}

- {count} templates -

{meta.description}

+
+
+ {meta.icon} +

{meta.label}

+ {count} templates +

{meta.description}

); })} @@ -54,23 +60,39 @@ const categoryCounts = getCategoryCounts();
-
-
+
+ +

Everything You Need

+
+
+
+
+ +
{templateCount}+

Templates

Professional designs across 12 categories. Blog, SaaS, GitHub, events, and more.

-
+
+
+ +
8

Platform Previews

See how your link looks on Twitter, Facebook, LinkedIn, Discord, Slack, Reddit, WhatsApp, and Google.

-
+
+
+ +
Free

REST API

Generate OG images via URL. CORS-enabled. No API key required. Cache-friendly.

-
+
+
+ +
MIT

Open Source

Fork it. Self-host it. Contribute templates. Built by developers, for developers.

@@ -79,28 +101,45 @@ const categoryCounts = getCategoryCounts();
- +

Three Simple Steps

-
-
- 1 -

Pick a Template

-

Browse {templateCount}+ templates across 12 categories. Filter and search to find the perfect design.

+
+
+
+
1
+
+

Pick a Template

+

Browse {templateCount}+ templates across 12 categories. Filter and search to find the perfect design.

+
-
- 2 -

Customize

-

Change text, colors, and branding. Instant preview updates — no waiting for server renders.

+
+
2
+
+

Customize

+

Change text, colors, and branding. Instant preview updates — no waiting for server renders.

+
-
- 3 -

Export

-

Download PNG, copy the API URL, or grab ready-to-paste meta tags. Done in seconds.

+
+
3
+
+
+

Export

+

Download PNG, copy the API URL, or grab ready-to-paste meta tags. Done in seconds.

+
+
+
+ + + +
+ curl og.codercops.com/api/og?title=Hello +
+
@@ -110,13 +149,17 @@ const categoryCounts = getCategoryCounts();
-
-
- - - +
+
+ + + + Terminal +
+ +
-
curl "https://og.codercops.com/api/og?\
+          
curl "https://og.codercops.com/api/og?\
   template=blog-minimal-dark&\
   title=Hello%20World&\
   author=OGCOPS"
@@ -125,6 +168,20 @@ const categoryCounts = getCategoryCounts();

Generate via URL

Use our REST API to generate OG images programmatically. No API key, no rate limits, CORS-enabled.

+
    +
  • + + No API key required +
  • +
  • + + CORS-enabled for browsers +
  • +
  • + + Cache-friendly headers +
  • +
View API Docs
@@ -132,16 +189,22 @@ const categoryCounts = getCategoryCounts();
-
+
+
+
+ +

Built in Public

OGCOPS is MIT licensed. Star us on GitHub, contribute templates, or self-host.

@@ -149,15 +212,55 @@ const categoryCounts = getCategoryCounts(); + + diff --git a/src/pages/templates.astro b/src/pages/templates.astro index 7dd1f18..ffc6b3e 100644 --- a/src/pages/templates.astro +++ b/src/pages/templates.astro @@ -1,6 +1,6 @@ --- import Layout from '@/layouts/Layout.astro'; -import { getAllTemplates, getTemplatesByCategory, getCategoryCounts } from '@/templates/registry'; +import { getAllTemplates, getCategoryCounts } from '@/templates/registry'; import { CATEGORY_META, ALL_CATEGORIES } from '@/templates/types'; const templates = getAllTemplates(); @@ -16,110 +16,254 @@ const categoryCounts = getCategoryCounts();

Browse {templates.length}+ templates. Click any template to customize and download.

+ + +
- + {ALL_CATEGORIES.map((cat) => { const meta = CATEGORY_META[cat]; const count = categoryCounts[cat] || 0; return ( ); })}
+
+ + +
diff --git a/src/styles/api-docs.css b/src/styles/api-docs.css index 953fccd..573bc54 100644 --- a/src/styles/api-docs.css +++ b/src/styles/api-docs.css @@ -18,14 +18,18 @@ .api-sidebar-nav { display: flex; flex-direction: column; - gap: var(--space-xs); + gap: 2px; } .api-sidebar-link { + display: flex; + align-items: center; + gap: var(--space-sm); padding: var(--space-sm) var(--space-md); font-size: var(--text-sm); color: var(--text-muted); - border-radius: var(--radius-sm); + border-radius: var(--radius); + border-left: 2px solid transparent; transition: all var(--transition); } @@ -37,9 +41,21 @@ .api-sidebar-link.active { color: var(--accent-primary); background: var(--accent-primary-subtle); + border-left-color: var(--accent-primary); font-weight: 500; } +.api-sidebar-icon { + flex-shrink: 0; + width: 16px; + height: 16px; + opacity: 0.7; +} + +.api-sidebar-link.active .api-sidebar-icon { + opacity: 1; +} + /* Content */ .api-content { min-width: 0; @@ -57,13 +73,32 @@ max-width: 600px; } +/* Section anchors */ +.section-anchor { + color: var(--text-subtle); + text-decoration: none; + opacity: 0; + margin-left: var(--space-sm); + transition: opacity var(--transition); +} + +.endpoint-card:hover .section-anchor { + opacity: 1; +} + +.section-anchor:hover { + color: var(--accent-primary); +} + /* Endpoint Card */ .endpoint-card { background: var(--bg-elevated); border: 1px solid var(--border); - border-radius: var(--radius-lg); + border-radius: var(--radius-xl); padding: var(--space-xl); margin-bottom: var(--space-2xl); + box-shadow: var(--shadow-sm); + scroll-margin-top: calc(var(--nav-height) + var(--space-lg)); } .endpoint-header { @@ -75,13 +110,14 @@ .endpoint-method { display: inline-flex; - padding: 4px 10px; + padding: 4px 12px; font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; - border-radius: var(--radius-sm); - background: rgba(34, 197, 94, 0.15); - color: #22c55e; + border-radius: var(--radius); + background: rgba(34, 197, 94, 0.12); + color: #16a34a; + letter-spacing: 0.02em; } .endpoint-path { @@ -122,6 +158,14 @@ vertical-align: top; } +.param-table tr:nth-child(even) td { + background: var(--bg-surface); +} + +.param-table tr:last-child td { + border-bottom: none; +} + .param-name { font-family: var(--font-mono); font-size: var(--text-xs); @@ -145,35 +189,105 @@ color: var(--text-secondary); } -/* Code Block */ +/* Code Block — Terminal Chrome */ .code-block { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); + background: #1a1a1a; + border-radius: var(--radius-lg); overflow: hidden; margin-bottom: var(--space-md); + box-shadow: var(--shadow-md); } .code-block-header { display: flex; align-items: center; justify-content: space-between; - padding: var(--space-sm) var(--space-md); - border-bottom: 1px solid var(--border); + padding: 10px 16px; + background: #252525; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.code-block-header span { font-size: var(--text-xs); - color: var(--text-muted); + color: #888; + font-family: var(--font-mono); +} + +.code-block-dots { + display: flex; + gap: 6px; +} + +.code-block-dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.code-block-dot:nth-child(1) { background: #FF5F57; } +.code-block-dot:nth-child(2) { background: #FEBC2E; } +.code-block-dot:nth-child(3) { background: #28C840; } + +.code-block-copy { + padding: 3px 10px; + font-size: 11px; + font-weight: 500; + color: #888; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-sm); + cursor: pointer; + transition: all var(--transition); +} + +.code-block-copy:hover { + color: #fff; + background: rgba(255, 255, 255, 0.15); } .code-block-body { - padding: var(--space-md); + padding: var(--space-md) var(--space-lg); font-family: var(--font-mono); font-size: var(--text-xs); - color: var(--text-secondary); - line-height: 1.6; + color: #e0e0e0; + line-height: 1.7; overflow-x: auto; margin: 0; } +/* Collapsible response */ +.code-block-collapsible { + overflow: hidden; +} + +.code-block-toggle { + display: flex; + align-items: center; + gap: var(--space-sm); + width: 100%; + padding: var(--space-sm) var(--space-md); + font-size: var(--text-xs); + font-weight: 500; + color: #888; + background: #1e1e1e; + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.06); + cursor: pointer; + transition: color var(--transition); +} + +.code-block-toggle:hover { + color: #ccc; +} + +.code-block-toggle svg { + transition: transform var(--transition); +} + +.code-block-toggle.expanded svg { + transform: rotate(180deg); +} + .code-tabs { display: flex; gap: var(--space-sm); @@ -211,5 +325,11 @@ } .api-sidebar-link { white-space: nowrap; + border-left: none; + border-bottom: 2px solid transparent; + } + .api-sidebar-link.active { + border-left-color: transparent; + border-bottom-color: var(--accent-primary); } } diff --git a/src/styles/editor.css b/src/styles/editor.css index 936bee8..376842c 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -18,6 +18,7 @@ gap: var(--space-md); flex-shrink: 0; z-index: 10; + box-shadow: var(--shadow-xs); } .editor-logo { @@ -42,6 +43,11 @@ margin-right: auto; } +.editor-topbar-sep { + color: var(--text-subtle); + font-size: var(--text-xs); +} + .editor-topbar-name { font-weight: 500; color: var(--text); @@ -129,11 +135,12 @@ border-radius: var(--radius); color: var(--text); outline: none; + transition: border-color var(--transition), box-shadow var(--transition); } .template-search:focus { border-color: var(--accent-primary); - box-shadow: 0 0 0 2px var(--accent-primary-subtle); + box-shadow: 0 0 0 3px var(--accent-primary-subtle); } .template-categories { @@ -168,6 +175,8 @@ .template-grid { display: grid; grid-template-columns: repeat(2, 1fr); + grid-auto-rows: min-content; + align-content: start; gap: var(--space-sm); padding: var(--space-md); overflow-y: auto; @@ -187,15 +196,17 @@ } .template-thumb:hover { border-color: var(--border-strong); transform: translateY(-2px); box-shadow: var(--shadow-md); } -.template-thumb.active { border-color: var(--accent-primary); box-shadow: 0 0 0 2px var(--accent-primary-subtle); } +.template-thumb.active { border-color: var(--accent-primary); box-shadow: 0 0 0 2px var(--accent-primary-subtle), var(--shadow-glow); } .template-thumb-preview { + width: 100%; aspect-ratio: 1200 / 630; overflow: hidden; background: var(--bg-subtle); } .template-thumb-preview img { + display: block; width: 100%; height: 100%; object-fit: cover; @@ -249,6 +260,10 @@ justify-content: center; flex: 1; min-height: 300px; + position: relative; + background-image: radial-gradient(circle, var(--border-muted) 1px, transparent 1px); + background-size: 20px 20px; + border-radius: var(--radius-lg); } .canvas-frame { @@ -256,7 +271,7 @@ max-width: 800px; border-radius: var(--radius-lg); overflow: hidden; - box-shadow: var(--shadow-lg); + box-shadow: var(--shadow-xl); } .canvas-image { @@ -286,6 +301,17 @@ animation: spin 0.6s linear infinite; } +/* Canvas loading shimmer */ +.canvas-shimmer { + width: 100%; + max-width: 800px; + aspect-ratio: 1200 / 630; + background: linear-gradient(90deg, var(--bg-surface) 0%, var(--bg-elevated) 50%, var(--bg-surface) 100%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--radius-lg); +} + /* ===== Customize Panel ===== */ .customize-panel { display: flex; @@ -334,7 +360,7 @@ font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--text-subtle); + color: var(--accent-primary); margin-bottom: var(--space-md); padding-bottom: var(--space-xs); border-bottom: 1px solid var(--border-muted); @@ -367,12 +393,12 @@ border-radius: var(--radius-sm); color: var(--text); outline: none; - transition: border-color var(--transition); + transition: border-color var(--transition), box-shadow var(--transition); } .editor-field-input:focus { border-color: var(--accent-primary); - box-shadow: 0 0 0 2px var(--accent-primary-subtle); + box-shadow: 0 0 0 3px var(--accent-primary-subtle); } .editor-field-textarea { @@ -480,6 +506,7 @@ background: var(--accent-primary); border-radius: 50%; cursor: pointer; + box-shadow: var(--shadow-sm); } /* Upload Button */ @@ -508,10 +535,19 @@ .export-bar { display: flex; align-items: center; +} + +.export-buttons { + display: flex; + align-items: center; gap: var(--space-sm); } + .export-btn { + display: inline-flex; + align-items: center; + gap: 4px; padding: 6px 14px; font-size: var(--text-xs); font-weight: 500; @@ -523,12 +559,13 @@ } .export-btn-primary { - background: var(--accent-primary); + background: var(--gradient-coral); color: white; border-color: var(--accent-primary); + box-shadow: var(--shadow-xs); } -.export-btn-primary:hover { background: var(--accent-primary-hover); } +.export-btn-primary:hover { box-shadow: var(--shadow-md), var(--shadow-glow); } .export-btn-primary:disabled { opacity: 0.6; cursor: not-allowed; } .export-btn-ghost { @@ -556,7 +593,7 @@ font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--text-subtle); + color: var(--accent-primary); } .platform-strip-scroll { @@ -572,11 +609,13 @@ .platform-mini { flex-shrink: 0; cursor: pointer; - transition: transform var(--transition), width var(--transition); + border-radius: var(--radius); + transition: transform var(--transition), box-shadow var(--transition); } .platform-mini:hover { - transform: translateY(-2px); + transform: translateY(-2px) scale(1.02); + box-shadow: var(--shadow-md); } .platform-mini-header { @@ -589,6 +628,7 @@ text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-subtle); + text-align: center; } /* ===== Viewport Switcher ===== */ @@ -657,6 +697,7 @@ inset: 0; z-index: 1000; background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; @@ -666,7 +707,7 @@ .platform-expanded-card { background: var(--bg-elevated); - border-radius: var(--radius-lg); + border-radius: var(--radius-xl); box-shadow: var(--shadow-xl); width: 100%; overflow: hidden; diff --git a/src/styles/global.css b/src/styles/global.css index 672483e..9ca3849 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -14,7 +14,7 @@ html { body { min-height: 100vh; - line-height: 1.6; + line-height: 1.65; font-family: var(--font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -41,25 +41,26 @@ ul, ol { /* ===== Theme Variables ===== */ :root { /* Backgrounds */ - --bg: #FAFAF9; - --bg-surface: #F5F5F4; + --bg: #FAFAFA; + --bg-surface: #F5F5F5; --bg-elevated: #FFFFFF; - --bg-subtle: #F0F0EE; + --bg-subtle: #F0F0F0; + --bg-frosted: rgba(255, 255, 255, 0.8); /* Borders */ - --border: #E7E5E4; - --border-muted: #F0EFEE; - --border-strong: #D6D3D1; + --border: #E5E5E5; + --border-muted: #F0F0F0; + --border-strong: #D4D4D4; /* Text */ - --text: #0C0A09; - --text-secondary: #292524; - --text-muted: #57534E; - --text-subtle: #A8A29E; + --text: #0A0A0A; + --text-secondary: #262626; + --text-muted: #525252; + --text-subtle: #A3A3A3; /* Accent */ - --accent: #0C0A09; - --accent-muted: #292524; + --accent: #0A0A0A; + --accent-muted: #262626; --accent-primary: #E07A5F; --accent-primary-hover: #D4694F; --accent-primary-subtle: rgba(224, 122, 95, 0.1); @@ -87,6 +88,7 @@ ul, ol { --text-4xl: 2.75rem; --text-5xl: 3.5rem; --text-6xl: 4.5rem; + --text-7xl: 5rem; /* Font Families */ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; @@ -102,14 +104,23 @@ ul, ol { --radius: 8px; --radius-lg: 12px; --radius-xl: 16px; + --radius-2xl: 20px; --radius-full: 9999px; - /* Shadows */ + /* Shadows — Raycast-style multi-layer */ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04); - --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02); - --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.02); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.08), 0 4px 8px rgba(0, 0, 0, 0.02); - --shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.04); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.02); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.06), 0 4px 8px rgba(0, 0, 0, 0.03), 0 1px 2px rgba(0, 0, 0, 0.02); + --shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.08), 0 8px 16px rgba(0, 0, 0, 0.04), 0 2px 4px rgba(0, 0, 0, 0.02); + --shadow-glow: 0 0 20px rgba(224, 122, 95, 0.15), 0 0 40px rgba(224, 122, 95, 0.08); + --shadow-glow-lg: 0 0 30px rgba(224, 122, 95, 0.2), 0 0 60px rgba(224, 122, 95, 0.1); + + /* Gradients */ + --gradient-hero: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(224, 122, 95, 0.12), transparent); + --gradient-card: linear-gradient(180deg, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0) 100%); + --gradient-border: linear-gradient(90deg, transparent, var(--accent-primary), transparent); + --gradient-coral: linear-gradient(135deg, #E07A5F 0%, #C96B52 100%); /* Transitions */ --ease-out: cubic-bezier(0.16, 1, 0.3, 1); @@ -139,7 +150,7 @@ h1, h2, h3, h4, h5, h6 { color: var(--text); } -h1 { font-size: var(--text-5xl); } +h1 { font-size: var(--text-5xl); letter-spacing: -0.035em; } h2 { font-size: var(--text-4xl); } h3 { font-size: var(--text-2xl); } h4 { font-size: var(--text-xl); } @@ -185,15 +196,18 @@ p { .btn:active { transform: translateY(0); transition-duration: var(--duration-fast); } .btn-primary { - background-color: var(--accent-primary); + background: var(--gradient-coral); color: #FFFFFF; border-color: var(--accent-primary); + box-shadow: var(--shadow-sm); } .btn-primary:hover { - background-color: var(--accent-primary-hover); - border-color: var(--accent-primary-hover); - box-shadow: var(--shadow-lg), 0 4px 20px var(--accent-primary-subtle); + box-shadow: var(--shadow-lg), var(--shadow-glow); +} + +.btn-primary:active { + box-shadow: var(--shadow-xs); } .btn-secondary { @@ -249,14 +263,16 @@ p { .section-label { display: inline-flex; + align-items: center; + gap: var(--space-sm); padding: var(--space-xs) var(--space-md); font-size: var(--text-xs); - font-weight: 500; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; border: 1px solid var(--border); border-radius: var(--radius-full); - color: var(--text-muted); + color: var(--accent-primary); margin-bottom: var(--space-lg); background: var(--bg-elevated); } @@ -268,6 +284,7 @@ p { background-color: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); transition: transform var(--transition), box-shadow var(--transition), @@ -280,12 +297,22 @@ p { border-color: var(--border-strong); } +.card-interactive { + cursor: pointer; +} + +.card-interactive:hover { + box-shadow: var(--shadow-lg), var(--shadow-glow); + border-color: rgba(224, 122, 95, 0.3); +} + /* Dashed Grid */ .dashed-grid { border: 1px solid var(--border); - border-radius: var(--radius-lg); + border-radius: var(--radius-xl); overflow: hidden; background: var(--bg-elevated); + box-shadow: var(--shadow-sm); } .dashed-grid-item { @@ -311,6 +338,26 @@ p { border-right: 1px solid var(--border); } +/* ===== Badge ===== */ +.badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 10px; + font-size: var(--text-xs); + font-weight: 500; + border-radius: var(--radius-full); + background: var(--bg-surface); + color: var(--text-muted); + border: 1px solid var(--border); +} + +.badge-coral { + background: var(--accent-primary-subtle); + color: var(--accent-primary); + border-color: rgba(224, 122, 95, 0.2); +} + /* ===== Pills ===== */ .pill { display: inline-flex; @@ -322,11 +369,22 @@ p { border-radius: var(--radius-full); color: var(--text-muted); background-color: var(--bg-elevated); - transition: border-color var(--transition), color var(--transition); + box-shadow: var(--shadow-xs); + transition: border-color var(--transition), color var(--transition), background var(--transition), box-shadow var(--transition); } -.pill:hover { border-color: var(--text-subtle); color: var(--text); } -.pill-active { background-color: var(--text); color: var(--bg); border-color: var(--text); } +.pill:hover { + border-color: var(--text-subtle); + color: var(--text); + background-color: var(--bg-surface); +} + +.pill-active { + background: var(--gradient-coral); + color: #FFFFFF; + border-color: var(--accent-primary); + box-shadow: var(--shadow-sm); +} .pill-group { display: flex; flex-wrap: wrap; gap: var(--space-sm); } @@ -370,20 +428,123 @@ p { /* ===== Dividers ===== */ .divider { width: 100%; height: 1px; background: var(--border); } +/* ===== Gradient Border (reusable) ===== */ +.gradient-border-top { + position: relative; +} + +.gradient-border-top::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: var(--gradient-border); +} + +.gradient-border-bottom { + position: relative; +} + +.gradient-border-bottom::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: var(--gradient-border); +} + +/* ===== Terminal Chrome ===== */ +.terminal { + background: #1a1a1a; + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: var(--shadow-lg); +} + +.terminal-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + background: #252525; +} + +.terminal-dot { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.terminal-dot:nth-child(1) { background: #FF5F57; } +.terminal-dot:nth-child(2) { background: #FEBC2E; } +.terminal-dot:nth-child(3) { background: #28C840; } + +.terminal-title { + flex: 1; + text-align: center; + font-size: var(--text-xs); + color: #888; + font-family: var(--font-mono); +} + +.terminal-actions { + display: flex; + gap: var(--space-sm); +} + +.terminal-copy-btn { + padding: 4px 10px; + font-size: 11px; + font-weight: 500; + color: #888; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-sm); + cursor: pointer; + transition: all var(--transition); +} + +.terminal-copy-btn:hover { + color: #fff; + background: rgba(255, 255, 255, 0.15); +} + +.terminal-body { + padding: var(--space-lg) var(--space-xl); + font-family: var(--font-mono); + font-size: var(--text-sm); + line-height: 1.7; + color: #e0e0e0; + overflow-x: auto; +} + +.terminal-body code { + color: inherit; +} + +.terminal-body .token-string { color: #98C379; } +.terminal-body .token-keyword { color: #C678DD; } +.terminal-body .token-comment { color: #5C6370; } +.terminal-body .token-param { color: #61AFEF; } + /* ===== Animations ===== */ [data-animate] { opacity: 0; - transform: translateY(20px); + transform: translateY(12px); transition: opacity var(--duration-slower) var(--ease-out), transform var(--duration-slower) var(--ease-out); } [data-animate].is-visible { opacity: 1; transform: translateY(0); } -[data-animate-delay="1"] { transition-delay: 50ms; } -[data-animate-delay="2"] { transition-delay: 100ms; } -[data-animate-delay="3"] { transition-delay: 150ms; } -[data-animate-delay="4"] { transition-delay: 200ms; } +[data-animate-delay="1"] { transition-delay: 80ms; } +[data-animate-delay="2"] { transition-delay: 160ms; } +[data-animate-delay="3"] { transition-delay: 240ms; } +[data-animate-delay="4"] { transition-delay: 320ms; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @@ -393,6 +554,16 @@ p { 100% { background-position: 200% 0; } } +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-6px); } +} + +@keyframes pulse-glow { + 0%, 100% { box-shadow: var(--shadow-sm), 0 0 0 rgba(224, 122, 95, 0); } + 50% { box-shadow: var(--shadow-md), var(--shadow-glow); } +} + .skeleton { background: linear-gradient(90deg, var(--bg-surface) 0%, var(--bg-elevated) 50%, var(--bg-surface) 100%); background-size: 200% 100%; @@ -428,9 +599,17 @@ p { .mb-lg { margin-bottom: var(--space-lg); } .mb-xl { margin-bottom: var(--space-xl); } +/* Gradient text utility */ +.text-gradient { + background: var(--gradient-coral); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + ::selection { - background-color: var(--text); - color: var(--bg); + background-color: var(--accent-primary); + color: #fff; } ::-webkit-scrollbar { width: 6px; height: 6px; } @@ -438,7 +617,7 @@ p { ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--radius-full); } ::-webkit-scrollbar-thumb:hover { background: var(--text-subtle); } -:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; } +:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; } /* ===== Responsive ===== */ @media (max-width: 1024px) { From 38c7967853de8bfb7f52f88182dfdf3f50b3cb80 Mon Sep 17 00:00:00 2001 From: Anurag Verma <78868769+anurag629@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:44:49 +0530 Subject: [PATCH 3/3] fix: add Content-Type header to visitor counter POST request (#8) Vercel CSRF protection blocks POST requests without explicit Content-Type: application/json, returning 403 Forbidden. --- src/components/Footer.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 8459d93..b11e817 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -194,7 +194,7 @@ const year = new Date().getFullYear(); function loadVisitors() { // One increment per session if (!sessionStorage.getItem('ogcops-counted')) { - fetch('/api/visitors', { method: 'POST' }) + fetch('/api/visitors', { method: 'POST', headers: { 'Content-Type': 'application/json' } }) .then(function(r) { return r.json(); }) .then(function(data) { sessionStorage.setItem('ogcops-counted', '1');