80 Commits

Author SHA1 Message Date
RMP Ops
e9c761b9a2 fix(feed): pin client highlights atop Industry News for 48h 2026-06-30 22:46:43 +00:00
03134cdc69 About menu: Contact links to relevantmediaproperties.com/contact 2026-06-29 18:35:45 +00:00
aa0d9540e8 Feed: strict newest-first sort; promos are badges only 2026-06-29 05:45:46 +00:00
7a84c94c3b Articles: force-dynamic + notFound for deleted posts (no stale ISR) 2026-06-29 05:38:52 +00:00
4730439e38 Team page: table layout without photos 2026-06-27 00:29:35 +00:00
eb05918960 fix(forum): complete member profiles — counts, avatars, strip AI metadata 2026-06-26 23:46:59 +00:00
4acc068732 fix(forum): hide Ask AV Beat AI widget on Integration Room routes 2026-06-26 23:17:50 +00:00
ab6f3041fc fix(forum): remove AI assistant disclosure from Integration Room 2026-06-26 23:13:21 +00:00
ceb5333077 Industry News: server-render initial feed posts (fix 0 articles on load) 2026-06-26 19:45:38 +00:00
7d41eebf62 Footer: fix copyright punctuation and title case 2026-06-26 17:39:59 +00:00
9fcf10817f Footer: remove Relevant Media Properties text from copyright 2026-06-26 17:36:29 +00:00
545c782d37 Industry News: remove author search filter 2026-06-26 17:32:57 +00:00
3f00851908 Content Partners: fix Magewell logo (official wordmark, not RMP) 2026-06-26 17:18:17 +00:00
3f23447af7 Manufacturers directory: fix light-theme text contrast and filter bar 2026-06-25 16:22:40 +00:00
64600c4f28 Manufacturer profile: fix light-theme text contrast and BMD logo 2026-06-25 16:15:36 +00:00
ce68699839 Content Partners: align with newsletter; Blackmagic PNG logo for dark band 2026-06-25 16:04:09 +00:00
423ec363d5 Content Partners: dark #0d1520 band matching Industry News section 2026-06-25 14:35:14 +00:00
b1baf2fd10 Content Partners: match Broadcast Beat partner logos (bb schema + broadcastbeat advertisers) 2026-06-25 14:15:39 +00:00
RMP Ops
8719b62808 ads: adopt canonical RMP BannerTracking (AdSanity-style gross + viewport dedup) 2026-06-18 03:26:26 +00:00
2623fbe4ac ads: pin aja-colorbox-300x250 as the first banner in every 300x250 rotation 2026-06-16 18:11:13 +00:00
b9621ede1a footer: surface RMP lockup next to RMP mention (light/dark-aware RmpMark) 2026-06-16 00:46:48 +00:00
f1e582610b newsletter: cross-property auto-subscribe disclosure on AV signup widgets 2026-06-15 21:07:27 +00:00
c9013249eb image-hosts: allow broadcastbeat.com + advertising.RMP
AV beat banners point at images on broadcastbeat.com and on
advertising.relevantmediaproperties.com. Without these in the Next.js
remotePatterns allowlist, /_next/image proxy returns 400 and the page
falls back to article-placeholder-v2.svg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 02:52:42 +00:00
c3b2c284b5 fix: forum login social auth button text color was white-on-white
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 21:21:53 +00:00
36dc2577bd fix(contact): editorial email → editor@relevantmediaproperties.com
Per Ryan 2026-06-05. The avbeat-domain editor@ address routed to the
local AVB mailbox; RMP-wide editorial inbox is the canonical destination
going forward. Single href + display-text change on /about/contact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 14:55:57 +00:00
91e60caa60 fix: /manufacturers UI cleanup + /about/team phone formatting + /about media-kit link
Three pointed fixes:

/manufacturers (cloned from BB; still had broadcast vibe)
  • Drop the hardcoded "NAB + IBC" filter chip.
  • Per-card "2026 NAB Show" / "IBC 2026" badges replaced by dynamic
    pills generated from shows_attending intersected with av.events
    (so each card now shows the actual AV shows that exhibitor is
    attending — InfoComm, CEDIA, ISE, DSE, etc.).
  • Page intro counts: drop nabCount / ibcCount hardcoding, compute
    "top 3 shows by exhibitor count" from the data and surface those
    counts dynamically. With only InfoComm 2026 currently tagged the
    line reads "851 at InfoComm 2026" — adds more rows once other
    shows are scraped.
  • Meta description + OG: pro-AV / display / signage instead of
    "broadcast, post-production, and live-production."

/about/team phone formatting
  • New formatPhone() helper: E.164 → "(213) 418-2600" pattern.
  • Extension format: ", Ext. NNN" (capital E, comma-separated).
  • tel: link still uses the raw digits + RFC-3966 ',ext' pause for
    PBX auto-dial on supported clients.

/about page media-kit link
  • Was href="/about/advertise" — a relative path that 404s (no such
    route exists). Replaced with the absolute media-kit URL on
    relevantmediaproperties.com that the /advertise + /about/contact
    pages already use. Opens in a new tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 14:53:54 +00:00
a63a5db6d3 fix(show-coverage): white show-title on dark card (was inheriting dark navy)
/show-coverage cards have bg-[#0b0f17] but the H3 had no color, so it
inherited text-[#0F172A] from the page wrapper — dark-on-dark, unreadable.
Forced text-white to match the header's main-menu text color.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 14:48:12 +00:00
8d548bdc9d fix(av-beat): skip_rewrite passthrough — Mike Nann (Writeturn) now publishes verbatim w/ noindex
Mike Nann's distribute.users record has skip_rewrite=true (set when task
#19 added the column). distribute-rmp's submit route forwards the flag
in its payload (task #20). BB's receive route handles it (task #21).
AV Beat's receive route was missing the branch entirely AND was writing
to .schema('bb') instead of 'av' — a copy-paste bug from when this file
was cloned from BB. So every Mike Nann submission to AV Beat was either
landing in BB's schema (wrong) or trying to and failing.

Fixed in this commit:

src/app/api/submission-receive/route.ts
  • .schema('bb')  →  .schema('av')   (the copy-paste bug)
  • Added skip_rewrite branch: when payload has skip_rewrite=true,
    insert straight into av.wp_imported_posts with status=published,
    noindex=true, and the contributor's firm/contact_name as byline.
    Skips the native_articles → AI-rewrite path entirely. Mirrors BB's
    pattern exactly. Audit row still written to original_submissions.

src/lib/articles/types.ts
  • Article.noindex?: boolean — surfaces the DB column to the renderer.

src/lib/articles/legacy-source.ts
  • SELECT_COLS adds noindex
  • ImportedPostRow gains noindex field
  • rowToArticle propagates !!row.noindex

src/app/news/[slug]/page.tsx + src/app/articles/[slug]/page.tsx
  • generateMetadata: when article.noindex, set robots = no-index +
    no-follow + nocache + googleBot no-image-index + other meta key
    "googlebot-news": "noindex". Matches BB's pattern.

src/app/news-sitemap.xml/route.ts
  • Adds .or('noindex.is.null,noindex.eq.false') so passthrough rows
    don't pollute the Google News sitemap.

src/app/rss/route.ts
  • Filters out a.noindex before emitting items.

Mike's existing user record is already flagged (DB verified), so the
moment this deploys + he submits, the next press release publishes
verbatim, gets noindex on every Google/social surface, and shows up
on the AV Beat news index without going through the AI rewrite job.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 14:32:04 +00:00
45cf96a203 feat(admin): mobile-friendly admin shell + promote-on-social MVP
Mobile admin chrome
- New <AdminShell> component: sticky dark top bar w/ hamburger that opens
  a right-side drawer listing every admin section grouped by area
  (Content / Editorial / Companies / Newsletter / AdOps / Accounting /
  People). Closes on route change, Esc, or backdrop tap. Body scroll
  locks while open.
- Wrapped /admin (dashboard) and /admin/articles with the shell. Other
  admin pages still render their own layouts — they'll get the wrapper
  in follow-ups; the shell sits ABOVE them so nothing breaks today.

Promote-story MVP (share URLs, no OAuth yet)
- New /admin/promote/[slug] page: per-article promote sheet w/ one button
  per platform. Each uses that platform's standard share URL:
    • LinkedIn  → /sharing/share-offsite/?url=
    • X         → /intent/tweet?text=…&url=…&hashtags=…
    • Facebook  → /sharer/sharer.php?u=…
    • Instagram → copies caption to clipboard + opens instagram:// camera
    • Email     → mailto: with subject + body pre-filled
  On iPhone each button opens the platform's native app via universal
  link. No OAuth, no API keys, works tonight.
- New "Promote" action button on every published article row in
  /admin/articles — links to /admin/promote/<slug>.

Next session
- Meta OAuth (Facebook Page + IG Business) — Ryan to register the Meta
  App in Business Suite first, then we wire one-tap server-side posting.
- LinkedIn OAuth + Marketing API.
- X paid API ($100/mo Basic decision pending).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 23:06:14 +00:00
a8dc457742 fix(mobile): mobile menu unreadable + secondary nav overflowing + forum auth white-on-light
Critical mobile bugs flagged from an iPhone screenshot.

1) Mobile hamburger menu — all main nav items (HOME / SHOW COVERAGE /
   DIRECTORY / FORUM / READING LIST) were `text-white` on
   `bg-[#F8FAFC]`, i.e. white-on-light. Effectively invisible. Legacy
   styling from the dark-theme era that never got updated. Flipped to
   `text-[#0F172A]` + `hover:bg-[#FFFFFF]` + `border-[#DCE6F2]` to match
   the My Profile / WP Import links right below them that were already
   correctly themed.

2) Secondary top nav (social icons / language switcher / Sign In + Join
   / newsletter email input) was a single h-8 flex row that always
   rendered — on a phone-width viewport the row spills the email input
   off the right edge. Added `hidden md:flex` so the whole bar
   disappears on mobile. Affordances stay reachable via the hamburger
   menu, in-forum Sign In/Join buttons, and the footer.

3) Forum auth pages (login, register, forgot-password, reset-password)
   had `text-white` on every H1 AND on every form input that uses
   `bg-[#F8FAFC]`. So users opening any of those four pages saw a blank
   white card with invisible labels and couldn't see what they were
   typing into the password fields. Same dark-theme regression as #1.
   Blanket-replaced `text-white` with `text-[#0F172A]` across all four
   files, then restored `text-white` only inside classNames that also
   contain `bg-[#1D4ED8]` / `bg-[#1E3A8A]` / `brand-gradient` (the
   submit buttons — those WERE correct).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 22:22:18 +00:00
761ebf0cdc fix(placeholder): rename SVG to bust Cloudflare cache
The new A-monogram placeholder shipped in d8ca8ff was correct in the
deployed container, but Cloudflare was serving the previous cached SVG
(cf-cache-status: HIT, last-modified from this morning) because the
account token doesn't carry Zone:Cache Purge scope, so we couldn't
issue an API purge.

Renaming the file to article-placeholder-v2.svg gives CF a fresh URL
to cache, sidestepping the missing-permission entirely. Updated all
6 referencing files (AppImage fallback, legacy-source FALLBACK_IMAGE
constants, FeaturedBento[FromDb], manufacturers/authors pages).

Old file (cached as the old wordmark) just becomes a dangling asset —
not referenced from anywhere, harmless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 21:44:20 +00:00
d8ca8ff3ec fix(article-detail): sidebar column width + placeholder using new mark
Two pointed bugs on the article detail page:

1) Width alignment. The right-rail Related Articles and Related Forum
   Posts boxes were free-stretching to the lg:col-span-4 width while the
   SidebarAdStack below them rendered ads at their natural 300px (items-
   end, right-aligned). The visual result: boxes overhung the ad column
   on both edges. Wrapped each sidebar in an inner `w-full lg:w-[300px]
   lg:ml-auto` div so every block (ads + boxes + newsletter) shares one
   300px column on lg+ and stays full-width on mobile. Applied to both
   article templates (/articles/[slug] and /news/[slug]).

2) Placeholder rendered the old "AV BEAT" wordmark when an article had no
   featured image. Rebuilt article-placeholder.svg around the new brand
   mark — the A-monogram tile with the blue gradient (#2563EB→#1E3A8A)
   that matches AnimatedLogo + the rest of the brand-gradient system.
   Same 1200×630 dimensions so OG/feed embeds aren't affected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 20:34:30 +00:00
3a65e776ac fix(ai-disclosure): remove every visible "AI persona" reference + team-card visibility
Two unrelated production bugs called out in the same review pass:

1) AI persona disclosure — strictly removed everywhere readers see it.
   - /about/team hero subtitle no longer says "trained AI personas working
     under house style"; copy now reads like every other newsroom team page.
   - /about "How we work" block drops the line about AI assistants
     rewriting press releases + the "Our AI personas are clearly labeled"
     sentence. Reworded to just describe the editorial process.
   - ArticleComments no longer renders the small "AI" chip beside
     is_ai_seeded commenters. The flag stays in the DB for internal
     accounting; it just doesn't surface in the UI.
   - Stale code comments in articles/[slug] + news/[slug] that referenced
     "AI personas participate (tagged)" cleaned up.
   The is_ai_seeded / is_ai_response columns are now strictly internal-only.

2) Team page card visibility — the page wrapper got switched from
   text-[#e5e7eb] to text-[#0F172A] in the recent restyle, which fixed
   white-on-white outside the cards but left the dark-card H3 names
   inheriting dark navy on a dark navy bg = invisible. Forced text-white
   on both H3s (editorial + ops). Email rows now use min-w-0 + flex-1 +
   block truncate so long addresses ellipsis inside the card instead of
   pushing the layout past the right edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 20:27:25 +00:00
ca317f26a9 feat(ratings): real reader ratings with no-login, 1-vote-per-session
Adds a no-auth star rating system backed by a centralized cross-property
table on supabase01.

Database:
- distribute.article_ratings (id, property, article_slug, rating 1..5,
  session_id, ip_hash, user_agent, created_at) with UNIQUE
  (property, article_slug, session_id). property column means every RMP
  site shares one table; AV Beat writes 'avbeat', BB writes 'bb', etc.
- distribute.article_rating_aggregates view rolls up real_count + real_avg
  per (property, slug).

API (/api/articles/rating):
- GET ?slug=X → { avg, count, my_rating } — reads aggregate, plus the
  current session's vote if any.
- POST { slug, rating } → inserts the vote; unique_violation on duplicate
  is silently swallowed (one vote per session). Returns updated summary.
- Session lives in `av_rate_sid` cookie (uuid, 1y, sameSite=lax). Cookie
  is minted by the API on first request, so the client never has to know
  about it.

StarRating component:
- Now interactive — hover preview, click to submit, "thanks!" affordance.
- Merges the existing deterministic baseline (slug-hash → 4.0–5.0 + age-
  scaled count) with the real aggregate using the baseline as a Bayesian
  prior, so a single real rating doesn't crater the displayed average.
- Once the current session has voted, stars show "your rating" and
  hover/click are disabled.
- Text colors updated #d0d0d0 → #0F172A so the rating number is readable
  on AV Beat's light backgrounds.

Property identifier hardcoded as "avbeat" at the top of the route — when
porting to BB / SNS / BLB / etc., change that constant and keep
everything else identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 19:32:28 +00:00
f44add70c7 feat(brand+promotion): brand-gradient system, restyled /about/* + team + contact, alias-based client matcher
Three coupled rollouts in one shot.

PART A — client highlight matcher (DB migration applied separately):
- promoted-articles.ts: AdvertiserName now carries brand_aliases; matcher
  iterates legal name + every alias with word-boundary regex and picks the
  longest match across all candidates so "Magewell Launches…" resolves to
  "Magewell Electronics Co., Ltd." via its Magewell alias.
- New isStoryAboutClient gate: hard-accept at position 0, accept if any
  editorial frame ("Product Spotlight:", "Review:", "First Look", …) appears
  in the prefix, hard-reject if the prefix ends with a partner/usage phrase
  ("with", "using", "powered by", "dealer of", "integration with", "used",
  "deploys", "runs on", "leverages", …), otherwise accept within a 32-char
  subject window. Validated against 5,000 historical posts: 27 partner
  false-positives blocked, 3 editorial-frame stories correctly saved, the
  Magewell article now highlights.

PART B — restyling /about, /about/team, /about/contact:
- New .brand-gradient utility: #2563EB→#1E3A8A linear-gradient with a
  16%-opacity diagonal sheen that sweeps in the first 30% of a 7s cycle
  and rests offscreen-right for the rest. Respects prefers-reduced-motion
  (band parked statically at 6% opacity).
- Team page: dark legacy hero (red radial, #ff5a55 eyebrows) replaced with
  brand-gradient hero; outer text wrapper #e5e7eb → #0F172A so section
  headings stop being white-on-white; all serif H1/H2/H3 → font-display;
  red accents (#ff5a55 / #ff6b66 / rgba(214,7,1)) → blue (#93C5FD / #7DD3FC
  / rgba(29,78,216)).
- About page: H1 + Stat values → font-display; Stat cards get a brand-
  gradient top stripe.
- Contact page: outer wrapper #e5e7eb → #0F172A (fixes the white-on-white
  Contact H1); H1 gets explicit text-[#0F172A] + font-display; dark contact
  cards keep dark bg but gain a brand-gradient top stripe + explicit
  text-[#e5e7eb] body + brighter cyan eyebrow/link colors.

PART C — richer Industry News client highlight:
- ArticleFeed: client-promoted row swaps the static border-l-4 for an
  absolute-positioned .brand-gradient strip on the left edge (sheen pulses
  through). Row gets a slightly richer bg tint (from-#1D4ED8/12 via /4).
- HIGHLIGHT pill now uses .brand-gradient instead of the static blue chip.

Also shipping the earlier-staged work:
- AnimatedLogo onDark: white ring around tile + brighter (#E2E8F0) tagline
  + cyan accent rule for the blue header.
- Article-prose: em/strong/code dark colors (em fixes the trailing
  copyright/trademark ghost text), hr line color, figcaption readable.
- ArticleComments: "Sign in to join the discussion" bar uses brand-gradient
  + white/outline buttons that read on blue.
- Forum: Discover/Following, Newest/Top Voted/Unanswered, Sign in to post,
  Start a thread, Create account — slim pills, brand-gradient active.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 18:37:15 +00:00
0b2ed8cd7f fix(article): kill mobile-breakpoint drop-cap rule missed in prior pass
The 720px media query had its own ::first-letter override at 52px that
survived the desktop-rule removal — first letter was still rendering
oversized on mobile. Removing this completes the drop-cap rip-out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 17:14:12 +00:00
abc4f6fc8e fix(article): remove drop-cap + match link color/font to body text
Two complaints on /news/[slug]:
- the oversized first-letter drop-cap looked editorial-magazine, not in
  spec — first paragraph should start as normal prose
- inline links rendered near-white against the white page because
  Tailwind Typography's prose-a variable was overriding the (no-!important)
  brand-blue rule, leaving ghost links unreadable

Drop the ::first-letter rule entirely. Force link color to #1e293b body,
font-family to inherit, and decoration color to currentColor so the
underline is the only visual link cue (still WCAG-distinguishable
without relying on color). Hover keeps brand blue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 16:52:59 +00:00
bf484c14c3 fix(header): switch AnimatedLogo to onDark variant for blue header contrast
Header bar is #1D4ED8 but logo was rendering with navy text on the tile's
#2563EB→#1E3A8A gradient — wordmark + tagline were nearly invisible. Flip
all three header breakpoints to variant="onDark" (white wordmark, slate-200
tagline). Also: switch the onDark tagline accent rule from #1D4ED8 (same
blue, invisible) to #7DD3FC cyan, and paint a faint white ring around the
tile so the rounded mark stays distinct from the header background.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 16:42:02 +00:00
ops-bot
e43d8737cf feat(brand): wire AnimatedLogo into Header + Footer
- Header: replaces AvBeatLogo across all 3 breakpoints (full / no-tagline / iconOnly).
- Footer: replaces AvBeatLogo full variant.
- AnimatedLogo: new iconOnly prop hides the wordmark+tagline span entirely for the mobile breakpoint.

Existing AvBeatLogo file kept on disk for now (unimported, can be deleted later) so we have a quick rollback target if anyone wants the static A+V monogram back.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 16:15:01 +00:00
ops-bot
6b1a5c281a feat(brand): AnimatedLogo component + Space Grotesk display font
- src/app/layout.tsx: load Space_Grotesk via next/font/google as
  --font-display (weights 500/600/700, self-hosted, display:swap).
- src/components/AnimatedLogo.tsx: standalone animated brand mark.
  Single-A monogram in #2563EB->#1E3A8A tile, Space Grotesk 700
  wordmark, Inter tagline with accent rule. Stroke-draw + cyan signal
  pulse + sheen sweep + text fade. Pure SVG/CSS, no animation libs.
  Respects prefers-reduced-motion. Variants: onLight | onDark.
- src/app/preview/animated-logo/page.tsx: preview route at
  /preview/animated-logo (noindex). Existing AvBeatLogo untouched —
  Header/Footer wiring unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 16:07:06 +00:00
ops-bot
aa5bee95e2 fix(seo): strip HTML from meta description + OG/Twitter/Schema.org
Article excerpts sometimes contain raw <p> tags; Next.js HTML-escapes
them into the page <meta>, surfacing as literal '&lt;p&gt;' in view-source
and breaking SEO previews. cleanExcerpt() strips tags + entities for all
plain-text meta surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 15:21:26 +00:00
ops-bot
e84f69f241 ui(cards): object-contain on article-card images so portraits don't crop
Boxes stay sized; tall/odd-ratio images letterbox cleanly against the
existing slate card background instead of being chopped at the edges.
Applied to FeaturedBento, FeaturedBentoFromDb, ArticleFeed, NewsPageClient,
SpotlightCarousel, FeaturedCarousel, AISuggestedArticles, search/page.tsx.

Avatars (rounded-full) unchanged — still object-cover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 15:08:50 +00:00
ops-bot
bc49c676c3 ui(placeholder): rebrand article-placeholder.svg from BROADCAST BEAT to AV BEAT
Sarah Quinn's author page (and any article missing a featured_image) was
showing the old BB-branded placeholder. New SVG uses the AV Beat navy/cyan
palette and 'PRO AV · DISPLAY · LIVE PRODUCTION' tagline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 15:07:38 +00:00
ops-bot
c35ed117b2 ui(footer): swap legacy logo.svg for AvBeatLogo (matches header)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 15:03:25 +00:00
ops-bot
c959a5f8f7 ui(article): editorial standard — cap featured image at 480px (mirror of BB)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 14:56:26 +00:00
ops-bot
9fa69ef74e ui(article): bb-article-prose Georgia -> Inter sans + body smoothing
Mirror of the BB article-prose change. Font-family swap only; colors were
already dark on AVB. Body smoothing re-enabled per Ryan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 14:53:40 +00:00
ops-bot
d832e624cc contact+ads: route advertising mail to advertising@relevantmediaproperties.com
Adverts/contact UI emails on /about/contact + /advertise now point at the
central advertising alias (forwarded to ryan.salazar@). Media-kit hyperlink
on /about/contact corrected to relevantmediaproperties.com/av-beat---media-kit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 14:11:19 +00:00
ops-bot
e521820a2c ui(featured): make 'All featured' CTA prominent — pill button instead of text link
Was 11px mono with no background; now a navy outlined pill with uppercase
bold tracking, light-blue fill, and hover state. Same target route.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 13:54:43 +00:00
6e9a163cfd fix(contrast,brand): full audit sweep — white-on-white + cross-component renames
- forum/page.tsx: text-white → navy on TopThreads, Following feed, Discover feed cards, search input, select, category grid, Community Guidelines; greys lightened where applicable for new white surface
- forum/[slug]/page.tsx: same sweep for category page (h1, h2, thread rows, new-thread form, search/sort bar) + hover:bg-[#1e2a3a] → slate-200
- forum/user/[username]/page.tsx: text-[#cccccc]/[#bfbfbf]/[#e0e0e0] + bg-[#222] tag chip + #9ca3af labels → readable on white card
- authors/[slug]/page.tsx: profile contact/facts cards readable
- search/page.tsx: h1 + h2 navy + hover slate
- news/[slug]/NewsArticleDetailClient.tsx: toast color
- news/NewsPageClient.tsx: import cleanExcerpt, strip HTML from listing excerpts
- privacy/terms/page.tsx: h1 + h2 + body greys to readable navy/slate
- error.tsx: page-level text to navy + button border slate
- about/page.tsx: page body text + Stat/Block cards to white surface w/ navy text
- Header.tsx: mobile hamburger + dropdown links → readable navy + slate border
- NotificationCenter.tsx: hover + unread bg flipped to light blue
- AISuggestedArticles.tsx: hover bg → slate-200
- Footer.tsx: link greys to #475569 + #DCE6F2 borders
- AskBBAI.tsx: copy rename 'BB AI' → 'AV Beat AI' (greeting, button, header, aria)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 22:13:42 +00:00
2826f0b566 ui(avbeat): industry-news card hover from near-black #1c1c1c → slate-200 #e2e8f0
src/styles/tailwind.css:195 .article-card:hover background-color was #1c1c1c (near-black, leftover from the dark theme). On the white page that made the card go black on hover and the text disappear into it. Swapped to #e2e8f0 (Tailwind slate-200) so the hover reads as a clean dark-gray distinct from the white surface while keeping the navy article title + slate excerpt fully readable.
2026-06-03 21:40:34 +00:00
a5d4c50186 fix(contrast,brand): secondary-page H1/subtitle white-on-white + rename 'The Crew Lounge' → 'The Integration Room'
CONTRAST (white-on-white on light surfaces, mirrors the prior article-body pass):
  - show-coverage/page.tsx:72  <main> wrapper text-[#e5e7eb] → text-[#0F172A]
  - show-coverage/page.tsx:76  '0 upcoming / 0 past / 0 total' subtitle text-[#9ca3af] → text-[#475569]
  - manufacturers/page.tsx:79  H1 'AV Integration Manufacturers' text-[#e0e0e0] → text-[#0F172A]
  - manufacturers/page.tsx:82  '… manufacturers' subtitle text-[#e0e0e0]/70 → text-[#475569]
  - forum/page.tsx:294 hero H1 text-white → text-[#0F172A]
  - forum/page.tsx:296 hero intro 'Connect with pro AV integrators…' text-[#aab4c4] → text-[#475569]
  - forum/page.tsx:116 'Top Performing Threads' H2 text-white → text-[#0F172A]
  - forum/page.tsx:581 'Browse Categories' H2 text-white → text-[#0F172A]

  Skipped per direct exclusion: forum/page.tsx:591 (per-category card titles).

  Homepage featured grid checked, no edit needed: FeaturedBento 2x2 h3 is already text-[#0F172A] on bg-#FFFFFF; FeaturedBentoFromDb hero is white on a dark image gradient (correct overlay pattern).

BRAND RENAME: forum is renamed everywhere user-visible.
  - forum/page.tsx hero img alt + H1 text 'The Crew Lounge' → 'The Integration Room'
  - forum/layout.tsx — all 7 occurrences (title, description, openGraph, twitter, JSON-LD WebPage name, breadcrumb item)
  - components/crew-lounge/CrewLoungeLogo.tsx — img alt
  - forum/user/[username]/page.tsx — '← Back to The Crew Lounge' link

  Logo SVG filename (/logos/crew-lounge-logo.svg) and React component name (CrewLoungeLogo) intentionally NOT renamed — they're internal asset paths and would cascade through unrelated imports; only user-visible brand strings touched.
2026-06-03 20:14:23 +00:00
93db296380 ui(avbeat): /advertise 'Request Media Kit' CTA → media-kit page on relevantmediaproperties.com
src/app/advertise/page.tsx:55 — hero CTA was pointing at the in-page
anchor '#contact'. Swapped to absolute URL
https://relevantmediaproperties.com/av-beat---media-kit (the AV Beat
media-kit landing). target=_blank + rel=noopener so the article tab
isn't replaced.
2026-06-03 20:07:18 +00:00
43d2bdd106 ui(avbeat): rename Industry News promoted-coverage badge 'Advertiser' → 'HIGHLIGHT'
src/app/home-page/components/ArticleFeed.tsx:597 — the article-card badge
shown when promoted==='client' said '◆ Advertiser' with a title tooltip
'Advertiser coverage: <name>'. Both copies now say 'HIGHLIGHT' / 'Highlight: <name>'.

Visual treatment (diamond glyph, blue token, uppercase tracking) preserved.
The ★ Featured badge for promoted==='premium' is untouched.

Note: BB still has the original 'Advertiser' label; this change is avbeat-only
unless Ryan asks for it mirrored on BB.
2026-06-03 18:15:49 +00:00
334115068b fix(contrast,typography): article header section — match body font (font-serif) + color (#1e293b)
Top-of-article block (above the featured image) used Inter (font-heading)
and inconsistent colors (H1 navy, dek light-grey #999, author name
near-white #e0e0e0). With the body section now in Georgia-style serif
on slate-800 (#1e293b), the header read as a separate visual zone.

Unified both detail routes:

src/app/news/[slug]/NewsArticleDetailClient.tsx
  - H1   :309 font-heading text-[#0F172A] → font-serif text-[#1e293b]
  - Dek  :315 font-body text-[#999]        → font-serif text-[#1e293b]   (this is the lead summary line that was reading as 'white' on the Magewell article — same near-white #999 on white surface)
  - Author name link :332 font-heading text-[#e0e0e0] → font-serif text-[#1e293b]

src/app/articles/[slug]/ArticleDetailClient.tsx
  - H1   :255 same swap as above
  - Author name link :274 same swap as above

Secondary meta (category eyebrow #1D4ED8 blue, read-time/date/author-title #777) intentionally kept at their muted shades — those are subordinate text and read fine on the light surface.
2026-06-03 16:59:12 +00:00
1a6b9d4333 fix(contrast): article-body white-on-white — body/dropcap/h2/h3/blockquote/links to navy + slate, drop prose-invert
Article detail pages (/news/[slug] and /articles/[slug]) inherited a dark-theme
prose color palette via the .bb-article-prose ruleset + Tailwind's
prose-invert utility. With the new light surface, every text element in
the article body rendered near-white on white, while the H1 (fixed in
the prior commit) was already navy. Bringing the body in line:

src/styles/tailwind.css:
  - .bb-article-prose wrapper           #d6d4d0  -> #1e293b !important  (slate-800 for long-form body)
  - .bb-article-prose p:first-of-type   #e8e6e0  -> #1e293b
  - .bb-article-prose first-letter      #f0eee8  -> #0F172A             (drop-cap full navy)
  - .bb-article-prose h2                #f5f3ee  -> #0F172A             (in-body subhead matches the page H1)
  - .bb-article-prose h3                #ece9e2  -> #0F172A
  - .bb-article-prose blockquote        #e8e6e0  -> #1e293b
  - .bb-article-prose a (BASE)          #d6d4d0  -> #1D4ED8             (inline links visibly blue at rest, no longer invisible-until-hover; hover unchanged)

src/app/news/[slug]/NewsArticleDetailClient.tsx
src/app/articles/[slug]/ArticleDetailClient.tsx
  - Drop 'prose-invert' from the wrapper className on both routes so any
    descendant the bb-article-prose rules don't explicitly color (lists,
    strong, em, captions) falls back to Tailwind Typography's default
    light-page slate-700 palette.
2026-06-03 16:49:48 +00:00
ecebc9b3b4 ui(avbeat): shrink featured-story hero excerpt ~50%
FeaturedBentoFromDb.tsx:119 — hero excerpt size ramp halved:
  text-base md:text-lg  (16 / 18 px)
  → text-xs md:text-sm  (12 / 14 px)

Restores a clear size gap below the (already-shrunk) hero headline; the
excerpt was visually competing with the title at the same breakpoint.
2026-06-03 16:27:37 +00:00
d5b54d077b ui(avbeat): rename homepage 'Staff Editorial' section label to 'Featured Editorial' 2026-06-03 16:25:40 +00:00
802fa05631 ui(avbeat): shrink featured-story hero headline ~50%
FeaturedBentoFromDb.tsx:115 — hero h3 size ramp halved:
  text-2xl  md:text-4xl  lg:text-5xl   (24 / 36 / 48 px)
  → text-base md:text-lg  lg:text-2xl  (16 / 18 / 24 px)

leading-tight and the rest of the class string preserved.
2026-06-03 16:18:03 +00:00
aeff448448 revert: typography phase-1 — back out body smoothing block + hero serif tightening
Reverts the two typography-aesthetic edits from f1ecfe3:
  - src/styles/tailwind.css body rule: restore the 'smoothing intentionally NOT overridden' state (drops -webkit-font-smoothing antialiased, -moz-osx-font-smoothing grayscale, text-rendering optimizeLegibility, font-feature-settings, letter-spacing -0.005em).
  - src/components/FeaturedBentoFromDb.tsx:115 cinematic hero headline: restore text-2xl md:text-4xl lg:text-5xl leading-tight (drops text-[40px]/[48px]/[56px], leading-[1.05], tracking-[-0.02em], font-bold).

Intentionally kept from f1ecfe3:
  - NewsArticleDetailClient.tsx + ArticleDetailClient.tsx H1 navy fix (#0F172A) — readability bug, not typography aesthetic.
  - ArticleFeed.tsx Topics chip row removal — separate UI direction.
2026-06-03 16:12:33 +00:00
f1ecfe3176 feat(avbeat): typography phase-1 slice + drop homepage Topics chip row
Typography (approved Phase 1 slice only — utility classes + other reclasses deferred):
- src/styles/tailwind.css body rule: re-add smoothing now that the surface palette is locked in — -webkit-font-smoothing antialiased, -moz-osx-font-smoothing grayscale, text-rendering optimizeLegibility, font-feature-settings 'kern','liga', global letter-spacing -0.005em.
- src/components/FeaturedBentoFromDb.tsx:115 cinematic hero headline: text-2xl md:text-4xl lg:text-5xl leading-tight → text-[40px] md:text-[48px] lg:text-[56px] leading-[1.05] tracking-[-0.02em] font-bold. Confident editorial display per the avinteractive reference.
- src/app/news/[slug]/NewsArticleDetailClient.tsx:309 and src/app/articles/[slug]/ArticleDetailClient.tsx:255 article H1: text-[#f0f0f0] → text-[#0F172A]. Fixes the leftover white-on-white readability bug on article pages.

UI:
- src/app/home-page/components/ArticleFeed.tsx: removed the 'Topics:' chip row from Industry News per current design direction; all stories now show without per-topic filter.
2026-06-03 15:57:40 +00:00
25abc55da1 feat(avbeat): point forum + auth at av schema, swap newsletter chips to AV verticals
Schema wiring — AV Beat was inheriting a 'bb' default schema from the
original fork. Fixed three call sites so the AV Beat app reads/writes
the av schema unless an explicit NEXT_PUBLIC_SUPABASE_SCHEMA override
is set:
  - src/lib/supabase/client.tsx:8 default 'bb' -> 'av'
  - src/lib/supabase/server.tsx:11 default 'bb' -> 'av'
  - src/app/api/auth/lookup-email/route.ts :31 + :42 hardcoded
    .schema('bb') -> .schema('av') so signup lookup hits the
    AV-side subscriber tables, not BB's.

Newsletter signup — homepage chips were the BB taxonomy ('Broadcast',
'Post Production', 'Animation', 'AI & Automation', 'Live Production',
'NAB Show'). Swapped for pro-AV verticals that align with the new
forum categories: Conference Rooms & UC, Digital Signage, Live Events
& Staging, Displays & LED, Audio, AV over IP, InfoComm & ISE.
Default selected flipped from 'Broadcast' to 'Conference Rooms & UC'
so the form doesn't pre-select a non-existent value.
2026-06-03 14:39:12 +00:00
190d374bb1 fix(avbeat): drop antialiased/grayscale font-smoothing on light theme
-webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale
were set on body and the Tailwind 'antialiased' utility was applied on
<body>. Both pair correctly with the prior dark theme (light text on
dark surfaces), but on the new white surface they render Inter + Lora
thinner and harsher — strokes lose weight, edges read as jagged.

Removed from src/styles/tailwind.css:59-60 (body rule) and from the
<body className="... antialiased"> utility in src/app/layout.tsx:143.
OS default smoothing (subpixel on Windows / older macOS, 'auto' on modern)
gives Inter + Lora their intended weight and roundness.
2026-06-03 13:34:02 +00:00
0e9d3c570e fix(avbeat): noStore() on homepage so hero actually re-renders
dynamic='force-dynamic' + revalidate=0 on the page weren't enough because
FeaturedBentoFromDb -> pickAds() -> fetch in src/lib/ads.ts:98 sets
next:{revalidate:30}. Next.js takes the lowest positive revalidate from
the render tree and opts the whole route into 30-second ISR (visible in
.next/prerender-manifest.json: '/' initialRevalidateSeconds: 30), masking
the page-level force-dynamic directive.

unstable_noStore() called at the top of the HomePage component body
forces an unambiguous, child-tree-overriding opt-out so the Telycam
hero swap (and any future DB-driven hero change) renders on every
request. lib/ads.ts caching is left alone — its revalidate is fine for
ad rotation; we just stop it from poisoning the page-level cache.
2026-06-03 13:29:54 +00:00
1977e077f8 fix(avbeat): 3 homepage bugs — invisible AI sidebar titles, dark tagline on blue header, literal <p> in excerpts
(A) src/components/AISuggestedArticles.tsx :186 + :287 — 'AI Suggested Articles' card titles
    used text-[#e0e0e0] on now-white card surfaces, making the title invisible until
    group-hover flipped it to #1D4ED8. Both occurrences changed to text-[#0F172A].
    (ArticleFeed.tsx:627 and NewsPageClient.tsx:276 were already navy from the
    prior contrast-fix deploy and cover both pagination + infinite-scroll variants.)

(B) src/components/AvBeatLogo.tsx :99 — header tagline 'Inside the Future of Pro AV'
    was rendered in --brand-text-muted (#475569 slate) on a now-blue nav (#1D4ED8),
    near-invisible. Changed to rgba(255,255,255,0.85) so it reads against the blue
    while sitting visually subordinate to the wordmark above it.

(C) src/lib/articles/excerpt.ts — cleanExcerpt now strips HTML tags (<p>, <br>, <em>, etc.)
    and decodes common entities (&nbsp;, &amp;, &lt;, &gt;, &quot;, &#039;, &apos;) at
    render time, then collapses whitespace before the existing trailing-junk/last-word
    logic runs. Display-layer only — stored excerpts are not mutated, matching the
    'fix at the DISPLAY layer' instruction.
2026-06-03 13:20:32 +00:00
a1148e213b fix(contrast): white-on-white text bug — homepage 2x2 titles, .nav-link-bb, /news date inputs
The 2026 rebrand swept surface hexes from dark to light but left near-white text colors baked into components. Three approved fixes:

- FeaturedBento.tsx:255 — 2x2 grid card titles base color #e0e0e0 -> #0F172A (brand-navy). Hover stays #1D4ED8.
- tailwind.css .nav-link-bb — base color #cccccc -> var(--brand-text). Hover unchanged.
- ArticleFeed.tsx :375 + :385 — date input text #888 -> #475569 (brand-text-muted) and dropped [color-scheme:dark] so Chrome stops rendering the dark date-picker chrome on a now-white input.

Findings #2, #3, #6 in the audit were not touched — Ryan flagged them as unverified.
2026-06-03 13:11:57 +00:00
61363f81be fix(cache): no-store on / and /news so CF edge never sticks on the dynamic hero/feed
Next.js sets a long s-maxage on prerendered HTML even for routes flagged force-dynamic; Cloudflare then caches that response and ignores subsequent dynamic renders. Explicit Cache-Control + CDN-Cache-Control no-store on / and /news/* keeps the hero swap visible immediately after a DB change.
2026-06-03 12:56:27 +00:00
de82125999 feat(avbeat): brand-blue nav, navy card text, tagline rename, hide Industry News dates + Topics row, force-dynamic homepage
- Main nav background → brand-blue #1D4ED8 (sampled from logo emblem).
- Nav link text → white with darker-blue hover bg so contrast holds.
- Article card title fixed from #e0e0e0 to navy #0F172A (was unreadable on the new white surface).
- Industry News card date row removed.
- /news Topics filter chip row removed.
- /news article hover from stark #111 to light-blue #EFF6FF tint matching the brand.
- Tagline 'News & Intelligence for Pro AV, Live Production & Display Tech' renamed to 'Pro AV, Display and Live Production Tech' across Header, RSS feed, newsletter welcome template, root layout title, page title, OG/Twitter tags.
- Homepage page.tsx now has `export const dynamic = 'force-dynamic'` + `revalidate = 0` so the hero (FeaturedBentoFromDb) actually re-renders against the live supabase row (previously the page was being statically prerendered which masked DB-driven hero swaps).
2026-06-03 12:50:55 +00:00
8042024c4a feat(brand): AV BEAT 2026 rebrand — blue/navy/white identity
- New AvBeatLogo inline-SVG component (rounded-square emblem with
  forward-leaning AV monogram + horizontal beam detail) at
  src/components/AvBeatLogo.tsx, three variants (full/wordmark/icon).
- Header.tsx now uses the responsive AV BEAT logo lockup in the
  top-left, links to /, full lockup on desktop (emblem+wordmark+tagline),
  emblem+wordmark on tablet, emblem-only on mobile.
- Removed DualSpeedTicker (the 'RECENT FORUM POSTS' ticker strip);
  the news ticker / glow chrome does not match the new aesthetic and the
  forum row was the explicit removal target.
- Tailwind theme + globals.css now expose the AV BEAT 2026 palette as
  semantic tokens: --brand-blue (#1D4ED8), --brand-blue-bright (#38BDF8),
  --brand-navy (#0F172A), --brand-bg (#F8FAFC), --brand-surface, --brand-border,
  --brand-text, --brand-text-muted. Legacy --color-* aliases re-point to the
  new tokens so any inline-styled component re-themes for free.
- Site-wide hex sweep migrates 2,769 hardcoded color literals across 144
  files from the old dark-broadcast palette to the new tokens (orange
  -> blue, dark-brown -> white surface / navy text, cream -> navy).
- Layout body class flipped from 'bb-neon' to 'bg-brand-bg text-brand-text'
  so the dark glow chrome no longer leaks through the new light theme.
2026-06-03 12:07:18 +00:00
39d2bf9073 AV: recolor email templates (blue palette inside template literals + NAB/IBC marketing phrasing → AV palette + InfoComm/ISE) 2026-06-03 01:53:01 +00:00
4bcd841c53 AV deep sweep: account / news / marketplace placeholders + newsletter default tag + admin overview text + content-status & dashboard adops slug mappings (broadcast-beat → avbeat) 2026-06-03 00:50:46 +00:00
2876ac6ecd AV: precise recolor to spec — warm dark backgrounds (#1c1815/#231d18/#26201a/#15110e) + gold #F0A623 / orange #D85A30 accents + warm text (#FBEFE0/#C9BBA8/#8a7e6e). 138 component files + 36 token swaps. 2026-06-02 23:46:00 +00:00
01c1abf99d AV: remove Studio Hero banner (BB-specific creative not relevant to pro AV) 2026-06-02 23:37:59 +00:00
10ecf723dc AV launch: global color scheme to match logo — gold #ffb800 primary, red #d60701 secondary, warm dark bars (1a1208 / 161310) replacing the inherited BB blue palette. Swapped 21 root tokens + 112 component files. 2026-06-02 23:24:54 +00:00
54117c34d3 AV launch: transparent SVG logo (drop black bg rect) + Header logo 200% bigger (h-12→h-24, 320→640w, 48→96h) + rename Production Industry Manufacturers → AV Integration Manufacturers 2026-06-02 23:22:59 +00:00
8695b338d2 AV round 3: body copy on /about /technology /advertise /marketplace — broadcast→pro AV (about description, technology category descs, advertise H1+sample, marketplace category+meta) 2026-06-02 23:11:39 +00:00
1cb6e3ec01 AV round 2: per-page metadata on /advertise /about /about-team /show-coverage /gear /technology /forum — broadcast→pro AV 2026-06-02 23:05:03 +00:00
3dbe7545d8 AV: catch home-page/page.tsx own metadata + H1 + NewsletterSignup heading + marketplace title (layout-level sweep missed these per-page overrides) 2026-06-02 23:04:18 +00:00
b96c424476 AV launch: comprehensive copy sweep — broadcast/NAB-to-IBC → pro AV/InfoComm-to-ISE across layout/header/forum/marketplace/search/advertise/AI prompts/newsletter/RSS; bump header logo h-7→h-12 + footer h-6→h-9 2026-06-02 22:35:51 +00:00
9bb04fb373 AV launch: rewrite newsletter signup + home description for pro AV industry (InfoComm/ISE), not broadcast (NAB/IBC) 2026-06-02 22:32:57 +00:00
4fd6a88158 AV launch: swap header/footer/schema.org logo to AV Beat SVG + install devDeps so Coolify NODE_ENV=production does not strip autoprefixer 2026-06-02 21:20:36 +00:00
190 changed files with 5557 additions and 3785 deletions

4
.gitignore vendored
View File

@@ -39,4 +39,6 @@ jest-test-results.json
.idea/ .idea/
# Optional: if using TurboRepo or monorepo # Optional: if using TurboRepo or monorepo
turbo/ turbo/
# Legacy WP uploads served from Supabase/CDN, not git
public/wp-content/uploads/

View File

@@ -14,7 +14,7 @@ ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_GA_MEASUREMENT_ID=$NEXT_PUBLIC_GA_MEASUREMENT_ID ENV NEXT_PUBLIC_GA_MEASUREMENT_ID=$NEXT_PUBLIC_GA_MEASUREMENT_ID
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm install --include=dev
COPY . . COPY . .
RUN npm run build RUN npm run build
ENV NODE_ENV=production ENV NODE_ENV=production

View File

@@ -31,4 +31,16 @@ export const imageHosts = [
protocol: 'https', protocol: 'https',
hostname: 'www.avbeat.com', hostname: 'www.avbeat.com',
}, },
{
protocol: 'https',
hostname: 'broadcastbeat.com',
},
{
protocol: 'https',
hostname: 'www.broadcastbeat.com',
},
{
protocol: 'https',
hostname: 'advertising.relevantmediaproperties.com',
},
]; ];

View File

@@ -49,6 +49,23 @@ const nextConfig = {
{ key: 'Cache-Control', value: 'public, max-age=86400, stale-while-revalidate=604800' }, { key: 'Cache-Control', value: 'public, max-age=86400, stale-while-revalidate=604800' },
], ],
}, },
{
// Homepage and news routes must never be cached at edge — the hero
// and feed are DB-driven via force-dynamic, but Next.js sets a
// long s-maxage on prerendered HTML which CF then caches for a year.
source: '/',
headers: [
{ key: 'Cache-Control', value: 'no-store, must-revalidate' },
{ key: 'CDN-Cache-Control', value: 'no-store' },
],
},
{
source: '/news/:path*',
headers: [
{ key: 'Cache-Control', value: 'no-store, must-revalidate' },
{ key: 'CDN-Cache-Control', value: 'no-store' },
],
},
]; ];
}, },

View File

@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" preserveAspectRatio="xMidYMid slice">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0F172A"/>
<stop offset="50%" stop-color="#1E3A8A"/>
<stop offset="100%" stop-color="#0F172A"/>
</linearGradient>
<radialGradient id="glow" cx="50%" cy="50%" r="60%">
<stop offset="0%" stop-color="#1D4ED8" stop-opacity="0.18"/>
<stop offset="100%" stop-color="#1D4ED8" stop-opacity="0"/>
</radialGradient>
<linearGradient id="tile" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#2563EB"/>
<stop offset="100%" stop-color="#1E3A8A"/>
</linearGradient>
</defs>
<!-- Background gradient -->
<rect width="1200" height="630" fill="url(#bg)"/>
<rect width="1200" height="630" fill="url(#glow)"/>
<!-- Brand-mark tile + A monogram, centered. Same geometry as AnimatedLogo. -->
<g transform="translate(420 175)">
<!-- Rounded tile (280x280, rx 62) with the brand gradient -->
<rect x="0" y="0" width="280" height="280" rx="62" ry="62" fill="url(#tile)"/>
<!-- Faint inner ring so the tile reads on dark bg -->
<rect x="3" y="3" width="274" height="274" rx="59" ry="59" fill="none" stroke="rgba(255,255,255,0.20)" stroke-width="3"/>
<!-- A — two strokes: left+right legs and crossbar. Same path as AnimatedLogo at 4.4x scale. -->
<path d="M 62 220 L 140 60 L 218 220"
fill="none" stroke="#FFFFFF" stroke-width="22"
stroke-linecap="round" stroke-linejoin="round"/>
<path d="M 92 158 L 188 158"
fill="none" stroke="#FFFFFF" stroke-width="18"
stroke-linecap="round"/>
</g>
<!-- Wordmark + tagline -->
<g transform="translate(600 525)" text-anchor="middle" font-family="system-ui, -apple-system, 'Segoe UI', sans-serif">
<text y="0" font-size="56" font-weight="700" fill="#FFFFFF" letter-spacing="-0.012em">AV BEAT</text>
<text y="34" font-size="14" font-weight="500" fill="#CBD5E1" letter-spacing="0.22em">INSIDE THE FUTURE OF PRO AV</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,20 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" preserveAspectRatio="xMidYMid slice">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a1f2e"/>
<stop offset="50%" stop-color="#0d1018"/>
<stop offset="100%" stop-color="#080a10"/>
</linearGradient>
<radialGradient id="r" cx="50%" cy="50%" r="60%">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.10"/>
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="1200" height="630" fill="url(#g)"/>
<rect width="1200" height="630" fill="url(#r)"/>
<g transform="translate(600 315)" text-anchor="middle" font-family="system-ui, sans-serif">
<text y="0" font-size="48" font-weight="700" fill="#3b82f6" letter-spacing="0.05em">BROADCAST</text>
<text y="56" font-size="48" font-weight="700" fill="#cbd5e1" letter-spacing="0.05em">BEAT</text>
<text y="116" font-size="16" font-weight="500" fill="#64748b" letter-spacing="0.25em" text-transform="uppercase">NEWS · INTELLIGENCE · TECHNOLOGY</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 320" width="1200" height="320" role="img" aria-label="AV Beat — Pro AV, Live Production, and Display Tech">
<defs>
<filter id="haloGold" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="18"/></filter>
<filter id="haloRed" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="18"/></filter>
<filter id="glowGold" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="3.5"/></filter>
<filter id="glowRed" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="3.5"/></filter>
<filter id="glowSoft" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="1.6"/></filter>
<style>
@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@800;900&amp;family=Inter:wght@500;600&amp;display=swap');
.word { font-family:'Barlow Condensed','Oswald','Nimbus Sans Narrow','Inter','Helvetica Neue',Arial,sans-serif; font-weight:900; font-size:156px; letter-spacing:-0.012em; }
.tm { font-family:'Inter','Nimbus Sans Narrow','Helvetica Neue',Arial,sans-serif; font-weight:600; font-size:22px; }
.tag { font-family:'Inter','Nimbus Sans Narrow','Helvetica Neue',Arial,sans-serif; font-weight:500; font-size:20px; letter-spacing:0.34em; }
</style>
</defs>
<!-- NO background fill — transparent SVG so the header bg shows through -->
<!-- ─── AV (gold neon) ────────────────────────────────────────── -->
<g text-anchor="end">
<text class="word" x="500" y="195" fill="#ffb800" opacity="0.85" filter="url(#haloGold)">AV</text>
<text class="word" x="500" y="195" fill="#ffd25a" opacity="0.95" filter="url(#glowGold)">AV</text>
<text class="word" x="500" y="195" fill="#ffffff">AV</text>
</g>
<!-- ─── BEAT (red neon) ──────────────────────────────────────── -->
<g text-anchor="start">
<text class="word" x="530" y="195" fill="#d60701" opacity="0.85" filter="url(#haloRed)">BEAT</text>
<text class="word" x="530" y="195" fill="#ff4035" opacity="0.95" filter="url(#glowRed)">BEAT</text>
<text class="word" x="530" y="195" fill="#ffffff">BEAT</text>
</g>
<!-- ™ mark -->
<g>
<text class="tm" x="935" y="92" fill="#ffd45e" filter="url(#glowGold)"></text>
<text class="tm" x="935" y="92" fill="#ffffff"></text>
</g>
<!-- Waveform -->
<g>
<path d="M 70 230 L 440 230 L 460 230 L 480 210 L 500 252 L 520 164 L 540 286 L 560 210 L 580 238 L 600 230 L 1130 230" fill="none" stroke="#ffb800" stroke-width="6" stroke-linejoin="round" stroke-linecap="round" filter="url(#haloGold)" opacity="0.75"/>
<path d="M 70 230 L 440 230 L 460 230 L 480 210 L 500 252 L 520 164 L 540 286 L 560 210 L 580 238 L 600 230 L 1130 230" fill="none" stroke="#ffb800" stroke-width="3" stroke-linejoin="round" stroke-linecap="round" filter="url(#glowGold)"/>
<path d="M 70 230 L 440 230 L 460 230 L 480 210 L 500 252 L 520 164 L 540 286 L 560 210 L 580 238 L 600 230 L 1130 230" fill="none" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</g>
<!-- Tagline -->
<g text-anchor="middle">
<text class="tag" x="600" y="294" fill="#ffffff" opacity="0.85" filter="url(#glowSoft)">PRO AV · LIVE PRODUCTION · DISPLAY TECH</text>
<text class="tag" x="600" y="294" fill="#ffffff">PRO AV · LIVE PRODUCTION · DISPLAY TECH</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,103 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 320" width="1200" height="320" role="img" aria-label="Broadcast Beat — The Pulse of Production Intelligence"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 320" width="1200" height="320" role="img" aria-label="AV Beat — Pro AV, Live Production, and Display Tech">
<defs> <defs>
<!-- Background gradient: deep navy → near-black --> <filter id="haloGold" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="18"/></filter>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"> <filter id="haloRed" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="18"/></filter>
<stop offset="0%" stop-color="#050B1E"/> <filter id="glowGold" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="3.5"/></filter>
<stop offset="100%" stop-color="#020613"/> <filter id="glowRed" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="3.5"/></filter>
</linearGradient> <filter id="glowSoft" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="1.6"/></filter>
<!-- Wide soft halo (outer bloom) -->
<filter id="haloPink" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="18"/>
</filter>
<filter id="haloCyan" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="18"/>
</filter>
<!-- Tighter glow (inner neon) -->
<filter id="glowPink" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="3.5"/>
</filter>
<filter id="glowCyan" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="3.5"/>
</filter>
<!-- Tagline subtle glow -->
<filter id="glowSoft" x="-20%" y="-50%" width="140%" height="200%">
<feGaussianBlur stdDeviation="1.6"/>
</filter>
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@800;900&amp;family=Inter:wght@500;600&amp;display=swap'); @import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@800;900&amp;family=Inter:wght@500;600&amp;display=swap');
.word { .word { font-family:'Barlow Condensed','Oswald','Nimbus Sans Narrow','Inter','Helvetica Neue',Arial,sans-serif; font-weight:900; font-size:156px; letter-spacing:-0.012em; }
font-family: 'Barlow Condensed', 'Oswald', 'Nimbus Sans Narrow', 'Inter', 'Helvetica Neue', Arial, sans-serif; .tm { font-family:'Inter','Nimbus Sans Narrow','Helvetica Neue',Arial,sans-serif; font-weight:600; font-size:22px; }
font-weight: 900; .tag { font-family:'Inter','Nimbus Sans Narrow','Helvetica Neue',Arial,sans-serif; font-weight:500; font-size:20px; letter-spacing:0.34em; }
font-size: 118px;
letter-spacing: -0.012em;
}
.tm {
font-family: 'Inter', 'Nimbus Sans Narrow', 'Helvetica Neue', Arial, sans-serif;
font-weight: 600;
font-size: 22px;
}
.tag {
font-family: 'Inter', 'Nimbus Sans Narrow', 'Helvetica Neue', Arial, sans-serif;
font-weight: 500;
font-size: 20px;
letter-spacing: 0.34em;
}
</style> </style>
</defs> </defs>
<!-- Dark background --> <!-- NO background fill — transparent SVG so the header bg shows through -->
<rect width="1200" height="320" fill="url(#bg)"/>
<!-- Faint vignette + scanline accents (optional broadcast vibe) --> <!-- ─── AV (gold neon) ────────────────────────────────────────── -->
<rect width="1200" height="1" y="0" fill="#00d4ff" opacity="0.10"/>
<rect width="1200" height="1" y="319" fill="#ff2188" opacity="0.10"/>
<!-- ─── BROADCAST (pink/magenta neon) ─────────────────────────── -->
<g text-anchor="end"> <g text-anchor="end">
<!-- outer halo --> <text class="word" x="500" y="195" fill="#ffb800" opacity="0.85" filter="url(#haloGold)">AV</text>
<text class="word" x="595" y="180" fill="#ff2188" opacity="0.85" filter="url(#haloPink)">BROADCAST</text> <text class="word" x="500" y="195" fill="#ffd25a" opacity="0.95" filter="url(#glowGold)">AV</text>
<!-- mid glow --> <text class="word" x="500" y="195" fill="#ffffff">AV</text>
<text class="word" x="595" y="180" fill="#ff58a8" opacity="0.95" filter="url(#glowPink)">BROADCAST</text>
<!-- white core -->
<text class="word" x="595" y="180" fill="#ffffff">BROADCAST</text>
</g> </g>
<!-- ─── BEAT (cyan neon) ──────────────────────────────────────── --> <!-- ─── BEAT (red neon) ──────────────────────────────────────── -->
<g text-anchor="start"> <g text-anchor="start">
<text class="word" x="615" y="180" fill="#00d4ff" opacity="0.85" filter="url(#haloCyan)">BEAT</text> <text class="word" x="530" y="195" fill="#d60701" opacity="0.85" filter="url(#haloRed)">BEAT</text>
<text class="word" x="615" y="180" fill="#5ee5ff" opacity="0.95" filter="url(#glowCyan)">BEAT</text> <text class="word" x="530" y="195" fill="#ff4035" opacity="0.95" filter="url(#glowRed)">BEAT</text>
<text class="word" x="615" y="180" fill="#ffffff">BEAT</text> <text class="word" x="530" y="195" fill="#ffffff">BEAT</text>
</g> </g>
<!-- ™ mark near end of BEAT, upper right --> <!-- ™ mark -->
<g> <g>
<text class="tm" x="918" y="90" fill="#7feaff" filter="url(#glowCyan)"></text> <text class="tm" x="935" y="92" fill="#ffd45e" filter="url(#glowGold)"></text>
<text class="tm" x="918" y="90" fill="#ffffff"></text> <text class="tm" x="935" y="92" fill="#ffffff"></text>
</g> </g>
<!-- ─── Waveform / heartbeat pulse ────────────────────────────── --> <!-- Waveform -->
<g> <g>
<!-- Halo pass --> <path d="M 70 230 L 440 230 L 460 230 L 480 210 L 500 252 L 520 164 L 540 286 L 560 210 L 580 238 L 600 230 L 1130 230" fill="none" stroke="#ffb800" stroke-width="6" stroke-linejoin="round" stroke-linecap="round" filter="url(#haloGold)" opacity="0.75"/>
<path d="M 70 218 L 460 218 L 480 218 L 500 198 L 520 240 L 540 152 L 560 274 L 580 198 L 600 226 L 620 218 L 1130 218" <path d="M 70 230 L 440 230 L 460 230 L 480 210 L 500 252 L 520 164 L 540 286 L 560 210 L 580 238 L 600 230 L 1130 230" fill="none" stroke="#ffb800" stroke-width="3" stroke-linejoin="round" stroke-linecap="round" filter="url(#glowGold)"/>
fill="none" stroke="#00d4ff" stroke-width="6" stroke-linejoin="round" stroke-linecap="round" <path d="M 70 230 L 440 230 L 460 230 L 480 210 L 500 252 L 520 164 L 540 286 L 560 210 L 580 238 L 600 230 L 1130 230" fill="none" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
filter="url(#haloCyan)" opacity="0.75"/>
<!-- Mid stroke -->
<path d="M 70 218 L 460 218 L 480 218 L 500 198 L 520 240 L 540 152 L 560 274 L 580 198 L 600 226 L 620 218 L 1130 218"
fill="none" stroke="#00d4ff" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"
filter="url(#glowCyan)"/>
<!-- Crisp white core -->
<path d="M 70 218 L 460 218 L 480 218 L 500 198 L 520 240 L 540 152 L 560 274 L 580 198 L 600 226 L 620 218 L 1130 218"
fill="none" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round" stroke-linecap="round"/>
</g> </g>
<!-- ─── Tagline ───────────────────────────────────────────────── --> <!-- Tagline -->
<g text-anchor="middle"> <g text-anchor="middle">
<text class="tag" x="600" y="282" fill="#ffffff" opacity="0.85" filter="url(#glowSoft)">THE PULSE OF PRODUCTION INTELLIGENCE</text> <text class="tag" x="600" y="294" fill="#ffffff" opacity="0.85" filter="url(#glowSoft)">PRO AV · LIVE PRODUCTION · DISPLAY TECH</text>
<text class="tag" x="600" y="282" fill="#ffffff">THE PULSE OF PRODUCTION INTELLIGENCE</text> <text class="tag" x="600" y="294" fill="#ffffff">PRO AV · LIVE PRODUCTION · DISPLAY TECH</text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -8,7 +8,7 @@ const CONTACTS = [
{ {
title: "Editorial", title: "Editorial",
body: ( body: (
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:editor@avbeat.com">editor@avbeat.com</a> <a className="text-[#7DD3FC] hover:underline" href="mailto:editor@relevantmediaproperties.com">editor@relevantmediaproperties.com</a>
), ),
hint: "Story tips, corrections, interview requests.", hint: "Story tips, corrections, interview requests.",
}, },
@@ -17,7 +17,7 @@ const CONTACTS = [
body: ( body: (
<> <>
Route PR-firm submissions through{" "} Route PR-firm submissions through{" "}
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="https://distribution.relevantmediaproperties.com">distribution.relevantmediaproperties.com</a>. <a className="text-[#7DD3FC] hover:underline" href="https://distribution.relevantmediaproperties.com">distribution.relevantmediaproperties.com</a>.
</> </>
), ),
hint: "Routed submissions land in our editorial queue with provenance.", hint: "Routed submissions land in our editorial queue with provenance.",
@@ -26,8 +26,8 @@ const CONTACTS = [
title: "Advertising", title: "Advertising",
body: ( body: (
<> <>
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:advertising@avbeat.com">advertising@avbeat.com</a> see the{" "} <a className="text-[#7DD3FC] hover:underline" href="mailto:advertising@relevantmediaproperties.com">advertising@relevantmediaproperties.com</a> see the{" "}
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/advertise">media kit</a>. <a className="text-[#7DD3FC] hover:underline" href="https://relevantmediaproperties.com/av-beat---media-kit">media kit</a>.
</> </>
), ),
hint: "728×90, 300×600, 300×250 inventory; CTR + impression reports monthly.", hint: "728×90, 300×600, 300×250 inventory; CTR + impression reports monthly.",
@@ -37,7 +37,7 @@ const CONTACTS = [
body: ( body: (
<> <>
Ryan Salazar ·{" "} Ryan Salazar ·{" "}
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a> <a className="text-[#7DD3FC] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a>
</> </>
), ),
hint: "Partnerships, M&A, syndication, network membership.", hint: "Partnerships, M&A, syndication, network membership.",
@@ -48,27 +48,31 @@ export default function ContactPage() {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<Header /> <Header />
<div className="max-w-container mx-auto px-4 py-12 text-[#e5e7eb]"> <div className="max-w-container mx-auto px-4 py-12 text-[#0F172A]">
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10"> <div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
<main> <main>
<header className="mb-8 border-b border-[#252525] pb-6"> <header className="mb-8 border-b border-[#DCE6F2] pb-6">
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2"> <div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[#1D4ED8] mb-2">
AV Beat AV Beat
</div> </div>
<h1 className="font-serif text-4xl md:text-5xl font-bold tracking-tight">Contact</h1> <h1 className="font-display text-4xl md:text-5xl font-bold tracking-tight text-[#0F172A]">Contact</h1>
<p className="mt-3 text-[#9ca3af] text-base"> <p className="mt-3 text-[#475569] text-base">
Pitches, press, partnerships pick the right desk and we'll get back fast. Pitches, press, partnerships pick the right desk and we'll get back fast.
</p> </p>
</header> </header>
<ul className="grid grid-cols-1 md:grid-cols-2 gap-4"> <ul className="grid grid-cols-1 md:grid-cols-2 gap-4">
{CONTACTS.map((c) => ( {CONTACTS.map((c) => (
<li key={c.title} className="rounded border border-[#252525] bg-[#0b0f17] p-5 hover:border-[var(--color-text-info,#60a5fa)]/60 transition-colors"> // Dark contact card with a brand-gradient top accent (animated
<div className="text-[10px] font-mono uppercase tracking-[0.18em] text-[var(--color-text-info,#60a5fa)] mb-2"> // sheen). Card body text is explicitly light because the parent
// wrapper is now dark-on-light at the page level.
<li key={c.title} className="relative overflow-hidden rounded border border-[#1a1f2a] bg-[#0b0f17] p-5 hover:border-[#1D4ED8]/60 transition-colors">
<span className="brand-gradient absolute top-0 left-0 right-0 h-1" aria-hidden />
<div className="text-[10px] font-mono uppercase tracking-[0.18em] text-[#93C5FD] mb-2">
{c.title} {c.title}
</div> </div>
<div className="text-[15px] leading-relaxed">{c.body}</div> <div className="text-[15px] leading-relaxed text-[#e5e7eb]">{c.body}</div>
<div className="mt-3 text-xs text-[#6b7280]">{c.hint}</div> <div className="mt-3 text-xs text-[#94a3b8]">{c.hint}</div>
</li> </li>
))} ))}
</ul> </ul>

View File

@@ -4,23 +4,23 @@ import SidebarAdStack from "@/components/SidebarAdStack";
export const metadata = { export const metadata = {
title: "About AV Beat", title: "About AV Beat",
description: "AV Beat is an independent industry publication covering broadcast, production, and post-production technology.", description: "AV Beat is an independent industry publication covering pro AV, live production, and display tech.",
}; };
export default function AboutPage() { export default function AboutPage() {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<Header /> <Header />
<div className="max-w-container mx-auto px-4 py-12 text-[#e5e7eb]"> <div className="max-w-container mx-auto px-4 py-12 text-[#1e293b]">
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10"> <div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
<main> <main>
<header className="mb-8 border-b border-[#252525] pb-6"> <header className="mb-8 border-b border-[#DCE6F2] pb-6">
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2"> <div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2">
AV Beat AV Beat
</div> </div>
<h1 className="font-serif text-4xl md:text-5xl font-bold tracking-tight">About</h1> <h1 className="font-display text-4xl md:text-5xl font-bold tracking-tight">About</h1>
<p className="mt-3 text-[#9ca3af] text-base max-w-2xl"> <p className="mt-3 text-[#475569] text-base max-w-2xl">
An independent trade publication covering broadcast, live production, streaming, and post-production technology. An independent trade publication covering pro AV, live production, display tech, and signal-flow technology.
</p> </p>
</header> </header>
@@ -30,7 +30,7 @@ export default function AboutPage() {
<Stat label="Stories archived" value="29k+" /> <Stat label="Stories archived" value="29k+" />
</div> </div>
<section className="space-y-6 text-[#d1d5db] text-[15px] leading-relaxed"> <section className="space-y-6 text-[#1e293b] text-[15px] leading-relaxed">
<Block title="What we cover"> <Block title="What we cover">
AV Beat reports on the equipment, services, and people AV Beat reports on the equipment, services, and people
that move the professional video industry forward. We follow that move the professional video industry forward. We follow
@@ -41,7 +41,7 @@ export default function AboutPage() {
<Block title="Where we go"> <Block title="Where we go">
We cover the major industry shows in person and remotely NAB We cover the major industry shows in person and remotely NAB
Show, IBC, BroadcastAsia, MPTS, InfoComm, and more track Show InfoComm, ISE, NAB Show, IBC, and more track
product launches and acquisitions year-round, and publish product launches and acquisitions year-round, and publish
interviews with the engineers, post supervisors, and executives interviews with the engineers, post supervisors, and executives
building the next generation of workflows. building the next generation of workflows.
@@ -51,14 +51,13 @@ export default function AboutPage() {
Editorial decisions are made independently of advertising. We Editorial decisions are made independently of advertising. We
do not accept payment for editorial coverage. Vendor-supplied do not accept payment for editorial coverage. Vendor-supplied
press releases routed through our distribution platform are press releases routed through our distribution platform are
rewritten by our staff or AI assistants according to our rewritten by our editorial team to our house style guide;
editorial style guide; the original submission is preserved on the original submission is preserved on file for provenance.
file. Our AI personas are clearly labeled wherever they appear.
</Block> </Block>
<Block title="Get involved"> <Block title="Get involved">
For editorial pitches: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:editor@avbeat.com">editor@avbeat.com</a>. For editorial pitches: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:editor@avbeat.com">editor@avbeat.com</a>.
{" "}For advertising: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/advertise">see the media kit</a>. {" "}For advertising: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="https://relevantmediaproperties.com/av-beat---media-kit" target="_blank" rel="noopener noreferrer">see the media kit</a>.
{" "}For our team: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/team">meet the editorial team</a>. {" "}For our team: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/team">meet the editorial team</a>.
</Block> </Block>
</section> </section>
@@ -78,10 +77,14 @@ export default function AboutPage() {
} }
function Stat({ label, value }: { label: string; value: string }) { function Stat({ label, value }: { label: string; value: string }) {
// Brand-gradient top accent (animated sheen) gives the stat tile its
// on-brand identity without redoing the layout. The tile itself stays
// white-on-light-border for legibility.
return ( return (
<div className="rounded border border-[#252525] bg-[#0b0f17] p-4"> <div className="relative overflow-hidden rounded border border-[#DCE6F2] bg-[#FFFFFF] p-4">
<div className="font-serif text-3xl text-white">{value}</div> <span className="brand-gradient absolute top-0 left-0 right-0 h-1" aria-hidden />
<div className="mt-1 text-[10px] font-mono uppercase tracking-[0.16em] text-[#6b7280]"> <div className="font-display text-3xl font-bold text-[#0F172A]">{value}</div>
<div className="mt-1 text-[10px] font-mono uppercase tracking-[0.16em] text-[#475569]">
{label} {label}
</div> </div>
</div> </div>
@@ -90,7 +93,7 @@ function Stat({ label, value }: { label: string; value: string }) {
function Block({ title, children }: { title: string; children: React.ReactNode }) { function Block({ title, children }: { title: string; children: React.ReactNode }) {
return ( return (
<div className="rounded border border-[#1e1e1e] bg-[#0b0f17]/70 p-5"> <div className="rounded border border-[#DCE6F2] bg-[#FFFFFF] p-5">
<h2 className="font-mono text-xs uppercase tracking-[0.18em] text-[var(--color-text-info,#60a5fa)] mb-2"> <h2 className="font-mono text-xs uppercase tracking-[0.18em] text-[var(--color-text-info,#60a5fa)] mb-2">
{title} {title}
</h2> </h2>

View File

@@ -9,7 +9,7 @@ export const revalidate = 600;
export const metadata = { export const metadata = {
title: "Our team — AV Beat", title: "Our team — AV Beat",
description: "Meet the AV Beat team — editorial staff plus accounts and support, covering broadcast, streaming, and post-production.", description: "Meet the AV Beat team — editorial staff plus accounts and support, covering pro AV, live production, and display tech.",
}; };
interface Persona { interface Persona {
@@ -44,7 +44,6 @@ export default async function TeamPage() {
.select("slug, name, bio, beat, avatar_url") .select("slug, name, bio, beat, avatar_url")
.eq("active", true); .eq("active", true);
const raw = (data || []) as Persona[]; const raw = (data || []) as Persona[];
// Ryan Salazar pinned first as Founder / Editor-in-Chief; rest alphabetical.
const team = [ const team = [
...raw.filter((p) => p.slug === "ryan-salazar"), ...raw.filter((p) => p.slug === "ryan-salazar"),
...raw ...raw
@@ -52,8 +51,6 @@ export default async function TeamPage() {
.sort((a, b) => a.name.localeCompare(b.name)), .sort((a, b) => a.name.localeCompare(b.name)),
]; ];
// Operations staff — pulsedesk personas (accounts receivable, support).
// Only the company-wide RMP roles, not property-specific auto-attendants.
const sbOps = createClient( const sbOps = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
@@ -67,18 +64,15 @@ export default async function TeamPage() {
const ops = (opsData || []) as OpsPersona[]; const ops = (opsData || []) as OpsPersona[];
const displayBeat = (p: Persona) => const displayBeat = (p: Persona) =>
p.slug === "ryan-salazar" ? "FOUNDER / EDITOR-IN-CHIEF" : p.beat?.replace(/-/g, " "); p.slug === "ryan-salazar" ? "Founder / Editor-in-Chief" : p.beat?.replace(/-/g, " ") ?? "";
const initials = (name: string) => const formatPhone = (raw: string): string => {
name const digits = raw.replace(/[^0-9]/g, "");
.split(" ") const local = digits.startsWith("1") && digits.length === 11 ? digits.slice(1) : digits;
.filter(Boolean) if (local.length !== 10) return raw;
.slice(0, 2) return `(${local.slice(0, 3)}) ${local.slice(3, 6)}-${local.slice(6)}`;
.map((w) => w.charAt(0).toUpperCase()) };
.join("");
// Derive @avbeat.com email from the persona slug. Real mailboxes
// are set up centrally; this is the authoring address surfaced to readers.
const editorialEmail = (p: Persona): { user: string; domain: string } | null => { const editorialEmail = (p: Persona): { user: string; domain: string } | null => {
if (!p.slug) return null; if (!p.slug) return null;
return { user: p.slug.replace(/-/g, "."), domain: "avbeat.com" }; return { user: p.slug.replace(/-/g, "."), domain: "avbeat.com" };
@@ -90,167 +84,178 @@ export default async function TeamPage() {
return { user: email.slice(0, at), domain: email.slice(at + 1) }; return { user: email.slice(0, at), domain: email.slice(at + 1) };
}; };
const tableWrap = "overflow-x-auto rounded-xl border border-[#1a1f2a] bg-[#0b0f17]";
const tableClass = "w-full min-w-[640px] text-sm text-left border-collapse";
const thClass =
"px-4 py-3 text-[10px] font-mono uppercase tracking-[0.14em] text-[#93C5FD] bg-[#141a25] border-b border-[#1a1f2a] whitespace-nowrap";
const tdClass = "px-4 py-4 align-top border-b border-[#1a1f2a] text-[#aab1bd]";
const trHover = "hover:bg-[#111827]/60 transition-colors";
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<Header /> <Header />
<div className="max-w-container mx-auto px-4 py-12 text-[#e5e7eb]"> <div className="max-w-container mx-auto px-4 py-12 text-[#0F172A]">
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10"> <div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
<main> <main>
{/* Hero */} <header className="brand-gradient rounded-xl mb-10">
<header className="relative overflow-hidden rounded-xl mb-10 border border-[#1a1f2a] bg-gradient-to-br from-[#0d1118] via-[#0b0f17] to-[#0d1118]"> <div className="px-6 py-10 sm:px-10 sm:py-14">
<div className="absolute inset-0 opacity-[0.04] bg-[radial-gradient(ellipse_at_top_right,#d60701,transparent_60%)]" aria-hidden /> <div className="inline-flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.22em] text-white/85 mb-3">
<div className="relative px-6 py-10 sm:px-10 sm:py-14"> <span className="w-6 h-px bg-white/60" />
<div className="inline-flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.22em] text-[#ff5a55] mb-3">
<span className="w-6 h-px bg-[#d60701]" />
AV Beat &middot; editorial AV Beat &middot; editorial
</div> </div>
<h1 className="font-serif text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight"> <h1 className="font-display text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight text-white">
The team behind every byline The team behind every byline
</h1> </h1>
<p className="mt-4 text-[#9ca3af] text-base md:text-lg max-w-2xl leading-relaxed"> <p className="mt-4 text-white/80 text-base md:text-lg max-w-2xl leading-relaxed">
Beats covered by trained AI personas working under house style, plus the The writers, editors, and operators behind the AV Beat newsroom
business and support people who keep things moving. covering pro AV, live production, and display tech every day.
</p> </p>
</div> </div>
</header> </header>
{/* Editorial team — three-col grid on desktop, larger portraits */}
<section className="mb-14"> <section className="mb-14">
<div className="flex items-baseline justify-between mb-5"> <div className="flex items-baseline justify-between mb-5">
<h2 className="font-serif text-2xl md:text-3xl font-bold tracking-tight">Editorial</h2> <h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight">Editorial</h2>
<span className="text-[10px] font-mono uppercase tracking-[0.18em] text-[#6c7686]"> <span className="text-[10px] font-mono uppercase tracking-[0.18em] text-[#6c7686]">
{team.length} {team.length === 1 ? "writer" : "writers"} {team.length} {team.length === 1 ? "writer" : "writers"}
</span> </span>
</div> </div>
<ul className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5"> <div className={tableWrap}>
{team.map((p) => { <table className={tableClass}>
const e = editorialEmail(p); <thead>
return ( <tr>
<li <th className={thClass}>Name</th>
key={p.slug} <th className={thClass}>Role</th>
className="group relative rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 hover:border-[#d60701]/50 hover:shadow-[0_0_0_1px_rgba(214,7,1,0.15)] transition-all" <th className={thClass}>Focus</th>
> <th className={thClass}>Contact</th>
{/* Top accent */} </tr>
<div className="absolute top-0 left-5 right-5 h-px bg-gradient-to-r from-transparent via-[#d60701]/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" aria-hidden /> </thead>
<tbody>
<div className="flex items-start gap-4"> {team.map((p) => {
{p.avatar_url ? ( const e = editorialEmail(p);
<a href={`/authors/${p.slug}`} className="flex-shrink-0"> return (
<img <tr key={p.slug} className={trHover}>
src={p.avatar_url} <td className={tdClass}>
alt={p.name} <a
width={96} href={`/authors/${p.slug}`}
height={96} className="font-display text-base font-bold text-white hover:text-[#7DD3FC] hover:underline"
className="w-24 h-24 rounded-full border-2 border-[#1a1f2a] group-hover:border-[#d60701]/40 transition-colors object-cover" >
/> {p.name}
</a> </a>
) : ( </td>
<div className="w-24 h-24 rounded-full border-2 border-[#1a1f2a] bg-[#141a25] flex items-center justify-center font-serif font-bold text-2xl text-[#6c7686] flex-shrink-0"> <td className={tdClass}>
{initials(p.name)} {(p.beat || p.slug === "ryan-salazar") && (
</div> <span className="inline-block px-2 py-0.5 rounded-sm bg-[#1D4ED8]/15 border border-[#1D4ED8]/35 text-[10px] font-mono uppercase tracking-[0.12em] text-[#93C5FD]">
)} {displayBeat(p)}
<div className="flex-1 min-w-0"> </span>
<a href={`/authors/${p.slug}`} className="block hover:underline"> )}
<h3 className="font-serif text-xl font-bold leading-tight">{p.name}</h3> </td>
</a> <td className={`${tdClass} max-w-md`}>
{(p.beat || p.slug === "ryan-salazar") && ( {p.bio ? (
<div className="inline-block mt-1.5 px-2 py-0.5 rounded-sm bg-[#d60701]/10 border border-[#d60701]/25 text-[10px] font-mono uppercase tracking-[0.12em] text-[#ff6b66]"> <p className="leading-relaxed">{p.bio}</p>
{displayBeat(p)} ) : (
<span className="text-[#6c7686]"></span>
)}
</td>
<td className={tdClass}>
<div className="space-y-2">
{e && (
<ObfuscatedEmail
user={e.user}
domain={e.domain}
className="text-[#aab1bd] hover:text-[#7DD3FC] font-mono block"
/>
)}
<a
href={`/authors/${p.slug}`}
className="inline-block text-[10px] font-mono uppercase tracking-[0.12em] text-[#6c7686] hover:text-[#7DD3FC]"
>
Profile
</a>
</div> </div>
)} </td>
</div> </tr>
</div> );
})}
{p.bio && ( </tbody>
<p className="text-sm text-[#aab1bd] mt-4 leading-relaxed line-clamp-4"> </table>
{p.bio} </div>
</p>
)}
{e && (
<div className="mt-4 pt-4 border-t border-[#1a1f2a] flex items-center justify-between text-xs">
<ObfuscatedEmail
user={e.user}
domain={e.domain}
className="text-[#aab1bd] hover:text-[#ff6b66] font-mono"
/>
<a
href={`/authors/${p.slug}`}
className="text-[10px] font-mono uppercase tracking-[0.12em] text-[#6c7686] hover:text-[#ff6b66]"
>
View
</a>
</div>
)}
</li>
);
})}
</ul>
</section> </section>
{/* Operations — denser layout */}
{ops.length > 0 && ( {ops.length > 0 && (
<section> <section>
<div className="flex items-baseline justify-between mb-5"> <div className="flex items-baseline justify-between mb-5">
<h2 className="font-serif text-2xl md:text-3xl font-bold tracking-tight"> <h2 className="font-display text-2xl md:text-3xl font-bold tracking-tight">
Accounts &amp; support Accounts &amp; support
</h2> </h2>
<span className="text-[10px] font-mono uppercase tracking-[0.18em] text-[#6c7686]"> <span className="text-[10px] font-mono uppercase tracking-[0.18em] text-[#6c7686]">
{ops.length} {ops.length === 1 ? "contact" : "contacts"} {ops.length} {ops.length === 1 ? "contact" : "contacts"}
</span> </span>
</div> </div>
<p className="text-[#9ca3af] text-sm mb-5 max-w-2xl"> <p className="text-[#475569] text-sm mb-5 max-w-2xl">
For billing, accounts receivable, vendor onboarding, sales, and For billing, accounts receivable, vendor onboarding, sales, and
general support questions these are the people to reach. general support questions these are the people to reach.
</p> </p>
<ul className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"> <div className={tableWrap}>
{ops.map((p) => ( <table className={tableClass}>
<li <thead>
key={p.id} <tr>
className="rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 flex gap-4 hover:border-[#d60701]/40 transition-colors" <th className={thClass}>Name</th>
> <th className={thClass}>Role</th>
{p.avatar_url ? ( <th className={thClass}>Email</th>
<img <th className={thClass}>Phone</th>
src={p.avatar_url} </tr>
alt={p.display_name} </thead>
width={80} <tbody>
height={80} {ops.map((p) => (
className="w-20 h-20 rounded-full border-2 border-[#1a1f2a] flex-shrink-0 object-cover" <tr key={p.id} className={trHover}>
/> <td className={tdClass}>
) : ( <span className="font-display text-base font-bold text-white">
<div className="w-20 h-20 rounded-full border-2 border-[#1a1f2a] flex-shrink-0 bg-[#141a25] flex items-center justify-center font-serif font-bold text-xl text-[#6c7686]"> {p.display_name}
{initials(p.display_name)} </span>
</div> </td>
)} <td className={tdClass}>
<div className="flex-1 min-w-0"> <span className="inline-block px-2 py-0.5 rounded-sm bg-[#1D4ED8]/15 border border-[#1D4ED8]/35 text-[10px] font-mono uppercase tracking-[0.12em] text-[#93C5FD]">
<h3 className="font-serif text-lg font-bold leading-tight">{p.display_name}</h3> {p.role}
<div className="inline-block mt-1 px-2 py-0.5 rounded-sm bg-[#1a2535] border border-[#3b82f6]/30 text-[10px] font-mono uppercase tracking-[0.12em] text-[#7ba0ff]"> </span>
{p.role} </td>
</div> <td className={tdClass}>
<div className="text-xs text-[#aab1bd] mt-3 space-y-1"> {p.email ? (
{p.email && (() => { (() => {
const e = splitEmail(p.email); const e = splitEmail(p.email!);
if (!e) return null; if (!e) return null;
return ( return (
<div className="font-mono"> <ObfuscatedEmail
<ObfuscatedEmail user={e.user} domain={e.domain} className="hover:text-[#ff6b66]" /> user={e.user}
</div> domain={e.domain}
); className="text-[#aab1bd] hover:text-[#7DD3FC] font-mono block"
})()} />
{p.phone && ( );
<div> })()
<a href={`tel:${p.phone.replace(/[^\d+]/g, '')}`} className="hover:text-[#ff6b66] font-mono"> ) : (
{p.phone}{p.extension ? ` ext. ${p.extension}` : ''} <span className="text-[#6c7686]"></span>
)}
</td>
<td className={tdClass}>
{p.phone ? (
<a
href={`tel:${p.phone.replace(/[^\d+]/g, "")}${p.extension ? "," + p.extension : ""}`}
className="hover:text-[#7DD3FC] font-mono text-[#aab1bd]"
>
{formatPhone(p.phone)}
{p.extension ? `, Ext. ${p.extension}` : ""}
</a> </a>
</div> ) : (
)} <span className="text-[#6c7686]"></span>
</div> )}
</div> </td>
</li> </tr>
))} ))}
</ul> </tbody>
</table>
</div>
</section> </section>
)} )}
</main> </main>
@@ -265,4 +270,4 @@ export default async function TeamPage() {
<Footer /> <Footer />
</div> </div>
); );
} }

View File

@@ -284,7 +284,7 @@ export default function AccountPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#111111]"> <div className="min-h-screen bg-[#F8FAFC]">
<Header /> <Header />
<div className="max-w-container mx-auto px-4 py-12"> <div className="max-w-container mx-auto px-4 py-12">
<div className="max-w-3xl mx-auto space-y-4"> <div className="max-w-3xl mx-auto space-y-4">
@@ -298,21 +298,21 @@ export default function AccountPage() {
} }
return ( return (
<div className="min-h-screen bg-[#111111]"> <div className="min-h-screen bg-[#F8FAFC]">
<Header /> <Header />
{/* Toast */} {/* Toast */}
{toast && ( {toast && (
<div <div
className={`fixed bottom-6 left-1/2 -translate-x-1/2 z-50 border font-body text-sm px-5 py-3 rounded-sm shadow-lg flex items-center gap-2 animate-fade-in ${ className={`fixed bottom-6 left-1/2 -translate-x-1/2 z-50 border font-body text-sm px-5 py-3 rounded-sm shadow-lg flex items-center gap-2 animate-fade-in ${
toast.type === "success" ?"bg-[#1a2535] border-[#3b82f6] text-[#e0e0e0]" :"bg-[#1a0a0a] border-[#cc0000] text-[#e0e0e0]" toast.type === "success" ?"bg-[#FFFFFF] border-[#1D4ED8] text-[#e0e0e0]" :"bg-[#1a0a0a] border-[#1D4ED8] text-[#e0e0e0]"
}`}> }`}>
{toast.type === "success" ? ( {toast.type === "success" ? (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2.5" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1D4ED8" strokeWidth="2.5" aria-hidden="true">
<polyline points="20 6 9 17 4 12" /> <polyline points="20 6 9 17 4 12" />
</svg> </svg>
) : ( ) : (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#cc0000" strokeWidth="2.5" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1D4ED8" strokeWidth="2.5" aria-hidden="true">
<circle cx="12" cy="12" r="10" /><line x1="15" y1="9" x2="9" y2="15" /><line x1="9" y1="9" x2="15" y2="15" /> <circle cx="12" cy="12" r="10" /><line x1="15" y1="9" x2="9" y2="15" /><line x1="9" y1="9" x2="15" y2="15" />
</svg> </svg>
)} )}
@@ -321,10 +321,10 @@ export default function AccountPage() {
)} )}
{/* Breadcrumb */} {/* Breadcrumb */}
<div className="bg-[#0d0d0d] border-b border-[#1e1e1e]"> <div className="bg-[#F8FAFC] border-b border-[#1e1e1e]">
<div className="max-w-container mx-auto px-4 py-2.5"> <div className="max-w-container mx-auto px-4 py-2.5">
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-xs font-body text-[#555]"> <nav aria-label="Breadcrumb" className="flex items-center gap-2 text-xs font-body text-[#555]">
<Link href="/home-page" className="hover:text-[#3b82f6] transition-colors">Home</Link> <Link href="/home-page" className="hover:text-[#1D4ED8] transition-colors">Home</Link>
<span aria-hidden="true">/</span> <span aria-hidden="true">/</span>
<span className="text-[#888]">Account</span> <span className="text-[#888]">Account</span>
</nav> </nav>
@@ -336,25 +336,25 @@ export default function AccountPage() {
{/* Page Header */} {/* Page Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div> <div>
<h1 className="font-heading text-2xl font-bold text-[#e8e8e8]">Account Settings</h1> <h1 className="font-heading text-2xl font-bold text-[#0F172A]">Account Settings</h1>
<p className="font-body text-sm text-[#666] mt-1">{user?.email}</p> <p className="font-body text-sm text-[#666] mt-1">{user?.email}</p>
</div> </div>
<button <button
onClick={handleSignOut} onClick={handleSignOut}
className="font-body text-xs font-bold uppercase tracking-wide text-[#888] hover:text-[#cc0000] border border-[#2a2a2a] hover:border-[#cc0000] px-4 py-2 rounded-sm transition-colors"> className="font-body text-xs font-bold uppercase tracking-wide text-[#888] hover:text-[#1D4ED8] border border-[#DCE6F2] hover:border-[#1D4ED8] px-4 py-2 rounded-sm transition-colors">
Sign Out Sign Out
</button> </button>
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex border-b border-[#2a2a2a] mb-6 overflow-x-auto scrollbar-none"> <div className="flex border-b border-[#DCE6F2] mb-6 overflow-x-auto scrollbar-none">
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`font-body text-sm font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors whitespace-nowrap ${ className={`font-body text-sm font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors whitespace-nowrap ${
activeTab === tab.id activeTab === tab.id
? "border-[#3b82f6] text-[#3b82f6]" ? "border-[#1D4ED8] text-[#1D4ED8]"
: "border-transparent text-[#666] hover:text-[#aaa]" : "border-transparent text-[#666] hover:text-[#aaa]"
}`}> }`}>
{tab.label} {tab.label}
@@ -365,8 +365,8 @@ export default function AccountPage() {
{/* Profile Tab */} {/* Profile Tab */}
{activeTab === "profile" && ( {activeTab === "profile" && (
<form onSubmit={handleSaveProfile} className="space-y-5"> <form onSubmit={handleSaveProfile} className="space-y-5">
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-6">
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#DCE6F2]">
Public Profile Public Profile
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
@@ -379,7 +379,7 @@ export default function AccountPage() {
value={fullName} value={fullName}
onChange={(e) => setFullName(e.target.value)} onChange={(e) => setFullName(e.target.value)}
placeholder="Your full name" placeholder="Your full name"
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#1D4ED8] focus:ring-1 focus:ring-[#1D4ED8] transition-colors placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -391,7 +391,7 @@ export default function AccountPage() {
onChange={(e) => setBio(e.target.value)} onChange={(e) => setBio(e.target.value)}
placeholder="Tell us a bit about yourself..." placeholder="Tell us a bit about yourself..."
rows={3} rows={3}
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444] resize-none" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#1D4ED8] focus:ring-1 focus:ring-[#1D4ED8] transition-colors placeholder-[#444] resize-none"
/> />
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@@ -404,7 +404,7 @@ export default function AccountPage() {
value={website} value={website}
onChange={(e) => setWebsite(e.target.value)} onChange={(e) => setWebsite(e.target.value)}
placeholder="https://yoursite.com" placeholder="https://yoursite.com"
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#1D4ED8] focus:ring-1 focus:ring-[#1D4ED8] transition-colors placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -416,15 +416,15 @@ export default function AccountPage() {
value={location} value={location}
onChange={(e) => setLocation(e.target.value)} onChange={(e) => setLocation(e.target.value)}
placeholder="City, Country" placeholder="City, Country"
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#1D4ED8] focus:ring-1 focus:ring-[#1D4ED8] transition-colors placeholder-[#444]"
/> />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-6">
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-3 pb-3 border-b border-[#2a2a2a]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-3 pb-3 border-b border-[#DCE6F2]">
Account Info Account Info
</h2> </h2>
<div> <div>
@@ -435,7 +435,7 @@ export default function AccountPage() {
type="email" type="email"
value={user?.email || ""} value={user?.email || ""}
disabled disabled
className="w-full bg-[#0d0d0d] border border-[#222] text-[#555] font-body text-sm px-3 py-2.5 rounded-sm cursor-not-allowed" className="w-full bg-[#F8FAFC] border border-[#222] text-[#555] font-body text-sm px-3 py-2.5 rounded-sm cursor-not-allowed"
/> />
<p className="font-body text-xs text-[#555] mt-1.5">Email cannot be changed here.</p> <p className="font-body text-xs text-[#555] mt-1.5">Email cannot be changed here.</p>
</div> </div>
@@ -445,7 +445,7 @@ export default function AccountPage() {
<button <button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body text-xs font-bold uppercase tracking-wide px-6 py-2.5 rounded-sm transition-colors disabled:opacity-60 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]"> className="bg-[#1D4ED8] hover:bg-[#1E3A8A] text-white font-body text-xs font-bold uppercase tracking-wide px-6 py-2.5 rounded-sm transition-colors disabled:opacity-60 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#1D4ED8]">
{saving ? "Saving..." : "Save Profile"} {saving ? "Saving..." : "Save Profile"}
</button> </button>
</div> </div>
@@ -456,8 +456,8 @@ export default function AccountPage() {
{activeTab === "security" && ( {activeTab === "security" && (
<div className="space-y-5"> <div className="space-y-5">
<form onSubmit={handleChangePassword}> <form onSubmit={handleChangePassword}>
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-6">
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#DCE6F2]">
Change Password Change Password
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
@@ -471,7 +471,7 @@ export default function AccountPage() {
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter new password" placeholder="Enter new password"
minLength={6} minLength={6}
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#1D4ED8] focus:ring-1 focus:ring-[#1D4ED8] transition-colors placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -484,7 +484,7 @@ export default function AccountPage() {
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm new password" placeholder="Confirm new password"
minLength={6} minLength={6}
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#1D4ED8] focus:ring-1 focus:ring-[#1D4ED8] transition-colors placeholder-[#444]"
/> />
</div> </div>
</div> </div>
@@ -492,7 +492,7 @@ export default function AccountPage() {
<button <button
type="submit" type="submit"
disabled={changingPassword || !newPassword || !confirmPassword} disabled={changingPassword || !newPassword || !confirmPassword}
className="bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body text-xs font-bold uppercase tracking-wide px-6 py-2.5 rounded-sm transition-colors disabled:opacity-60 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]"> className="bg-[#1D4ED8] hover:bg-[#1E3A8A] text-white font-body text-xs font-bold uppercase tracking-wide px-6 py-2.5 rounded-sm transition-colors disabled:opacity-60 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#1D4ED8]">
{changingPassword ? "Updating..." : "Update Password"} {changingPassword ? "Updating..." : "Update Password"}
</button> </button>
</div> </div>
@@ -506,7 +506,7 @@ export default function AccountPage() {
</p> </p>
<button <button
onClick={handleSignOut} onClick={handleSignOut}
className="font-body text-xs font-bold uppercase tracking-wide text-[#cc0000] border border-[#cc0000]/40 hover:border-[#cc0000] hover:bg-[#cc0000]/10 px-5 py-2 rounded-sm transition-colors"> className="font-body text-xs font-bold uppercase tracking-wide text-[#1D4ED8] border border-[#1D4ED8]/40 hover:border-[#1D4ED8] hover:bg-[#1D4ED8]/10 px-5 py-2 rounded-sm transition-colors">
Sign Out of All Sessions Sign Out of All Sessions
</button> </button>
</div> </div>
@@ -520,12 +520,12 @@ export default function AccountPage() {
<h2 className="font-heading text-base font-bold text-[#e0e0e0]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0]">
Saved Articles Saved Articles
{bookmarks.length > 0 && ( {bookmarks.length > 0 && (
<span className="ml-2 font-body text-xs text-[#666] bg-[#1a1a1a] border border-[#2a2a2a] px-2 py-0.5 rounded-sm"> <span className="ml-2 font-body text-xs text-[#666] bg-[#FFFFFF] border border-[#DCE6F2] px-2 py-0.5 rounded-sm">
{bookmarks.length} {bookmarks.length}
</span> </span>
)} )}
</h2> </h2>
<Link href="/reading-list" className="font-body text-xs font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors"> <Link href="/reading-list" className="font-body text-xs font-bold uppercase tracking-wide text-[#1D4ED8] hover:text-[#1E3A8A] transition-colors">
Full List Full List
</Link> </Link>
</div> </div>
@@ -533,37 +533,37 @@ export default function AccountPage() {
{bookmarksLoading ? ( {bookmarksLoading ? (
<div className="space-y-3"> <div className="space-y-3">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<div key={i} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 animate-pulse"> <div key={i} className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-4 animate-pulse">
<div className="flex gap-3"> <div className="flex gap-3">
<div className="w-20 h-14 bg-[#252525] rounded-sm flex-shrink-0" /> <div className="w-20 h-14 bg-[#DCE6F2] rounded-sm flex-shrink-0" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<div className="h-3 bg-[#252525] rounded w-1/4" /> <div className="h-3 bg-[#DCE6F2] rounded w-1/4" />
<div className="h-4 bg-[#252525] rounded w-3/4" /> <div className="h-4 bg-[#DCE6F2] rounded w-3/4" />
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
) : bookmarks.length === 0 ? ( ) : bookmarks.length === 0 ? (
<div className="text-center py-12 bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm"> <div className="text-center py-12 bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" /> <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg> </svg>
<p className="font-body text-sm text-[#666]">No saved articles yet.</p> <p className="font-body text-sm text-[#666]">No saved articles yet.</p>
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors"> <Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#1D4ED8] hover:text-[#1E3A8A] transition-colors">
Browse News Browse News
</Link> </Link>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{bookmarks.map((item) => ( {bookmarks.map((item) => (
<div key={item.id} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm hover:border-[#3a3a3a] transition-colors group"> <div key={item.id} className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm hover:border-[#3a3a3a] transition-colors group">
<div className="flex gap-3 p-4"> <div className="flex gap-3 p-4">
<Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm"> <Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm">
{item.article_image ? ( {item.article_image ? (
<AppImage src={item.article_image} alt={item.article_image_alt || item.article_title} width={80} height={56} className="w-full h-full object-cover" /> <AppImage src={item.article_image} alt={item.article_image_alt || item.article_title} width={80} height={56} className="w-full h-full object-cover" />
) : ( ) : (
<div className="w-full h-full bg-[#252525] flex items-center justify-center"> <div className="w-full h-full bg-[#DCE6F2] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /> <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg> </svg>
@@ -572,10 +572,10 @@ export default function AccountPage() {
</Link> </Link>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{item.article_category && ( {item.article_category && (
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#3b82f6]">{item.article_category}</span> <span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#1D4ED8]">{item.article_category}</span>
)} )}
<Link href={`/articles/${item.article_slug}`}> <Link href={`/articles/${item.article_slug}`}>
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug mt-0.5"> <h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#1D4ED8] transition-colors line-clamp-2 leading-snug mt-0.5">
{item.article_title} {item.article_title}
</h3> </h3>
</Link> </Link>
@@ -605,7 +605,7 @@ export default function AccountPage() {
<h2 className="font-heading text-base font-bold text-[#e0e0e0]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0]">
Reading History Reading History
{history.length > 0 && ( {history.length > 0 && (
<span className="ml-2 font-body text-xs text-[#666] bg-[#1a1a1a] border border-[#2a2a2a] px-2 py-0.5 rounded-sm"> <span className="ml-2 font-body text-xs text-[#666] bg-[#FFFFFF] border border-[#DCE6F2] px-2 py-0.5 rounded-sm">
{history.length} {history.length}
</span> </span>
)} )}
@@ -622,37 +622,37 @@ export default function AccountPage() {
{historyLoading ? ( {historyLoading ? (
<div className="space-y-3"> <div className="space-y-3">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<div key={i} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 animate-pulse"> <div key={i} className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-4 animate-pulse">
<div className="flex gap-3"> <div className="flex gap-3">
<div className="w-20 h-14 bg-[#252525] rounded-sm flex-shrink-0" /> <div className="w-20 h-14 bg-[#DCE6F2] rounded-sm flex-shrink-0" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<div className="h-3 bg-[#252525] rounded w-1/4" /> <div className="h-3 bg-[#DCE6F2] rounded w-1/4" />
<div className="h-4 bg-[#252525] rounded w-3/4" /> <div className="h-4 bg-[#DCE6F2] rounded w-3/4" />
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
) : history.length === 0 ? ( ) : history.length === 0 ? (
<div className="text-center py-12 bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm"> <div className="text-center py-12 bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true">
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /> <circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
</svg> </svg>
<p className="font-body text-sm text-[#666]">No reading history yet.</p> <p className="font-body text-sm text-[#666]">No reading history yet.</p>
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors"> <Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#1D4ED8] hover:text-[#1E3A8A] transition-colors">
Browse News Browse News
</Link> </Link>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{history.map((item) => ( {history.map((item) => (
<div key={item.id} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm hover:border-[#3a3a3a] transition-colors group"> <div key={item.id} className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm hover:border-[#3a3a3a] transition-colors group">
<div className="flex gap-3 p-4"> <div className="flex gap-3 p-4">
<Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm"> <Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm">
{item.article_image ? ( {item.article_image ? (
<AppImage src={item.article_image} alt={item.article_image_alt || item.article_title} width={80} height={56} className="w-full h-full object-cover" /> <AppImage src={item.article_image} alt={item.article_image_alt || item.article_title} width={80} height={56} className="w-full h-full object-cover" />
) : ( ) : (
<div className="w-full h-full bg-[#252525] flex items-center justify-center"> <div className="w-full h-full bg-[#DCE6F2] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /> <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg> </svg>
@@ -661,10 +661,10 @@ export default function AccountPage() {
</Link> </Link>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{item.article_category && ( {item.article_category && (
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#3b82f6]">{item.article_category}</span> <span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#1D4ED8]">{item.article_category}</span>
)} )}
<Link href={`/articles/${item.article_slug}`}> <Link href={`/articles/${item.article_slug}`}>
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug mt-0.5"> <h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#1D4ED8] transition-colors line-clamp-2 leading-snug mt-0.5">
{item.article_title} {item.article_title}
</h3> </h3>
</Link> </Link>
@@ -693,17 +693,17 @@ export default function AccountPage() {
{topicsLoading ? ( {topicsLoading ? (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{[1, 2, 3, 4, 5, 6].map((i) => ( {[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 animate-pulse h-16" /> <div key={i} className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-4 animate-pulse h-16" />
))} ))}
</div> </div>
) : topics.length === 0 ? ( ) : topics.length === 0 ? (
<div className="text-center py-12 bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm"> <div className="text-center py-12 bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true">
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" /><line x1="7" y1="7" x2="7.01" y2="7" /> <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" /><line x1="7" y1="7" x2="7.01" y2="7" />
</svg> </svg>
<p className="font-body text-sm text-[#666]">No topic preferences tracked yet.</p> <p className="font-body text-sm text-[#666]">No topic preferences tracked yet.</p>
<p className="font-body text-xs text-[#555] mt-1">Read articles to build your interest profile.</p> <p className="font-body text-xs text-[#555] mt-1">Read articles to build your interest profile.</p>
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors"> <Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#1D4ED8] hover:text-[#1E3A8A] transition-colors">
Browse News Browse News
</Link> </Link>
</div> </div>
@@ -714,18 +714,18 @@ export default function AccountPage() {
const maxCount = topics[0]?.view_count || 1; const maxCount = topics[0]?.view_count || 1;
const pct = Math.round((t.view_count / maxCount) * 100); const pct = Math.round((t.view_count / maxCount) * 100);
return ( return (
<div key={t.id} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 hover:border-[#3a3a3a] transition-colors"> <div key={t.id} className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-4 hover:border-[#3a3a3a] transition-colors">
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-2">
<span className="font-body text-xs font-bold uppercase tracking-wider text-[#e0e0e0] capitalize leading-tight"> <span className="font-body text-xs font-bold uppercase tracking-wider text-[#e0e0e0] capitalize leading-tight">
{t.topic} {t.topic}
</span> </span>
<span className="font-body text-[10px] text-[#3b82f6] font-bold ml-2 flex-shrink-0"> <span className="font-body text-[10px] text-[#1D4ED8] font-bold ml-2 flex-shrink-0">
{t.view_count}× {t.view_count}×
</span> </span>
</div> </div>
<div className="w-full bg-[#252525] rounded-full h-1"> <div className="w-full bg-[#DCE6F2] rounded-full h-1">
<div <div
className="bg-[#3b82f6] h-1 rounded-full transition-all" className="bg-[#1D4ED8] h-1 rounded-full transition-all"
style={{ width: `${pct}%` }} style={{ width: `${pct}%` }}
/> />
</div> </div>
@@ -744,13 +744,13 @@ export default function AccountPage() {
{/* Preferences Tab */} {/* Preferences Tab */}
{activeTab === "preferences" && ( {activeTab === "preferences" && (
<div className="space-y-5"> <div className="space-y-5">
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-6">
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#DCE6F2]">
Forum Reputation Forum Reputation
</h2> </h2>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex flex-col items-center justify-center bg-[#111] border border-[#2a2a2a] rounded-sm px-6 py-4 min-w-[100px]"> <div className="flex flex-col items-center justify-center bg-[#111] border border-[#DCE6F2] rounded-sm px-6 py-4 min-w-[100px]">
<span className="font-heading text-3xl font-bold text-[#3b82f6]"> <span className="font-heading text-3xl font-bold text-[#1D4ED8]">
{profile?.reputation ?? 0} {profile?.reputation ?? 0}
</span> </span>
<span className="font-body text-xs text-[#666] mt-1 uppercase tracking-wider">Points</span> <span className="font-body text-xs text-[#666] mt-1 uppercase tracking-wider">Points</span>
@@ -759,26 +759,26 @@ export default function AccountPage() {
<p className="font-body text-sm text-[#aaa] leading-relaxed"> <p className="font-body text-sm text-[#aaa] leading-relaxed">
Earn reputation by posting helpful threads and replies in the community forum. Earn reputation by posting helpful threads and replies in the community forum.
</p> </p>
<Link href="/forum" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors"> <Link href="/forum" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#1D4ED8] hover:text-[#1E3A8A] transition-colors">
Go to Forum Go to Forum
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-6">
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#DCE6F2]">
Reading Preferences Reading Preferences
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between py-3 border-b border-[#222]"> <div className="flex items-center justify-between py-3 border-b border-[#222]">
<div> <div>
<p className="font-body text-sm font-bold text-[#e0e0e0]">Email Newsletter</p> <p className="font-body text-sm font-bold text-[#e0e0e0]">Email Newsletter</p>
<p className="font-body text-xs text-[#666] mt-0.5">Receive weekly broadcast industry news</p> <p className="font-body text-xs text-[#666] mt-0.5">Receive weekly pro AV industry news</p>
</div> </div>
<Link <Link
href="/home-page" href="/home-page"
className="font-body text-xs font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors"> className="font-body text-xs font-bold uppercase tracking-wide text-[#1D4ED8] hover:text-[#1E3A8A] transition-colors">
Manage Manage
</Link> </Link>
</div> </div>
@@ -789,7 +789,7 @@ export default function AccountPage() {
</div> </div>
<Link <Link
href="/reading-list" href="/reading-list"
className="font-body text-xs font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors"> className="font-body text-xs font-bold uppercase tracking-wide text-[#1D4ED8] hover:text-[#1E3A8A] transition-colors">
View List View List
</Link> </Link>
</div> </div>
@@ -808,8 +808,8 @@ export default function AccountPage() {
onClick={() => handleToggleDigest(!weeklyDigest)} onClick={() => handleToggleDigest(!weeklyDigest)}
aria-pressed={weeklyDigest} aria-pressed={weeklyDigest}
aria-label="Toggle weekly digest email" aria-label="Toggle weekly digest email"
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] disabled:opacity-50 ${ className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-[#1D4ED8] disabled:opacity-50 ${
weeklyDigest ? "bg-[#3b82f6] border-[#3b82f6]" : "bg-[#2a2a2a] border-[#333]" weeklyDigest ? "bg-[#1D4ED8] border-[#1D4ED8]" : "bg-[#DCE6F2] border-[#333]"
}`}> }`}>
<span <span
className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow transition duration-200 ease-in-out mt-0.5 ${ className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow transition duration-200 ease-in-out mt-0.5 ${

View File

@@ -91,7 +91,7 @@ export default function ClientsPage() {
setForm({ company_name: '', contact_name: '', contact_email: '', contact_phone: '', billing_address: '', notes: '' }); setForm({ company_name: '', contact_name: '', contact_email: '', contact_phone: '', billing_address: '', notes: '' });
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -102,7 +102,7 @@ export default function ClientsPage() {
<h1 className="text-xl font-bold">Clients</h1> <h1 className="text-xl font-bold">Clients</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 transition-colors">+ New Client</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 transition-colors">+ New Client</button>
</div> </div>
{/* Success Toast */} {/* Success Toast */}
@@ -116,14 +116,14 @@ export default function ClientsPage() {
{/* Filters */} {/* Filters */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{['active','inactive','all'].map(s => ( {['active','inactive','all'].map(s => (
<button key={s} onClick={() => setFilter(s)} className={`px-3 py-1 rounded text-xs capitalize ${filter === s ? 'bg-[#3b82f6] text-white' : 'bg-[#111] border border-[#252525] text-[#888] hover:text-white'}`}>{s}</button> <button key={s} onClick={() => setFilter(s)} className={`px-3 py-1 rounded text-xs capitalize ${filter === s ? 'bg-[#1D4ED8] text-white' : 'bg-[#111] border border-[#DCE6F2] text-[#888] hover:text-white'}`}>{s}</button>
))} ))}
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search clients…" className="ml-auto px-3 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#3b82f6]" /> <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search clients…" className="ml-auto px-3 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
{/* New Client Form */} {/* New Client Form */}
{showForm && ( {showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Client</h3> <h3 className="text-sm font-semibold">New Client</h3>
{errors._form && ( {errors._form && (
@@ -148,7 +148,7 @@ export default function ClientsPage() {
type={f.key === 'contact_email' ? 'email' : 'text'} type={f.key === 'contact_email' ? 'email' : 'text'}
value={(form as any)[f.key]} value={(form as any)[f.key]}
onChange={e => handleChange(f.key, e.target.value)} onChange={e => handleChange(f.key, e.target.value)}
className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors[f.key] ? 'border-red-500/60' : 'border-[#252525]'}`} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8] ${errors[f.key] ? 'border-red-500/60' : 'border-[#DCE6F2]'}`}
/> />
{errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>} {errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>}
</div> </div>
@@ -156,26 +156,26 @@ export default function ClientsPage() {
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Billing Address</label> <label className="text-xs text-[#888] block mb-1">Billing Address</label>
<input value={form.billing_address} onChange={e => handleChange('billing_address', e.target.value)} <input value={form.billing_address} onChange={e => handleChange('billing_address', e.target.value)}
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Notes</label> <label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2} <textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2}
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Client'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Client'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
{/* Table */} {/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Company','Contact','Email','Active Orders','Total Revenue','Status',''].map(h => ( {['Company','Contact','Email','Active Orders','Total Revenue','Status',''].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -187,17 +187,17 @@ export default function ClientsPage() {
) : clients.length === 0 ? ( ) : clients.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No clients found</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No clients found</td></tr>
) : clients.map(c => ( ) : clients.map(c => (
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={c.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-4 py-2 text-white font-medium">{c.company_name}</td> <td className="px-4 py-2 text-white font-medium">{c.company_name}</td>
<td className="px-4 py-2 text-[#ccc]">{c.contact_name}</td> <td className="px-4 py-2 text-[#ccc]">{c.contact_name}</td>
<td className="px-4 py-2 text-[#888]">{c.contact_email}</td> <td className="px-4 py-2 text-[#888]">{c.contact_email}</td>
<td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td> <td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td>
<td className="px-4 py-2 text-green-400">{fmt(c.total_revenue ?? 0)}</td> <td className="px-4 py-2 text-green-400">{fmt(c.total_revenue ?? 0)}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${c.status === 'active' ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{c.status}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${c.status === 'active' ? 'bg-green-400/10 text-green-400' : 'bg-[#FFFFFF] text-[#555]'}`}>{c.status}</span>
</td> </td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<Link href={`/admin/accounting/clients/${c.id}`} className="text-xs text-[#3b82f6] hover:underline">View</Link> <Link href={`/admin/accounting/clients/${c.id}`} className="text-xs text-[#1D4ED8] hover:underline">View</Link>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -41,7 +41,7 @@ export default function CommissionsPage() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -53,10 +53,10 @@ export default function CommissionsPage() {
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<select value={year} onChange={e => setYear(Number(e.target.value))} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={year} onChange={e => setYear(Number(e.target.value))} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
{[2024, 2025, 2026, 2027].map(y => <option key={y} value={y}>{y}</option>)} {[2024, 2025, 2026, 2027].map(y => <option key={y} value={y}>{y}</option>)}
</select> </select>
<select value={filterStaff} onChange={e => setFilterStaff(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterStaff} onChange={e => setFilterStaff(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Staff</option> <option value="">All Staff</option>
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)} {staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
</select> </select>
@@ -68,7 +68,7 @@ export default function CommissionsPage() {
{summary.map((s: any) => { {summary.map((s: any) => {
const pct = s.next_threshold_cents ? Math.min(100, Math.round((s.ytd_sales_cents / s.next_threshold_cents) * 100)) : 100; const pct = s.next_threshold_cents ? Math.min(100, Math.round((s.ytd_sales_cents / s.next_threshold_cents) * 100)) : 100;
return ( return (
<div key={s.staff_id} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-2"> <div key={s.staff_id} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm font-semibold text-white">{s.full_name}</p> <p className="text-sm font-semibold text-white">{s.full_name}</p>
<span className="text-xs text-[#888]">{s.current_tier}</span> <span className="text-xs text-[#888]">{s.current_tier}</span>
@@ -83,8 +83,8 @@ export default function CommissionsPage() {
<div className="flex justify-between text-xs text-[#555] mb-1"> <div className="flex justify-between text-xs text-[#555] mb-1">
<span>Tier progress</span><span>{pct}%</span> <span>Tier progress</span><span>{pct}%</span>
</div> </div>
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden"> <div className="h-1.5 bg-[#FFFFFF] rounded-full overflow-hidden">
<div className="h-full bg-[#3b82f6] rounded-full transition-all" style={{ width: `${pct}%` }} /> <div className="h-full bg-[#1D4ED8] rounded-full transition-all" style={{ width: `${pct}%` }} />
</div> </div>
</div> </div>
)} )}
@@ -94,10 +94,10 @@ export default function CommissionsPage() {
</div> </div>
{/* Detail table */} {/* Detail table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Staff','Invoice','Sale Amount','Rate','Commission','YTD at Time','Paid',''].map(h => ( {['Staff','Invoice','Sale Amount','Rate','Commission','YTD at Time','Paid',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -109,9 +109,9 @@ export default function CommissionsPage() {
) : commissions.length === 0 ? ( ) : commissions.length === 0 ? (
<tr><td colSpan={8} className="px-4 py-8 text-center text-[#555] text-xs">No commission records</td></tr> <tr><td colSpan={8} className="px-4 py-8 text-center text-[#555] text-xs">No commission records</td></tr>
) : commissions.map((c: any) => ( ) : commissions.map((c: any) => (
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={c.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-white text-xs">{c.rmp_sales_staff?.full_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{c.rmp_sales_staff?.full_name ?? '—'}</td>
<td className="px-3 py-2 text-[#3b82f6] text-xs font-mono">{c.rmp_invoices?.invoice_number ?? '—'}</td> <td className="px-3 py-2 text-[#1D4ED8] text-xs font-mono">{c.rmp_invoices?.invoice_number ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{fmt(c.sale_amount_cents ?? 0)}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{fmt(c.sale_amount_cents ?? 0)}</td>
<td className="px-3 py-2 text-[#888] text-xs">{((c.commission_rate ?? 0) * 100).toFixed(1)}%</td> <td className="px-3 py-2 text-[#888] text-xs">{((c.commission_rate ?? 0) * 100).toFixed(1)}%</td>
<td className="px-3 py-2 text-green-400 text-xs">{fmt(c.commission_amount_cents ?? 0)}</td> <td className="px-3 py-2 text-green-400 text-xs">{fmt(c.commission_amount_cents ?? 0)}</td>

View File

@@ -43,7 +43,7 @@ export default function DocumentsPage() {
const years = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i); const years = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -54,70 +54,70 @@ export default function DocumentsPage() {
<h1 className="text-xl font-bold">Document Vault</h1> <h1 className="text-xl font-bold">Document Vault</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Upload Document</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500">+ Upload Document</button>
</div> </div>
{/* Year tabs */} {/* Year tabs */}
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
<button onClick={() => setFilterYear('')} className={`px-3 py-1 rounded text-xs ${!filterYear ? 'bg-[#3b82f6] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>All Years</button> <button onClick={() => setFilterYear('')} className={`px-3 py-1 rounded text-xs ${!filterYear ? 'bg-[#1D4ED8] text-white' : 'bg-[#111] border border-[#DCE6F2] text-[#888]'}`}>All Years</button>
{years.map(y => ( {years.map(y => (
<button key={y} onClick={() => setFilterYear(String(y))} className={`px-3 py-1 rounded text-xs ${filterYear === String(y) ? 'bg-[#3b82f6] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>{y}</button> <button key={y} onClick={() => setFilterYear(String(y))} className={`px-3 py-1 rounded text-xs ${filterYear === String(y) ? 'bg-[#1D4ED8] text-white' : 'bg-[#111] border border-[#DCE6F2] text-[#888]'}`}>{y}</button>
))} ))}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Types</option> <option value="">All Types</option>
{DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)} {DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select> </select>
</div> </div>
{showForm && ( {showForm && (
<form onSubmit={handleCreate} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Upload Document</h3> <h3 className="text-sm font-semibold">Upload Document</h3>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Type</label> <label className="text-xs text-[#888] block mb-1">Type</label>
<select value={form.document_type} onChange={e => setForm(p => ({ ...p, document_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={form.document_type} onChange={e => setForm(p => ({ ...p, document_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
{DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)} {DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Title *</label> <label className="text-xs text-[#888] block mb-1">Title *</label>
<input required value={form.title} onChange={e => setForm(p => ({ ...p, title: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input required value={form.title} onChange={e => setForm(p => ({ ...p, title: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
{form.document_type === 'sales_agreement' && ( {form.document_type === 'sales_agreement' && (
<> <>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Person *</label> <label className="text-xs text-[#888] block mb-1">Person *</label>
<input required value={form.person} onChange={e => setForm(p => ({ ...p, person: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input required value={form.person} onChange={e => setForm(p => ({ ...p, person: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Year *</label> <label className="text-xs text-[#888] block mb-1">Year *</label>
<input type="number" required value={form.year} onChange={e => setForm(p => ({ ...p, year: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input type="number" required value={form.year} onChange={e => setForm(p => ({ ...p, year: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</> </>
)} )}
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">File URL</label> <label className="text-xs text-[#888] block mb-1">File URL</label>
<input value={form.file_url} onChange={e => setForm(p => ({ ...p, file_url: e.target.value }))} placeholder="https://…" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.file_url} onChange={e => setForm(p => ({ ...p, file_url: e.target.value }))} placeholder="https://…" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Notes</label> <label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Save Document'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Save Document'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Type','Title','Person','Year','Date',''].map(h => ( {['Type','Title','Person','Year','Date',''].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -129,14 +129,14 @@ export default function DocumentsPage() {
) : docs.length === 0 ? ( ) : docs.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No documents found</td></tr> <tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No documents found</td></tr>
) : docs.map((d: any) => ( ) : docs.map((d: any) => (
<tr key={d.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={d.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-4 py-2 text-[#888] text-xs capitalize">{d.document_type?.replace(/_/g, ' ')}</td> <td className="px-4 py-2 text-[#888] text-xs capitalize">{d.document_type?.replace(/_/g, ' ')}</td>
<td className="px-4 py-2 text-white text-xs">{d.title}</td> <td className="px-4 py-2 text-white text-xs">{d.title}</td>
<td className="px-4 py-2 text-[#ccc] text-xs">{d.person ?? '—'}</td> <td className="px-4 py-2 text-[#ccc] text-xs">{d.person ?? '—'}</td>
<td className="px-4 py-2 text-[#888] text-xs">{d.year ?? '—'}</td> <td className="px-4 py-2 text-[#888] text-xs">{d.year ?? '—'}</td>
<td className="px-4 py-2 text-[#555] text-xs">{d.uploaded_at ? new Date(d.uploaded_at).toLocaleDateString() : '—'}</td> <td className="px-4 py-2 text-[#555] text-xs">{d.uploaded_at ? new Date(d.uploaded_at).toLocaleDateString() : '—'}</td>
<td className="px-4 py-2 flex gap-2"> <td className="px-4 py-2 flex gap-2">
{d.file_url && <a href={d.file_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#3b82f6] hover:underline">Download</a>} {d.file_url && <a href={d.file_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#1D4ED8] hover:underline">Download</a>}
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -84,7 +84,7 @@ export default function ExpensesPage() {
setForm({ vendor: '', amount: '', category: 'other', description: '', expense_date: new Date().toISOString().split('T')[0], receipt_source: 'manual' }); setForm({ vendor: '', amount: '', category: 'other', description: '', expense_date: new Date().toISOString().split('T')[0], receipt_source: 'manual' });
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -95,7 +95,7 @@ export default function ExpensesPage() {
<h1 className="text-xl font-bold">Expenses</h1> <h1 className="text-xl font-bold">Expenses</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Log Expense</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500">+ Log Expense</button>
</div> </div>
{/* Success Toast */} {/* Success Toast */}
@@ -107,11 +107,11 @@ export default function ExpensesPage() {
)} )}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<select value={filterCat} onChange={e => setFilterCat(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterCat} onChange={e => setFilterCat(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Categories</option> <option value="">All Categories</option>
{CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)} {CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)}
</select> </select>
<select value={filterReconciled} onChange={e => setFilterReconciled(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterReconciled} onChange={e => setFilterReconciled(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All</option> <option value="">All</option>
<option value="true">Reconciled</option> <option value="true">Reconciled</option>
<option value="false">Unreconciled</option> <option value="false">Unreconciled</option>
@@ -119,7 +119,7 @@ export default function ExpensesPage() {
</div> </div>
{showForm && ( {showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Log Expense</h3> <h3 className="text-sm font-semibold">Log Expense</h3>
{errors._form && ( {errors._form && (
@@ -132,40 +132,40 @@ export default function ExpensesPage() {
<div className="grid grid-cols-2 md:grid-cols-3 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Vendor</label> <label className="text-xs text-[#888] block mb-1">Vendor</label>
<input type="text" value={form.vendor} onChange={e => handleChange('vendor', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input type="text" value={form.vendor} onChange={e => handleChange('vendor', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Amount ($) <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Amount ($) <span className="text-red-400">*</span></label>
<input type="number" step="0.01" value={form.amount} onChange={e => handleChange('amount', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors.amount ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="number" step="0.01" value={form.amount} onChange={e => handleChange('amount', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8] ${errors.amount ? 'border-red-500/60' : 'border-[#DCE6F2]'}`} />
{errors.amount && <p className="mt-1 text-xs text-red-400">{errors.amount}</p>} {errors.amount && <p className="mt-1 text-xs text-red-400">{errors.amount}</p>}
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Date <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Date <span className="text-red-400">*</span></label>
<input type="date" value={form.expense_date} onChange={e => handleChange('expense_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors.expense_date ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="date" value={form.expense_date} onChange={e => handleChange('expense_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8] ${errors.expense_date ? 'border-red-500/60' : 'border-[#DCE6F2]'}`} />
{errors.expense_date && <p className="mt-1 text-xs text-red-400">{errors.expense_date}</p>} {errors.expense_date && <p className="mt-1 text-xs text-red-400">{errors.expense_date}</p>}
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Category</label> <label className="text-xs text-[#888] block mb-1">Category</label>
<select value={form.category} onChange={e => handleChange('category', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={form.category} onChange={e => handleChange('category', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
{CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)} {CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)}
</select> </select>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Description</label> <label className="text-xs text-[#888] block mb-1">Description</label>
<input value={form.description} onChange={e => handleChange('description', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.description} onChange={e => handleChange('description', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Log Expense'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Log Expense'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Date','Vendor','Amount','Category','Source','Reconciled','Description'].map(h => ( {['Date','Vendor','Amount','Category','Source','Reconciled','Description'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -177,7 +177,7 @@ export default function ExpensesPage() {
) : expenses.length === 0 ? ( ) : expenses.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No expenses found</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No expenses found</td></tr>
) : expenses.map((ex: any) => ( ) : expenses.map((ex: any) => (
<tr key={ex.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={ex.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-[#888] text-xs">{ex.expense_date}</td> <td className="px-3 py-2 text-[#888] text-xs">{ex.expense_date}</td>
<td className="px-3 py-2 text-white text-xs">{ex.vendor ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{ex.vendor ?? '—'}</td>
<td className="px-3 py-2 text-red-400 text-xs">{ex.amount_cents ? fmt(ex.amount_cents) : '—'}</td> <td className="px-3 py-2 text-red-400 text-xs">{ex.amount_cents ? fmt(ex.amount_cents) : '—'}</td>

View File

@@ -10,7 +10,7 @@ const STATUS_COLORS: Record<string, string> = {
paid: 'bg-green-400/10 text-green-400', paid: 'bg-green-400/10 text-green-400',
pending: 'bg-yellow-400/10 text-yellow-400', pending: 'bg-yellow-400/10 text-yellow-400',
overdue: 'bg-red-400/10 text-red-400', overdue: 'bg-red-400/10 text-red-400',
cancelled: 'bg-[#1a1a1a] text-[#555]', cancelled: 'bg-[#FFFFFF] text-[#555]',
}; };
function InvoicesContent() { function InvoicesContent() {
@@ -83,7 +83,7 @@ function InvoicesContent() {
setPayForm({ method: 'check', paid_date: new Date().toISOString().split('T')[0], wire_reference: '' }); setPayForm({ method: 'check', paid_date: new Date().toISOString().split('T')[0], wire_reference: '' });
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -94,7 +94,7 @@ function InvoicesContent() {
<h1 className="text-xl font-bold">Invoices</h1> <h1 className="text-xl font-bold">Invoices</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ New Invoice (via Order)</Link> <Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500">+ New Invoice (via Order)</Link>
</div> </div>
{/* Success Toast */} {/* Success Toast */}
@@ -108,14 +108,14 @@ function InvoicesContent() {
{/* Filters */} {/* Filters */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{['','pending','paid','overdue','cancelled'].map(s => ( {['','pending','paid','overdue','cancelled'].map(s => (
<button key={s} onClick={() => setFilterStatus(s)} className={`px-3 py-1 rounded text-xs capitalize ${filterStatus === s ? 'bg-[#3b82f6] text-white' : 'bg-[#111] border border-[#252525] text-[#888] hover:text-white'}`}>{s || 'All'}</button> <button key={s} onClick={() => setFilterStatus(s)} className={`px-3 py-1 rounded text-xs capitalize ${filterStatus === s ? 'bg-[#1D4ED8] text-white' : 'bg-[#111] border border-[#DCE6F2] text-[#888] hover:text-white'}`}>{s || 'All'}</button>
))} ))}
</div> </div>
{/* Mark Paid Modal */} {/* Mark Paid Modal */}
{markPaidId && ( {markPaidId && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<form onSubmit={handleMarkPaid} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-5 w-80 space-y-3"> <form onSubmit={handleMarkPaid} noValidate className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5 w-80 space-y-3">
<h3 className="text-sm font-semibold">Mark Invoice as Paid</h3> <h3 className="text-sm font-semibold">Mark Invoice as Paid</h3>
{payErrors._form && ( {payErrors._form && (
@@ -127,35 +127,35 @@ function InvoicesContent() {
<div> <div>
<label className="text-xs text-[#888] block mb-1">Payment Method</label> <label className="text-xs text-[#888] block mb-1">Payment Method</label>
<select value={payForm.method} onChange={e => handlePayChange('method', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={payForm.method} onChange={e => handlePayChange('method', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
{['stripe','wire','check','other'].map(m => <option key={m} value={m}>{m}</option>)} {['stripe','wire','check','other'].map(m => <option key={m} value={m}>{m}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Paid Date <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Paid Date <span className="text-red-400">*</span></label>
<input type="date" value={payForm.paid_date} onChange={e => handlePayChange('paid_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.paid_date ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="date" value={payForm.paid_date} onChange={e => handlePayChange('paid_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.paid_date ? 'border-red-500/60' : 'border-[#DCE6F2]'}`} />
{payErrors.paid_date && <p className="mt-1 text-xs text-red-400">{payErrors.paid_date}</p>} {payErrors.paid_date && <p className="mt-1 text-xs text-red-400">{payErrors.paid_date}</p>}
</div> </div>
{payForm.method === 'wire' && ( {payForm.method === 'wire' && (
<div> <div>
<label className="text-xs text-[#888] block mb-1">Wire Reference <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Wire Reference <span className="text-red-400">*</span></label>
<input value={payForm.wire_reference} onChange={e => handlePayChange('wire_reference', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.wire_reference ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input value={payForm.wire_reference} onChange={e => handlePayChange('wire_reference', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.wire_reference ? 'border-red-500/60' : 'border-[#DCE6F2]'}`} />
{payErrors.wire_reference && <p className="mt-1 text-xs text-red-400">{payErrors.wire_reference}</p>} {payErrors.wire_reference && <p className="mt-1 text-xs text-red-400">{payErrors.wire_reference}</p>}
</div> </div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-green-600 rounded text-xs font-medium hover:bg-green-500 disabled:opacity-50">{saving ? 'Saving…' : 'Mark Paid'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-green-600 rounded text-xs font-medium hover:bg-green-500 disabled:opacity-50">{saving ? 'Saving…' : 'Mark Paid'}</button>
<button type="button" onClick={handleCloseModal} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={handleCloseModal} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
</div> </div>
)} )}
{/* Table */} {/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Invoice#','Client','Site','Order#','Amount','Status','Due Date','Salesperson',''].map(h => ( {['Invoice#','Client','Site','Order#','Amount','Status','Due Date','Salesperson',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))} ))}
@@ -167,14 +167,14 @@ function InvoicesContent() {
) : invoices.length === 0 ? ( ) : invoices.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No invoices found</td></tr> <tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No invoices found</td></tr>
) : invoices.map((inv: any) => ( ) : invoices.map((inv: any) => (
<tr key={inv.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={inv.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-[#3b82f6] font-mono text-xs">{inv.invoice_number}</td> <td className="px-3 py-2 text-[#1D4ED8] font-mono text-xs">{inv.invoice_number}</td>
<td className="px-3 py-2 text-white text-xs">{inv.rmp_clients?.company_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{inv.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{inv.site}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{inv.site}</td>
<td className="px-3 py-2 text-[#888] text-xs font-mono">{inv.rmp_orders?.internal_order_number ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs font-mono">{inv.rmp_orders?.internal_order_number ?? '—'}</td>
<td className="px-3 py-2 text-green-400 text-xs">{inv.amount_cents ? fmt(inv.amount_cents) : '—'}</td> <td className="px-3 py-2 text-green-400 text-xs">{inv.amount_cents ? fmt(inv.amount_cents) : '—'}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${STATUS_COLORS[inv.status] ?? 'bg-[#1a1a1a] text-[#555]'}`}>{inv.status}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${STATUS_COLORS[inv.status] ?? 'bg-[#FFFFFF] text-[#555]'}`}>{inv.status}</span>
</td> </td>
<td className="px-3 py-2 text-[#888] text-xs">{inv.due_date ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs">{inv.due_date ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{inv.rmp_sales_staff?.full_name ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{inv.rmp_sales_staff?.full_name ?? '—'}</td>
@@ -195,7 +195,7 @@ function InvoicesContent() {
export default function InvoicesPage() { export default function InvoicesPage() {
return ( return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>}> <Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>}>
<InvoicesContent /> <InvoicesContent />
</Suspense> </Suspense>
); );

View File

@@ -104,7 +104,7 @@ function OrdersContent() {
setForm({ client_id: '', site: '', product_id: '', ad_unit: '', description: '', start_date: '', end_date: '', total_amount_cents: '', currency: 'USD', invoicing_schedule: 'single', next_invoice_date: '', staff_id: '', po_number: '', notes: '' }); setForm({ client_id: '', site: '', product_id: '', ad_unit: '', description: '', start_date: '', end_date: '', total_amount_cents: '', currency: 'USD', invoicing_schedule: 'single', next_invoice_date: '', staff_id: '', po_number: '', notes: '' });
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -115,7 +115,7 @@ function OrdersContent() {
<h1 className="text-xl font-bold">Orders</h1> <h1 className="text-xl font-bold">Orders</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button>
</div> </div>
{/* Success Toast */} {/* Success Toast */}
@@ -128,20 +128,20 @@ function OrdersContent() {
{/* Filters */} {/* Filters */}
<div className="flex gap-2 flex-wrap items-center"> <div className="flex gap-2 flex-wrap items-center">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option> <option value="">All Statuses</option>
{STATUSES.map(s => <option key={s} value={s}>{s}</option>)} {STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
<input value={filterClient} onChange={e => setFilterClient(e.target.value)} placeholder="Filter by client…" className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#3b82f6]" /> <input value={filterClient} onChange={e => setFilterClient(e.target.value)} placeholder="Filter by client…" className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
{/* New Order Form */} {/* New Order Form */}
{showForm && ( {showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Order</h3> <h3 className="text-sm font-semibold">New Order</h3>
{errors._form && ( {errors._form && (
@@ -154,7 +154,7 @@ function OrdersContent() {
<div className="grid grid-cols-2 md:grid-cols-3 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Client <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Client <span className="text-red-400">*</span></label>
<select value={form.client_id} onChange={e => handleChange('client_id', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors.client_id ? 'border-red-500/60' : 'border-[#252525]'}`}> <select value={form.client_id} onChange={e => handleChange('client_id', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8] ${errors.client_id ? 'border-red-500/60' : 'border-[#DCE6F2]'}`}>
<option value="">Select client</option> <option value="">Select client</option>
{clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)} {clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)}
</select> </select>
@@ -162,7 +162,7 @@ function OrdersContent() {
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Site <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Site <span className="text-red-400">*</span></label>
<select value={form.site} onChange={e => handleChange('site', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors.site ? 'border-red-500/60' : 'border-[#252525]'}`}> <select value={form.site} onChange={e => handleChange('site', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8] ${errors.site ? 'border-red-500/60' : 'border-[#DCE6F2]'}`}>
<option value="">Select site</option> <option value="">Select site</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
@@ -170,67 +170,67 @@ function OrdersContent() {
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Product</label> <label className="text-xs text-[#888] block mb-1">Product</label>
<select value={form.product_id} onChange={e => handleChange('product_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]"> <select value={form.product_id} onChange={e => handleChange('product_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]">
<option value="">Select product</option> <option value="">Select product</option>
{products.map((p: any) => <option key={p.id} value={p.id}>{p.name}</option>)} {products.map((p: any) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Ad Unit</label> <label className="text-xs text-[#888] block mb-1">Ad Unit</label>
<input value={form.ad_unit} onChange={e => handleChange('ad_unit', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.ad_unit} onChange={e => handleChange('ad_unit', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Start Date</label> <label className="text-xs text-[#888] block mb-1">Start Date</label>
<input type="date" value={form.start_date} onChange={e => handleChange('start_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input type="date" value={form.start_date} onChange={e => handleChange('start_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">End Date</label> <label className="text-xs text-[#888] block mb-1">End Date</label>
<input type="date" value={form.end_date} onChange={e => handleChange('end_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors.end_date ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="date" value={form.end_date} onChange={e => handleChange('end_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8] ${errors.end_date ? 'border-red-500/60' : 'border-[#DCE6F2]'}`} />
{errors.end_date && <p className="mt-1 text-xs text-red-400">{errors.end_date}</p>} {errors.end_date && <p className="mt-1 text-xs text-red-400">{errors.end_date}</p>}
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Total Amount ($)</label> <label className="text-xs text-[#888] block mb-1">Total Amount ($)</label>
<input type="number" step="0.01" value={form.total_amount_cents} onChange={e => handleChange('total_amount_cents', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors.total_amount_cents ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="number" step="0.01" value={form.total_amount_cents} onChange={e => handleChange('total_amount_cents', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8] ${errors.total_amount_cents ? 'border-red-500/60' : 'border-[#DCE6F2]'}`} />
{errors.total_amount_cents && <p className="mt-1 text-xs text-red-400">{errors.total_amount_cents}</p>} {errors.total_amount_cents && <p className="mt-1 text-xs text-red-400">{errors.total_amount_cents}</p>}
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Invoicing Schedule</label> <label className="text-xs text-[#888] block mb-1">Invoicing Schedule</label>
<select value={form.invoicing_schedule} onChange={e => handleChange('invoicing_schedule', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]"> <select value={form.invoicing_schedule} onChange={e => handleChange('invoicing_schedule', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]">
{SCHEDULES.map(s => <option key={s} value={s}>{s}</option>)} {SCHEDULES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Next Invoice Date</label> <label className="text-xs text-[#888] block mb-1">Next Invoice Date</label>
<input type="date" value={form.next_invoice_date} onChange={e => handleChange('next_invoice_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input type="date" value={form.next_invoice_date} onChange={e => handleChange('next_invoice_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Salesperson</label> <label className="text-xs text-[#888] block mb-1">Salesperson</label>
<select value={form.staff_id} onChange={e => handleChange('staff_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]"> <select value={form.staff_id} onChange={e => handleChange('staff_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]">
<option value="">None</option> <option value="">None</option>
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)} {staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">PO Number</label> <label className="text-xs text-[#888] block mb-1">PO Number</label>
<input value={form.po_number} onChange={e => handleChange('po_number', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.po_number} onChange={e => handleChange('po_number', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div className="col-span-2 md:col-span-3"> <div className="col-span-2 md:col-span-3">
<label className="text-xs text-[#888] block mb-1">Notes</label> <label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Order'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Order'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
{/* Table */} {/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Client','PO#','Order#','Site','Product','Ad Unit','Dates','Amount','Status',''].map(h => ( {['Client','PO#','Order#','Site','Product','Ad Unit','Dates','Amount','Status',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))} ))}
@@ -242,20 +242,20 @@ function OrdersContent() {
) : orders.length === 0 ? ( ) : orders.length === 0 ? (
<tr><td colSpan={10} className="px-4 py-8 text-center text-[#555] text-xs">No orders found</td></tr> <tr><td colSpan={10} className="px-4 py-8 text-center text-[#555] text-xs">No orders found</td></tr>
) : orders.map((o: any) => ( ) : orders.map((o: any) => (
<tr key={o.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={o.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-white text-xs">{o.rmp_clients?.company_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{o.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{o.po_number ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs">{o.po_number ?? '—'}</td>
<td className="px-3 py-2 text-[#3b82f6] text-xs font-mono">{o.internal_order_number}</td> <td className="px-3 py-2 text-[#1D4ED8] text-xs font-mono">{o.internal_order_number}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{o.site}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{o.site}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{o.rmp_products?.name ?? o.ad_unit ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{o.rmp_products?.name ?? o.ad_unit ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{o.ad_unit ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs">{o.ad_unit ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs whitespace-nowrap">{o.start_date} {o.end_date ?? '∞'}</td> <td className="px-3 py-2 text-[#888] text-xs whitespace-nowrap">{o.start_date} {o.end_date ?? '∞'}</td>
<td className="px-3 py-2 text-green-400 text-xs">{o.total_amount_cents ? fmt(o.total_amount_cents) : '—'}</td> <td className="px-3 py-2 text-green-400 text-xs">{o.total_amount_cents ? fmt(o.total_amount_cents) : '—'}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${o.status === 'active' ? 'bg-green-400/10 text-green-400' : o.status === 'cancelled' ? 'bg-red-400/10 text-red-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{o.status}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${o.status === 'active' ? 'bg-green-400/10 text-green-400' : o.status === 'cancelled' ? 'bg-red-400/10 text-red-400' : 'bg-[#FFFFFF] text-[#555]'}`}>{o.status}</span>
</td> </td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#3b82f6] hover:underline">View</Link> <Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#1D4ED8] hover:underline">View</Link>
</td> </td>
</tr> </tr>
))} ))}
@@ -269,7 +269,7 @@ function OrdersContent() {
export default function OrdersPage() { export default function OrdersPage() {
return ( return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>}> <Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>}>
<OrdersContent /> <OrdersContent />
</Suspense> </Suspense>
); );

View File

@@ -44,7 +44,7 @@ export default function AccountingDashboard() {
if (loading || loadingData) return ( if (loading || loadingData) return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
if (!user) return null; if (!user) return null;
@@ -59,18 +59,18 @@ export default function AccountingDashboard() {
<p className="text-[#888] text-sm mt-1">Relevant Media Properties, LLC</p> <p className="text-[#888] text-sm mt-1">Relevant Media Properties, LLC</p>
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Link href="/admin/accounting/clients" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Client</Link> <Link href="/admin/accounting/clients" className="px-3 py-1.5 bg-[#FFFFFF] border border-[#2a3a50] rounded text-xs text-[#1D4ED8] hover:bg-[#0d1a2d] transition-colors">+ New Client</Link>
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Order</Link> <Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#FFFFFF] border border-[#2a3a50] rounded text-xs text-[#1D4ED8] hover:bg-[#0d1a2d] transition-colors">+ New Order</Link>
<Link href="/admin/accounting/invoices" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Invoice</Link> <Link href="/admin/accounting/invoices" className="px-3 py-1.5 bg-[#FFFFFF] border border-[#2a3a50] rounded text-xs text-[#1D4ED8] hover:bg-[#0d1a2d] transition-colors">+ New Invoice</Link>
<Link href="/admin/accounting/expenses" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ Log Expense</Link> <Link href="/admin/accounting/expenses" className="px-3 py-1.5 bg-[#FFFFFF] border border-[#2a3a50] rounded text-xs text-[#1D4ED8] hover:bg-[#0d1a2d] transition-colors">+ Log Expense</Link>
<Link href="/admin/accounting/reconciliation" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link> <Link href="/admin/accounting/reconciliation" className="px-3 py-1.5 bg-[#FFFFFF] border border-[#2a3a50] rounded text-xs text-[#1D4ED8] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link>
</div> </div>
</div> </div>
{/* Stat Cards */} {/* Stat Cards */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
{STAT_CARDS.map(c => ( {STAT_CARDS.map(c => (
<Link key={c.key} href={c.href} className="bg-[#111] border border-[#252525] rounded-lg p-4 hover:border-[#333] transition-colors"> <Link key={c.key} href={c.href} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 hover:border-[#333] transition-colors">
<p className={`text-lg font-bold ${c.color}`}>{fmt(stats[c.key] ?? 0)}</p> <p className={`text-lg font-bold ${c.color}`}>{fmt(stats[c.key] ?? 0)}</p>
<p className="text-xs text-[#888] mt-1">{c.label}</p> <p className="text-xs text-[#888] mt-1">{c.label}</p>
</Link> </Link>
@@ -78,13 +78,13 @@ export default function AccountingDashboard() {
</div> </div>
{/* Site Revenue Table */} {/* Site Revenue Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"> <div className="px-4 py-3 border-b border-[#DCE6F2]">
<h2 className="text-sm font-semibold text-white">Revenue by Site</h2> <h2 className="text-sm font-semibold text-white">Revenue by Site</h2>
</div> </div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Site','Revenue MTD','Revenue YTD','Outstanding','Active Orders'].map(h => ( {['Site','Revenue MTD','Revenue YTD','Outstanding','Active Orders'].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -94,7 +94,7 @@ export default function AccountingDashboard() {
{siteRevenue.length === 0 ? ( {siteRevenue.length === 0 ? (
<tr><td colSpan={5} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr> <tr><td colSpan={5} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr>
) : siteRevenue.map((row: any) => ( ) : siteRevenue.map((row: any) => (
<tr key={row.site} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={row.site} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-4 py-2 text-white font-medium capitalize">{row.site}</td> <td className="px-4 py-2 text-white font-medium capitalize">{row.site}</td>
<td className="px-4 py-2 text-green-400">{fmt(row.revenue_mtd ?? 0)}</td> <td className="px-4 py-2 text-green-400">{fmt(row.revenue_mtd ?? 0)}</td>
<td className="px-4 py-2 text-green-400">{fmt(row.revenue_ytd ?? 0)}</td> <td className="px-4 py-2 text-green-400">{fmt(row.revenue_ytd ?? 0)}</td>
@@ -107,13 +107,13 @@ export default function AccountingDashboard() {
</div> </div>
{/* Top Clients */} {/* Top Clients */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"> <div className="px-4 py-3 border-b border-[#DCE6F2]">
<h2 className="text-sm font-semibold text-white">Top 5 Clients YTD</h2> <h2 className="text-sm font-semibold text-white">Top 5 Clients YTD</h2>
</div> </div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Client','Revenue YTD','Active Orders'].map(h => ( {['Client','Revenue YTD','Active Orders'].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -123,7 +123,7 @@ export default function AccountingDashboard() {
{topClients.length === 0 ? ( {topClients.length === 0 ? (
<tr><td colSpan={3} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr> <tr><td colSpan={3} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr>
) : topClients.map((c: any) => ( ) : topClients.map((c: any) => (
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={c.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-4 py-2 text-white">{c.company_name}</td> <td className="px-4 py-2 text-white">{c.company_name}</td>
<td className="px-4 py-2 text-green-400">{fmt(c.revenue_ytd ?? 0)}</td> <td className="px-4 py-2 text-green-400">{fmt(c.revenue_ytd ?? 0)}</td>
<td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td> <td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td>
@@ -146,7 +146,7 @@ export default function AccountingDashboard() {
{ label: 'Reports', href: '/admin/accounting/reports' }, { label: 'Reports', href: '/admin/accounting/reports' },
{ label: 'Documents', href: '/admin/accounting/documents' }, { label: 'Documents', href: '/admin/accounting/documents' },
].map(l => ( ].map(l => (
<Link key={l.href} href={l.href} className="bg-[#111] border border-[#252525] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#3b82f6] transition-colors"> <Link key={l.href} href={l.href} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#1D4ED8] transition-colors">
{l.label} {l.label}
</Link> </Link>
))} ))}

View File

@@ -48,7 +48,7 @@ export default function ReconciliationPage() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -59,7 +59,7 @@ export default function ReconciliationPage() {
<h1 className="text-xl font-bold">Bank Reconciliation</h1> <h1 className="text-xl font-bold">Bank Reconciliation</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<label className={`px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 cursor-pointer ${uploading ? 'opacity-50' : ''}`}> <label className={`px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 cursor-pointer ${uploading ? 'opacity-50' : ''}`}>
{uploading ? 'Uploading…' : 'Upload Bank CSV'} {uploading ? 'Uploading…' : 'Upload Bank CSV'}
<input type="file" accept=".csv" className="hidden" onChange={handleUpload} disabled={uploading} /> <input type="file" accept=".csv" className="hidden" onChange={handleUpload} disabled={uploading} />
</label> </label>
@@ -78,10 +78,10 @@ export default function ReconciliationPage() {
</div> </div>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Date','Description','Amount','Type','Matched To','Reconciled','Action'].map(h => ( {['Date','Description','Amount','Type','Matched To','Reconciled','Action'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -93,7 +93,7 @@ export default function ReconciliationPage() {
) : transactions.length === 0 ? ( ) : transactions.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No transactions. Upload a CSV to begin.</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No transactions. Upload a CSV to begin.</td></tr>
) : transactions.map((tx: any) => ( ) : transactions.map((tx: any) => (
<tr key={tx.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={tx.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-[#888] text-xs">{tx.transaction_date}</td> <td className="px-3 py-2 text-[#888] text-xs">{tx.transaction_date}</td>
<td className="px-3 py-2 text-white text-xs max-w-xs truncate">{tx.description}</td> <td className="px-3 py-2 text-white text-xs max-w-xs truncate">{tx.description}</td>
<td className={`px-3 py-2 text-xs font-medium ${tx.type === 'credit' ? 'text-green-400' : 'text-red-400'}`}>{fmt(Math.abs(tx.amount_cents ?? 0))}</td> <td className={`px-3 py-2 text-xs font-medium ${tx.type === 'credit' ? 'text-green-400' : 'text-red-400'}`}>{fmt(Math.abs(tx.amount_cents ?? 0))}</td>

View File

@@ -28,7 +28,7 @@ export default function ReportsPage() {
useEffect(() => { if (user) loadReport(); }, [user]); useEffect(() => { if (user) loadReport(); }, [user]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -40,41 +40,41 @@ export default function ReportsPage() {
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<input type="date" value={startDate} onChange={e => setStartDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="date" value={startDate} onChange={e => setStartDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none" />
<span className="text-[#555] text-xs">to</span> <span className="text-[#555] text-xs">to</span>
<input type="date" value={endDate} onChange={e => setEndDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="date" value={endDate} onChange={e => setEndDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none" />
<button onClick={loadReport} disabled={loadingData} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Run Report</button> <button onClick={loadReport} disabled={loadingData} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Run Report</button>
</div> </div>
</div> </div>
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-20"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div> <div className="flex items-center justify-center py-20"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>
) : report ? ( ) : report ? (
<div className="space-y-4"> <div className="space-y-4">
{/* P&L Summary */} {/* P&L Summary */}
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4">
<p className="text-xs text-[#555]">Total Revenue</p> <p className="text-xs text-[#555]">Total Revenue</p>
<p className="text-2xl font-bold text-green-400 mt-1">{fmt(report.total_revenue_cents ?? 0)}</p> <p className="text-2xl font-bold text-green-400 mt-1">{fmt(report.total_revenue_cents ?? 0)}</p>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4">
<p className="text-xs text-[#555]">Total Expenses</p> <p className="text-xs text-[#555]">Total Expenses</p>
<p className="text-2xl font-bold text-red-400 mt-1">{fmt(report.total_expenses_cents ?? 0)}</p> <p className="text-2xl font-bold text-red-400 mt-1">{fmt(report.total_expenses_cents ?? 0)}</p>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4">
<p className="text-xs text-[#555]">Net Profit</p> <p className="text-xs text-[#555]">Net Profit</p>
<p className={`text-2xl font-bold mt-1 ${(report.net_profit_cents ?? 0) >= 0 ? 'text-blue-400' : 'text-red-400'}`}>{fmt(report.net_profit_cents ?? 0)}</p> <p className={`text-2xl font-bold mt-1 ${(report.net_profit_cents ?? 0) >= 0 ? 'text-blue-400' : 'text-red-400'}`}>{fmt(report.net_profit_cents ?? 0)}</p>
</div> </div>
</div> </div>
{/* Revenue by Site */} {/* Revenue by Site */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"><h2 className="text-sm font-semibold">Revenue by Site</h2></div> <div className="px-4 py-3 border-b border-[#DCE6F2]"><h2 className="text-sm font-semibold">Revenue by Site</h2></div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Site','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead> <thead><tr className="border-b border-[#FFFFFF]">{['Site','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody> <tbody>
{(report.by_site ?? []).map((r: any) => ( {(report.by_site ?? []).map((r: any) => (
<tr key={r.site} className="border-b border-[#1a1a1a]"> <tr key={r.site} className="border-b border-[#FFFFFF]">
<td className="px-4 py-2 text-white text-xs capitalize">{r.site}</td> <td className="px-4 py-2 text-white text-xs capitalize">{r.site}</td>
<td className="px-4 py-2 text-green-400 text-xs">{fmt(r.revenue_cents ?? 0)}</td> <td className="px-4 py-2 text-green-400 text-xs">{fmt(r.revenue_cents ?? 0)}</td>
</tr> </tr>
@@ -84,13 +84,13 @@ export default function ReportsPage() {
</div> </div>
{/* Top Clients */} {/* Top Clients */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"><h2 className="text-sm font-semibold">Top 10 Clients</h2></div> <div className="px-4 py-3 border-b border-[#DCE6F2]"><h2 className="text-sm font-semibold">Top 10 Clients</h2></div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Client','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead> <thead><tr className="border-b border-[#FFFFFF]">{['Client','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody> <tbody>
{(report.top_clients ?? []).map((c: any) => ( {(report.top_clients ?? []).map((c: any) => (
<tr key={c.id} className="border-b border-[#1a1a1a]"> <tr key={c.id} className="border-b border-[#FFFFFF]">
<td className="px-4 py-2 text-white text-xs">{c.company_name}</td> <td className="px-4 py-2 text-white text-xs">{c.company_name}</td>
<td className="px-4 py-2 text-green-400 text-xs">{fmt(c.revenue_cents ?? 0)}</td> <td className="px-4 py-2 text-green-400 text-xs">{fmt(c.revenue_cents ?? 0)}</td>
</tr> </tr>
@@ -100,13 +100,13 @@ export default function ReportsPage() {
</div> </div>
{/* Expenses by Category */} {/* Expenses by Category */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"><h2 className="text-sm font-semibold">Expenses by Category</h2></div> <div className="px-4 py-3 border-b border-[#DCE6F2]"><h2 className="text-sm font-semibold">Expenses by Category</h2></div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Category','Amount'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead> <thead><tr className="border-b border-[#FFFFFF]">{['Category','Amount'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody> <tbody>
{(report.by_category ?? []).map((c: any) => ( {(report.by_category ?? []).map((c: any) => (
<tr key={c.category} className="border-b border-[#1a1a1a]"> <tr key={c.category} className="border-b border-[#FFFFFF]">
<td className="px-4 py-2 text-white text-xs">{c.category?.replace(/_/g, ' ')}</td> <td className="px-4 py-2 text-white text-xs">{c.category?.replace(/_/g, ' ')}</td>
<td className="px-4 py-2 text-red-400 text-xs">{fmt(c.amount_cents ?? 0)}</td> <td className="px-4 py-2 text-red-400 text-xs">{fmt(c.amount_cents ?? 0)}</td>
</tr> </tr>

View File

@@ -97,7 +97,7 @@ export default function StaffPage() {
if (errors[errKey]) setErrors(p => { const n = { ...p }; delete n[errKey]; return n; }); if (errors[errKey]) setErrors(p => { const n = { ...p }; delete n[errKey]; return n; });
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -108,7 +108,7 @@ export default function StaffPage() {
<h1 className="text-xl font-bold">Sales Staff</h1> <h1 className="text-xl font-bold">Sales Staff</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Salesperson</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500">+ Add Salesperson</button>
</div> </div>
{/* Success Toast */} {/* Success Toast */}
@@ -120,7 +120,7 @@ export default function StaffPage() {
)} )}
{showForm && ( {showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Salesperson</h3> <h3 className="text-sm font-semibold">New Salesperson</h3>
{errors._form && ( {errors._form && (
@@ -140,7 +140,7 @@ export default function StaffPage() {
type={f.key === 'email' ? 'email' : 'text'} type={f.key === 'email' ? 'email' : 'text'}
value={(form as any)[f.key]} value={(form as any)[f.key]}
onChange={e => handleChange(f.key, e.target.value)} onChange={e => handleChange(f.key, e.target.value)}
className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#3b82f6] ${errors[f.key] ? 'border-red-500/60' : 'border-[#252525]'}`} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8] ${errors[f.key] ? 'border-red-500/60' : 'border-[#DCE6F2]'}`}
/> />
{errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>} {errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>}
</div> </div>
@@ -149,34 +149,34 @@ export default function StaffPage() {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<label className="text-xs text-[#888]">Commission Tiers ({new Date().getFullYear()})</label> <label className="text-xs text-[#888]">Commission Tiers ({new Date().getFullYear()})</label>
<button type="button" onClick={() => setTiers(t => [...t, { tier_order: t.length + 1, threshold_cents: '', rate: '' }])} className="text-xs text-[#3b82f6] hover:underline">+ Add Tier</button> <button type="button" onClick={() => setTiers(t => [...t, { tier_order: t.length + 1, threshold_cents: '', rate: '' }])} className="text-xs text-[#1D4ED8] hover:underline">+ Add Tier</button>
</div> </div>
{tiers.map((tier, i) => ( {tiers.map((tier, i) => (
<div key={i} className="flex gap-2 mb-2 items-start"> <div key={i} className="flex gap-2 mb-2 items-start">
<span className="text-xs text-[#555] w-12 pt-5">Tier {tier.tier_order}</span> <span className="text-xs text-[#555] w-12 pt-5">Tier {tier.tier_order}</span>
<div className="flex-1"> <div className="flex-1">
<label className="text-xs text-[#555] block mb-0.5">Threshold ($, blank = no cap)</label> <label className="text-xs text-[#555] block mb-0.5">Threshold ($, blank = no cap)</label>
<input type="number" step="0.01" value={tier.threshold_cents} onChange={e => handleTierChange(i, 'threshold_cents', e.target.value)} placeholder="100000" className="w-full px-2 py-1 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="number" step="0.01" value={tier.threshold_cents} onChange={e => handleTierChange(i, 'threshold_cents', e.target.value)} placeholder="100000" className="w-full px-2 py-1 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<label className="text-xs text-[#555] block mb-0.5">Rate (%) <span className="text-red-400">*</span></label> <label className="text-xs text-[#555] block mb-0.5">Rate (%) <span className="text-red-400">*</span></label>
<input type="number" step="0.1" value={tier.rate} onChange={e => handleTierChange(i, 'rate', e.target.value)} placeholder="25" className={`w-full px-2 py-1 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${errors[`tier_rate_${i}`] ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="number" step="0.1" value={tier.rate} onChange={e => handleTierChange(i, 'rate', e.target.value)} placeholder="25" className={`w-full px-2 py-1 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${errors[`tier_rate_${i}`] ? 'border-red-500/60' : 'border-[#DCE6F2]'}`} />
{errors[`tier_rate_${i}`] && <p className="mt-1 text-xs text-red-400">{errors[`tier_rate_${i}`]}</p>} {errors[`tier_rate_${i}`] && <p className="mt-1 text-xs text-red-400">{errors[`tier_rate_${i}`]}</p>}
</div> </div>
</div> </div>
))} ))}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Name','Email','Status','Commission YTD','Outstanding',''].map(h => ( {['Name','Email','Status','Commission YTD','Outstanding',''].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -188,16 +188,16 @@ export default function StaffPage() {
) : staff.length === 0 ? ( ) : staff.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No staff found</td></tr> <tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No staff found</td></tr>
) : staff.map((s: any) => ( ) : staff.map((s: any) => (
<tr key={s.id} className={`border-b border-[#1a1a1a] hover:bg-[#161616] ${s.status === 'terminated' ? 'opacity-50' : ''}`}> <tr key={s.id} className={`border-b border-[#FFFFFF] hover:bg-[#FFFFFF] ${s.status === 'terminated' ? 'opacity-50' : ''}`}>
<td className="px-4 py-2 text-white">{s.full_name}</td> <td className="px-4 py-2 text-white">{s.full_name}</td>
<td className="px-4 py-2 text-[#888] text-xs">{s.email}</td> <td className="px-4 py-2 text-[#888] text-xs">{s.email}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${s.status === 'active' ? 'bg-green-400/10 text-green-400' : s.status === 'terminated' ? 'bg-red-400/10 text-red-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{s.status}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${s.status === 'active' ? 'bg-green-400/10 text-green-400' : s.status === 'terminated' ? 'bg-red-400/10 text-red-400' : 'bg-[#FFFFFF] text-[#555]'}`}>{s.status}</span>
</td> </td>
<td className="px-4 py-2 text-green-400 text-xs">{fmt(s.commission_ytd ?? 0)}</td> <td className="px-4 py-2 text-green-400 text-xs">{fmt(s.commission_ytd ?? 0)}</td>
<td className="px-4 py-2 text-yellow-400 text-xs">{fmt(s.commission_outstanding ?? 0)}</td> <td className="px-4 py-2 text-yellow-400 text-xs">{fmt(s.commission_outstanding ?? 0)}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<Link href={`/admin/accounting/staff/${s.id}`} className="text-xs text-[#3b82f6] hover:underline">View</Link> <Link href={`/admin/accounting/staff/${s.id}`} className="text-xs text-[#1D4ED8] hover:underline">View</Link>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -9,7 +9,7 @@ const FLIGHT_COLORS: Record<string, string> = {
scheduled: 'bg-blue-400/10 text-blue-400', scheduled: 'bg-blue-400/10 text-blue-400',
paused: 'bg-gray-600/20 text-gray-400', paused: 'bg-gray-600/20 text-gray-400',
expired: 'bg-red-400/10 text-red-400', expired: 'bg-red-400/10 text-red-400',
cancelled: 'bg-[#1a1a1a] text-[#555]', cancelled: 'bg-[#FFFFFF] text-[#555]',
}; };
export default function AdOpsIOsPage() { export default function AdOpsIOsPage() {
@@ -73,7 +73,7 @@ export default function AdOpsIOsPage() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
const selectedProduct = products.find((p: any) => p.id === form.product_id); const selectedProduct = products.find((p: any) => p.id === form.product_id);
@@ -88,63 +88,63 @@ export default function AdOpsIOsPage() {
<h1 className="text-xl font-bold">Insertion Orders</h1> <h1 className="text-xl font-bold">Insertion Orders</h1>
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link> <Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ New IO Extension</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500">+ New IO Extension</button>
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option> <option value="">All Statuses</option>
{['scheduled','active','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)} {['scheduled','active','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)} {['avbeat','broadcastbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
{showForm && ( {showForm && (
<form onSubmit={handleCreate} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New IO Extension</h3> <h3 className="text-sm font-semibold">New IO Extension</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Existing IO ID *</label> <label className="text-xs text-[#888] block mb-1">Existing IO ID *</label>
<input required value={form.existing_io_id} onChange={e => setForm(p => ({ ...p, existing_io_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input required value={form.existing_io_id} onChange={e => setForm(p => ({ ...p, existing_io_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Client</label> <label className="text-xs text-[#888] block mb-1">Client</label>
<select value={form.client_id} onChange={e => setForm(p => ({ ...p, client_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={form.client_id} onChange={e => setForm(p => ({ ...p, client_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
<option value="">Select</option> <option value="">Select</option>
{clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)} {clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Salesperson</label> <label className="text-xs text-[#888] block mb-1">Salesperson</label>
<select value={form.staff_id} onChange={e => setForm(p => ({ ...p, staff_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={form.staff_id} onChange={e => setForm(p => ({ ...p, staff_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
<option value="">None</option> <option value="">None</option>
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)} {staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Start Date *</label> <label className="text-xs text-[#888] block mb-1">Start Date *</label>
<input type="date" required value={form.start_date} onChange={e => setForm(p => ({ ...p, start_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="date" required value={form.start_date} onChange={e => setForm(p => ({ ...p, start_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">End Date</label> <label className="text-xs text-[#888] block mb-1">End Date</label>
<input type="date" value={form.end_date} onChange={e => setForm(p => ({ ...p, end_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="date" value={form.end_date} onChange={e => setForm(p => ({ ...p, end_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Flight Status</label> <label className="text-xs text-[#888] block mb-1">Flight Status</label>
<select value={form.flight_status} onChange={e => setForm(p => ({ ...p, flight_status: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={form.flight_status} onChange={e => setForm(p => ({ ...p, flight_status: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
{['scheduled','active','paused'].map(s => <option key={s} value={s}>{s}</option>)} {['scheduled','active','paused'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Banner Image URL</label> <label className="text-xs text-[#888] block mb-1">Banner Image URL</label>
<input value={form.banner_image_url} onChange={e => setForm(p => ({ ...p, banner_image_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.banner_image_url} onChange={e => setForm(p => ({ ...p, banner_image_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Click-Through URL</label> <label className="text-xs text-[#888] block mb-1">Click-Through URL</label>
<input value={form.click_through_url} onChange={e => setForm(p => ({ ...p, click_through_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.click_through_url} onChange={e => setForm(p => ({ ...p, click_through_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div className="flex items-center gap-3 pt-4"> <div className="flex items-center gap-3 pt-4">
<label className="flex items-center gap-2 text-xs text-[#888] cursor-pointer"> <label className="flex items-center gap-2 text-xs text-[#888] cursor-pointer">
@@ -159,47 +159,47 @@ export default function AdOpsIOsPage() {
{form.is_comp && ( {form.is_comp && (
<div> <div>
<label className="text-xs text-[#888] block mb-1">Comp Reason</label> <label className="text-xs text-[#888] block mb-1">Comp Reason</label>
<input value={form.comp_reason} onChange={e => setForm(p => ({ ...p, comp_reason: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.comp_reason} onChange={e => setForm(p => ({ ...p, comp_reason: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
)} )}
{isEmailProduct && ( {isEmailProduct && (
<> <>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Email HTML URL</label> <label className="text-xs text-[#888] block mb-1">Email HTML URL</label>
<input value={form.email_html_url} onChange={e => setForm(p => ({ ...p, email_html_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.email_html_url} onChange={e => setForm(p => ({ ...p, email_html_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Scheduled Send Date</label> <label className="text-xs text-[#888] block mb-1">Scheduled Send Date</label>
<input type="date" value={form.email_scheduled_date} onChange={e => setForm(p => ({ ...p, email_scheduled_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="date" value={form.email_scheduled_date} onChange={e => setForm(p => ({ ...p, email_scheduled_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">List Segment</label> <label className="text-xs text-[#888] block mb-1">List Segment</label>
<input value={form.email_list_segment} onChange={e => setForm(p => ({ ...p, email_list_segment: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.email_list_segment} onChange={e => setForm(p => ({ ...p, email_list_segment: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</> </>
)} )}
{isAdvertorial && ( {isAdvertorial && (
<div> <div>
<label className="text-xs text-[#888] block mb-1">Page URL (permanent)</label> <label className="text-xs text-[#888] block mb-1">Page URL (permanent)</label>
<input value={form.page_url} onChange={e => setForm(p => ({ ...p, page_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.page_url} onChange={e => setForm(p => ({ ...p, page_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
)} )}
<div className="col-span-2 md:col-span-3"> <div className="col-span-2 md:col-span-3">
<label className="text-xs text-[#888] block mb-1">Notes</label> <label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create IO Extension'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create IO Extension'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['IO ID','Client','Product','Start','End','Status','Comp','Paid','Actions'].map(h => ( {['IO ID','Client','Product','Start','End','Status','Comp','Paid','Actions'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))} ))}
@@ -211,8 +211,8 @@ export default function AdOpsIOsPage() {
) : ios.length === 0 ? ( ) : ios.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No IO extensions found</td></tr> <tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No IO extensions found</td></tr>
) : ios.map((io: any) => ( ) : ios.map((io: any) => (
<tr key={io.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={io.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-[#3b82f6] font-mono text-xs">{io.existing_io_id}</td> <td className="px-3 py-2 text-[#1D4ED8] font-mono text-xs">{io.existing_io_id}</td>
<td className="px-3 py-2 text-white text-xs">{io.rmp_clients?.company_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{io.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{io.rmp_products?.name ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{io.rmp_products?.name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{io.start_date}</td> <td className="px-3 py-2 text-[#888] text-xs">{io.start_date}</td>
@@ -227,7 +227,7 @@ export default function AdOpsIOsPage() {
<span className={`px-2 py-0.5 rounded-full text-xs ${io.is_paid ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{io.is_paid ? 'Paid' : 'Unpaid'}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${io.is_paid ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{io.is_paid ? 'Paid' : 'Unpaid'}</span>
</td> </td>
<td className="px-3 py-2 flex gap-2"> <td className="px-3 py-2 flex gap-2">
<Link href={`/admin/adops/ios/${io.id}`} className="text-xs text-[#3b82f6] hover:underline">View</Link> <Link href={`/admin/adops/ios/${io.id}`} className="text-xs text-[#1D4ED8] hover:underline">View</Link>
{io.flight_status === 'expired' && ( {io.flight_status === 'expired' && (
<button onClick={() => { const r = prompt('Reactivation reason?'); if (r) updateStatus(io.id, 'active', r); }} className="text-xs text-green-400 hover:underline">Reactivate</button> <button onClick={() => { const r = prompt('Reactivation reason?'); if (r) updateStatus(io.id, 'active', r); }} className="text-xs text-green-400 hover:underline">Reactivate</button>
)} )}

View File

@@ -22,7 +22,7 @@ export default function AdOpsNotificationsPage() {
useEffect(() => { if (user) load(); }, [user, load]); useEffect(() => { if (user) load(); }, [user, load]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -33,10 +33,10 @@ export default function AdOpsNotificationsPage() {
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link> <Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Type','IO','Recipient','Via','Sent At','Status','Message'].map(h => ( {['Type','IO','Recipient','Via','Sent At','Status','Message'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -48,9 +48,9 @@ export default function AdOpsNotificationsPage() {
) : notifications.length === 0 ? ( ) : notifications.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No notifications yet</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No notifications yet</td></tr>
) : notifications.map((n: any) => ( ) : notifications.map((n: any) => (
<tr key={n.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={n.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-[#888] text-xs">{n.notification_type?.replace(/_/g, ' ')}</td> <td className="px-3 py-2 text-[#888] text-xs">{n.notification_type?.replace(/_/g, ' ')}</td>
<td className="px-3 py-2 text-[#3b82f6] font-mono text-xs">{n.io_extension_id?.slice(0, 8) ?? '—'}</td> <td className="px-3 py-2 text-[#1D4ED8] font-mono text-xs">{n.io_extension_id?.slice(0, 8) ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{n.recipient_email ?? n.recipient_phone ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{n.recipient_email ?? n.recipient_phone ?? '—'}</td>
<td className="px-3 py-2 text-[#555] text-xs">{n.sent_via}</td> <td className="px-3 py-2 text-[#555] text-xs">{n.sent_via}</td>
<td className="px-3 py-2 text-[#555] text-xs">{n.sent_at ? new Date(n.sent_at).toLocaleString() : '—'}</td> <td className="px-3 py-2 text-[#555] text-xs">{n.sent_at ? new Date(n.sent_at).toLocaleString() : '—'}</td>

View File

@@ -9,7 +9,7 @@ const FLIGHT_COLORS: Record<string, string> = {
scheduled: 'bg-blue-400/10 text-blue-400 border-blue-400/20', scheduled: 'bg-blue-400/10 text-blue-400 border-blue-400/20',
paused: 'bg-gray-600/20 text-gray-400 border-gray-600/20', paused: 'bg-gray-600/20 text-gray-400 border-gray-600/20',
expired: 'bg-red-400/10 text-red-400 border-red-400/20', expired: 'bg-red-400/10 text-red-400 border-red-400/20',
cancelled: 'bg-[#1a1a1a] text-[#555] border-[#252525]', cancelled: 'bg-[#FFFFFF] text-[#555] border-[#DCE6F2]',
}; };
export default function AdOpsDashboardPage() { export default function AdOpsDashboardPage() {
@@ -43,7 +43,7 @@ export default function AdOpsDashboardPage() {
useEffect(() => { if (user) load(); }, [user, load]); useEffect(() => { if (user) load(); }, [user, load]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -64,7 +64,7 @@ export default function AdOpsDashboardPage() {
{ label: 'Unpaid Active', key: 'unpaid_active', color: 'text-orange-400' }, { label: 'Unpaid Active', key: 'unpaid_active', color: 'text-orange-400' },
{ label: 'Email Blasts Scheduled', key: 'email_scheduled', color: 'text-purple-400' }, { label: 'Email Blasts Scheduled', key: 'email_scheduled', color: 'text-purple-400' },
].map(c => ( ].map(c => (
<div key={c.key} className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div key={c.key} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4">
<p className={`text-2xl font-bold ${c.color}`}>{stats?.[c.key] ?? 0}</p> <p className={`text-2xl font-bold ${c.color}`}>{stats?.[c.key] ?? 0}</p>
<p className="text-xs text-[#888] mt-1">{c.label}</p> <p className="text-xs text-[#888] mt-1">{c.label}</p>
</div> </div>
@@ -73,25 +73,25 @@ export default function AdOpsDashboardPage() {
{/* Filters */} {/* Filters */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)} {['avbeat','broadcastbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option> <option value="">All Statuses</option>
{['active','scheduled','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)} {['active','scheduled','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
{/* Active Flights Table */} {/* Active Flights Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<div className="px-4 py-3 border-b border-[#252525] flex items-center justify-between"> <div className="px-4 py-3 border-b border-[#DCE6F2] flex items-center justify-between">
<h2 className="text-sm font-semibold">Active Flights</h2> <h2 className="text-sm font-semibold">Active Flights</h2>
<Link href="/admin/adops/ios" className="text-xs text-[#3b82f6] hover:underline">View All IOs </Link> <Link href="/admin/adops/ios" className="text-xs text-[#1D4ED8] hover:underline">View All IOs </Link>
</div> </div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Client','Product','Site','Start','End','Status','Salesperson','Paid',''].map(h => ( {['Client','Product','Site','Start','End','Status','Salesperson','Paid',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))} ))}
@@ -103,7 +103,7 @@ export default function AdOpsDashboardPage() {
) : flights.length === 0 ? ( ) : flights.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No flights found</td></tr> <tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No flights found</td></tr>
) : flights.map((f: any) => ( ) : flights.map((f: any) => (
<tr key={f.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={f.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-white text-xs">{f.rmp_clients?.company_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{f.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{f.rmp_products?.name ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{f.rmp_products?.name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{f.rmp_orders?.site ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{f.rmp_orders?.site ?? '—'}</td>
@@ -117,7 +117,7 @@ export default function AdOpsDashboardPage() {
<span className={`px-2 py-0.5 rounded-full text-xs ${f.is_paid ? 'bg-green-400/10 text-green-400' : f.is_comp ? 'bg-blue-400/10 text-blue-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{f.is_comp ? 'Comp' : f.is_paid ? 'Paid' : 'Unpaid'}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${f.is_paid ? 'bg-green-400/10 text-green-400' : f.is_comp ? 'bg-blue-400/10 text-blue-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{f.is_comp ? 'Comp' : f.is_paid ? 'Paid' : 'Unpaid'}</span>
</td> </td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<Link href={`/admin/adops/ios/${f.id}`} className="text-xs text-[#3b82f6] hover:underline">View</Link> <Link href={`/admin/adops/ios/${f.id}`} className="text-xs text-[#1D4ED8] hover:underline">View</Link>
</td> </td>
</tr> </tr>
))} ))}
@@ -126,13 +126,13 @@ export default function AdOpsDashboardPage() {
</div> </div>
{/* Email Schedule */} {/* Email Schedule */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"> <div className="px-4 py-3 border-b border-[#DCE6F2]">
<h2 className="text-sm font-semibold">Email Distribution Schedule</h2> <h2 className="text-sm font-semibold">Email Distribution Schedule</h2>
</div> </div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Client','Type','Site','Scheduled Date','Status',''].map(h => ( {['Client','Type','Site','Scheduled Date','Status',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -142,7 +142,7 @@ export default function AdOpsDashboardPage() {
{emailSchedule.length === 0 ? ( {emailSchedule.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-6 text-center text-[#555] text-xs">No email blasts scheduled</td></tr> <tr><td colSpan={6} className="px-4 py-6 text-center text-[#555] text-xs">No email blasts scheduled</td></tr>
) : emailSchedule.map((e: any) => ( ) : emailSchedule.map((e: any) => (
<tr key={e.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={e.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-white text-xs">{e.rmp_clients?.company_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{e.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{e.rmp_products?.product_type?.replace(/_/g, ' ')}</td> <td className="px-3 py-2 text-[#888] text-xs">{e.rmp_products?.product_type?.replace(/_/g, ' ')}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{e.rmp_orders?.site ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{e.rmp_orders?.site ?? '—'}</td>
@@ -151,7 +151,7 @@ export default function AdOpsDashboardPage() {
<span className={`px-2 py-0.5 rounded-full text-xs ${e.email_sent_at ? 'bg-green-400/10 text-green-400' : 'bg-blue-400/10 text-blue-400'}`}>{e.email_sent_at ? 'Sent' : 'Pending'}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${e.email_sent_at ? 'bg-green-400/10 text-green-400' : 'bg-blue-400/10 text-blue-400'}`}>{e.email_sent_at ? 'Sent' : 'Pending'}</span>
</td> </td>
<td className="px-3 py-2"> <td className="px-3 py-2">
{e.email_html_url && <a href={e.email_html_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#3b82f6] hover:underline">Preview</a>} {e.email_html_url && <a href={e.email_html_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#1D4ED8] hover:underline">Preview</a>}
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -49,7 +49,7 @@ export default function AdOpsProductsPage() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -60,62 +60,62 @@ export default function AdOpsProductsPage() {
<h1 className="text-xl font-bold">Product Catalog</h1> <h1 className="text-xl font-bold">Product Catalog</h1>
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link> <Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Product</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500">+ Add Product</button>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
{showForm && ( {showForm && (
<form onSubmit={handleCreate} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Product</h3> <h3 className="text-sm font-semibold">New Product</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Site *</label> <label className="text-xs text-[#888] block mb-1">Site *</label>
<select required value={form.site} onChange={e => setForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select required value={form.site} onChange={e => setForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
<option value="">Select</option> <option value="">Select</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Product Type *</label> <label className="text-xs text-[#888] block mb-1">Product Type *</label>
<select required value={form.product_type} onChange={e => setForm(p => ({ ...p, product_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select required value={form.product_type} onChange={e => setForm(p => ({ ...p, product_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
<option value="">Select</option> <option value="">Select</option>
{PRODUCT_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)} {PRODUCT_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Name *</label> <label className="text-xs text-[#888] block mb-1">Name *</label>
<input required value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input required value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Dimensions</label> <label className="text-xs text-[#888] block mb-1">Dimensions</label>
<input value={form.dimensions} onChange={e => setForm(p => ({ ...p, dimensions: e.target.value }))} placeholder="e.g. 300x250" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.dimensions} onChange={e => setForm(p => ({ ...p, dimensions: e.target.value }))} placeholder="e.g. 300x250" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Duration (days)</label> <label className="text-xs text-[#888] block mb-1">Duration (days)</label>
<input type="number" value={form.duration_days} onChange={e => setForm(p => ({ ...p, duration_days: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input type="number" value={form.duration_days} onChange={e => setForm(p => ({ ...p, duration_days: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div className="col-span-2 md:col-span-3"> <div className="col-span-2 md:col-span-3">
<label className="text-xs text-[#888] block mb-1">Description</label> <label className="text-xs text-[#888] block mb-1">Description</label>
<input value={form.description} onChange={e => setForm(p => ({ ...p, description: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={form.description} onChange={e => setForm(p => ({ ...p, description: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Product'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Product'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Site','Type','Name','Dimensions','Duration','Active',''].map(h => ( {['Site','Type','Name','Dimensions','Duration','Active',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -127,17 +127,17 @@ export default function AdOpsProductsPage() {
) : products.length === 0 ? ( ) : products.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No products found</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No products found</td></tr>
) : products.map((p: any) => ( ) : products.map((p: any) => (
<tr key={p.id} className={`border-b border-[#1a1a1a] hover:bg-[#161616] ${!p.active ? 'opacity-50' : ''}`}> <tr key={p.id} className={`border-b border-[#FFFFFF] hover:bg-[#FFFFFF] ${!p.active ? 'opacity-50' : ''}`}>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{p.site}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{p.site}</td>
<td className="px-3 py-2 text-[#555] text-xs">{p.product_type?.replace(/_/g, ' ')}</td> <td className="px-3 py-2 text-[#555] text-xs">{p.product_type?.replace(/_/g, ' ')}</td>
<td className="px-3 py-2 text-white text-xs">{p.name}</td> <td className="px-3 py-2 text-white text-xs">{p.name}</td>
<td className="px-3 py-2 text-[#888] text-xs">{p.dimensions ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs">{p.dimensions ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{p.duration_days ? `${p.duration_days}d` : '—'}</td> <td className="px-3 py-2 text-[#888] text-xs">{p.duration_days ? `${p.duration_days}d` : '—'}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${p.active ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{p.active ? 'Active' : 'Inactive'}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${p.active ? 'bg-green-400/10 text-green-400' : 'bg-[#FFFFFF] text-[#555]'}`}>{p.active ? 'Active' : 'Inactive'}</span>
</td> </td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<button onClick={() => toggleActive(p.id, p.active)} className="text-xs text-[#3b82f6] hover:underline">{p.active ? 'Deactivate' : 'Activate'}</button> <button onClick={() => toggleActive(p.id, p.active)} className="text-xs text-[#1D4ED8] hover:underline">{p.active ? 'Deactivate' : 'Activate'}</button>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -64,7 +64,7 @@ interface MonthlyTrend {
// ─── Mock / derived data helpers ────────────────────────────────────────────── // ─── Mock / derived data helpers ──────────────────────────────────────────────
const CATEGORY_COLORS = [ const CATEGORY_COLORS = [
'#3b82f6', '#f59e0b', '#10b981', '#8b5cf6', '#1D4ED8', '#f59e0b', '#10b981', '#8b5cf6',
'#ef4444', '#06b6d4', '#f97316', '#84cc16', '#ef4444', '#06b6d4', '#f97316', '#84cc16',
]; ];
@@ -78,9 +78,9 @@ interface StatCardProps {
icon: React.ReactNode; icon: React.ReactNode;
} }
function StatCard({ label, value, sub, accent = '#3b82f6', icon }: StatCardProps) { function StatCard({ label, value, sub, accent = '#1D4ED8', icon }: StatCardProps) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex items-start gap-4"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5 flex items-start gap-4">
<div <div
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0" className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
style={{ background: `${accent}18` }} style={{ background: `${accent}18` }}
@@ -112,7 +112,7 @@ function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }
function ChartTooltip({ active, payload, label }: any) { function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null; if (!active || !payload?.length) return null;
return ( return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl"> <div className="bg-[#FFFFFF] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1">{label}</p>} {label && <p className="text-[#888] mb-1">{label}</p>}
{payload.map((entry: any, i: number) => ( {payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5"> <p key={i} style={{ color: entry.color }} className="leading-5">
@@ -252,7 +252,7 @@ export default function AdminAnalyticsPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading analytics</p> <p className="text-[#555] text-sm font-body">Loading analytics</p>
</div> </div>
</div> </div>
@@ -266,7 +266,7 @@ export default function AdminAnalyticsPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-white"> <div className="min-h-screen bg-[#0a0a0a] text-white">
{/* ── Page Header ─────────────────────────────────────────────────── */} {/* ── Page Header ─────────────────────────────────────────────────── */}
<div className="bg-[#111] border-b border-[#252525]"> <div className="bg-[#111] border-b border-[#DCE6F2]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-5 flex items-center justify-between gap-4"> <div className="max-w-7xl mx-auto px-4 sm:px-6 py-5 flex items-center justify-between gap-4">
<div> <div>
<h1 className="text-white text-xl font-bold font-heading uppercase tracking-wider">Analytics</h1> <h1 className="text-white text-xl font-bold font-heading uppercase tracking-wider">Analytics</h1>
@@ -275,20 +275,20 @@ export default function AdminAnalyticsPage() {
</p> </p>
</div> </div>
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<Link href="/admin/articles" className="text-[#555] hover:text-[#3b82f6] text-xs font-body uppercase tracking-wider transition-colors"> <Link href="/admin/articles" className="text-[#555] hover:text-[#1D4ED8] text-xs font-body uppercase tracking-wider transition-colors">
Articles Articles
</Link> </Link>
<span className="text-[#333]">·</span> <span className="text-[#333]">·</span>
<Link href="/admin/users" className="text-[#555] hover:text-[#3b82f6] text-xs font-body uppercase tracking-wider transition-colors"> <Link href="/admin/users" className="text-[#555] hover:text-[#1D4ED8] text-xs font-body uppercase tracking-wider transition-colors">
Users Users
</Link> </Link>
<span className="text-[#333]">·</span> <span className="text-[#333]">·</span>
<Link href="/admin/import" className="text-[#555] hover:text-[#3b82f6] text-xs font-body uppercase tracking-wider transition-colors"> <Link href="/admin/import" className="text-[#555] hover:text-[#1D4ED8] text-xs font-body uppercase tracking-wider transition-colors">
WP Import WP Import
</Link> </Link>
<button <button
onClick={fetchAnalytics} onClick={fetchAnalytics}
className="ml-2 px-3 py-1.5 bg-[#1a1a1a] border border-[#333] hover:border-[#3b82f6] text-[#888] hover:text-[#3b82f6] text-xs font-body rounded transition-colors" className="ml-2 px-3 py-1.5 bg-[#FFFFFF] border border-[#333] hover:border-[#1D4ED8] text-[#888] hover:text-[#1D4ED8] text-xs font-body rounded transition-colors"
> >
Refresh Refresh
</button> </button>
@@ -312,7 +312,7 @@ export default function AdminAnalyticsPage() {
label="Total Articles" label="Total Articles"
value={metrics?.totalArticles ?? 0} value={metrics?.totalArticles ?? 0}
icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>} icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>}
accent="#3b82f6" accent="#1D4ED8"
/> />
<StatCard <StatCard
label="Published" label="Published"
@@ -361,7 +361,7 @@ export default function AdminAnalyticsPage() {
value={`${publishRate}%`} value={`${publishRate}%`}
sub="Published / total" sub="Published / total"
icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>} icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>}
accent="#3b82f6" accent="#1D4ED8"
/> />
</div> </div>
</section> </section>
@@ -369,7 +369,7 @@ export default function AdminAnalyticsPage() {
{/* ── Monthly Trend ────────────────────────────────────────────── */} {/* ── Monthly Trend ────────────────────────────────────────────── */}
<section> <section>
<SectionHeader title="Monthly Trends" subtitle="Published, imported, and native articles over the last 6 months" /> <SectionHeader title="Monthly Trends" subtitle="Published, imported, and native articles over the last 6 months" />
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5">
{monthlyTrends.length > 0 ? ( {monthlyTrends.length > 0 ? (
<ResponsiveContainer width="100%" height={240}> <ResponsiveContainer width="100%" height={240}>
<LineChart data={monthlyTrends} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}> <LineChart data={monthlyTrends} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
@@ -395,7 +395,7 @@ export default function AdminAnalyticsPage() {
{/* Author Contributions */} {/* Author Contributions */}
<section> <section>
<SectionHeader title="Author Contributions" subtitle="Top contributors by article count" /> <SectionHeader title="Author Contributions" subtitle="Top contributors by article count" />
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5">
{authorStats.length > 0 ? ( {authorStats.length > 0 ? (
<ResponsiveContainer width="100%" height={280}> <ResponsiveContainer width="100%" height={280}>
<BarChart <BarChart
@@ -429,7 +429,7 @@ export default function AdminAnalyticsPage() {
{/* Top Categories */} {/* Top Categories */}
<section> <section>
<SectionHeader title="Top-Performing Categories" subtitle="Article distribution by category" /> <SectionHeader title="Top-Performing Categories" subtitle="Article distribution by category" />
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5">
{categoryStats.length > 0 ? ( {categoryStats.length > 0 ? (
<> <>
<ResponsiveContainer width="100%" height={200}> <ResponsiveContainer width="100%" height={200}>
@@ -481,7 +481,7 @@ export default function AdminAnalyticsPage() {
<SectionHeader title="Import Success Rates" subtitle="Publishing success across content sources" /> <SectionHeader title="Import Success Rates" subtitle="Publishing success across content sources" />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{importStats.map((stat) => ( {importStats.map((stat) => (
<div key={stat.label} className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div key={stat.label} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<p className="text-white text-sm font-bold font-heading uppercase tracking-wider">{stat.label}</p> <p className="text-white text-sm font-bold font-heading uppercase tracking-wider">{stat.label}</p>
<span <span
@@ -526,7 +526,7 @@ export default function AdminAnalyticsPage() {
{/* ── Author Detail Table ──────────────────────────────────────── */} {/* ── Author Detail Table ──────────────────────────────────────── */}
<section> <section>
<SectionHeader title="Author Detail Breakdown" subtitle="Per-author article status distribution" /> <SectionHeader title="Author Detail Breakdown" subtitle="Per-author article status distribution" />
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm font-body"> <table className="w-full text-sm font-body">
<thead> <thead>
@@ -550,7 +550,7 @@ export default function AdminAnalyticsPage() {
return ( return (
<tr <tr
key={a.author_name} key={a.author_name}
className={`border-b border-[#1a1a1a] hover:bg-[#151515] transition-colors ${i % 2 === 0 ? '' : 'bg-[#0d0d0d]'}`} className={`border-b border-[#FFFFFF] hover:bg-[#151515] transition-colors ${i % 2 === 0 ? '' : 'bg-[#F8FAFC]'}`}
> >
<td className="px-4 py-3 text-white font-medium">{a.author_name}</td> <td className="px-4 py-3 text-white font-medium">{a.author_name}</td>
<td className="px-4 py-3 text-right text-[#888]">{a.total}</td> <td className="px-4 py-3 text-right text-[#888]">{a.total}</td>

View File

@@ -6,6 +6,7 @@ import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import AppImage from '@/components/ui/AppImage'; import AppImage from '@/components/ui/AppImage';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import AdminShell from '@/components/admin/AdminShell';
const RichTextEditor = dynamic(() => import('@/components/RichTextEditor'), { ssr: false }); const RichTextEditor = dynamic(() => import('@/components/RichTextEditor'), { ssr: false });
@@ -33,7 +34,7 @@ const STATUS_COLORS: Record<string, string> = {
published: 'text-green-400 bg-green-400/10', published: 'text-green-400 bg-green-400/10',
draft: 'text-yellow-400 bg-yellow-400/10', draft: 'text-yellow-400 bg-yellow-400/10',
pending: 'text-orange-400 bg-orange-400/10', pending: 'text-orange-400 bg-orange-400/10',
archived: 'text-[#555] bg-[#1a1a1a]', archived: 'text-[#555] bg-[#FFFFFF]',
}; };
const STATUS_LABELS: Record<string, string> = { const STATUS_LABELS: Record<string, string> = {
@@ -384,12 +385,13 @@ export default function AdminArticlesPage() {
if (loading || (!user && !loading)) { if (loading || (!user && !loading)) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
return ( return (
<AdminShell title="Articles">
<div className="min-h-screen bg-[#0a0a0a] text-[#cccccc]"> <div className="min-h-screen bg-[#0a0a0a] text-[#cccccc]">
{/* Notification */} {/* Notification */}
{notification && ( {notification && (
@@ -406,10 +408,10 @@ export default function AdminArticlesPage() {
)} )}
{/* Page Header */} {/* Page Header */}
<div className="bg-[#111111] border-b border-[#252525]"> <div className="bg-[#F8FAFC] border-b border-[#DCE6F2]">
<div className="max-w-7xl mx-auto px-4 py-5"> <div className="max-w-7xl mx-auto px-4 py-5">
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-3 mb-1">
<Link href="/home-page" className="text-[#555] hover:text-[#3b82f6] transition-colors text-sm">Home</Link> <Link href="/home-page" className="text-[#555] hover:text-[#1D4ED8] transition-colors text-sm">Home</Link>
<span className="text-[#333]">/</span> <span className="text-[#333]">/</span>
<span className="text-[#888] text-sm">Admin</span> <span className="text-[#888] text-sm">Admin</span>
<span className="text-[#333]">/</span> <span className="text-[#333]">/</span>
@@ -423,13 +425,13 @@ export default function AdminArticlesPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setShowNewArticle(true)} onClick={() => setShowNewArticle(true)}
className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#3b82f6] hover:bg-[#2563eb] rounded px-3 py-2 transition-colors"> className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#1D4ED8] hover:bg-[#1E3A8A] rounded px-3 py-2 transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>
New Article New Article
</button> </button>
<Link <Link
href="/admin/import" href="/admin/import"
className="flex items-center gap-1.5 text-xs text-[#666] hover:text-[#3b82f6] border border-[#252525] hover:border-[#3b82f6] rounded px-3 py-2 transition-colors"> className="flex items-center gap-1.5 text-xs text-[#666] hover:text-[#1D4ED8] border border-[#DCE6F2] hover:border-[#1D4ED8] rounded px-3 py-2 transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></svg>
WP Import WP Import
</Link> </Link>
@@ -471,7 +473,7 @@ export default function AdminArticlesPage() {
{ label: 'Drafts', value: counts.draft, color: 'text-yellow-400' }, { label: 'Drafts', value: counts.draft, color: 'text-yellow-400' },
{ label: 'Archived', value: counts.archived, color: 'text-[#555]' }, { label: 'Archived', value: counts.archived, color: 'text-[#555]' },
].map((s) => ( ].map((s) => (
<div key={s.label} className="bg-[#111] border border-[#252525] rounded-lg p-4 text-center"> <div key={s.label} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 text-center">
<p className={`text-2xl font-bold ${s.color}`}>{s.value}</p> <p className={`text-2xl font-bold ${s.color}`}>{s.value}</p>
<p className="text-[#555] text-xs mt-0.5 uppercase tracking-wider">{s.label}</p> <p className="text-[#555] text-xs mt-0.5 uppercase tracking-wider">{s.label}</p>
</div> </div>
@@ -479,7 +481,7 @@ export default function AdminArticlesPage() {
</div> </div>
{/* Status Tabs */} {/* Status Tabs */}
<div className="flex items-center gap-1 border-b border-[#252525] overflow-x-auto"> <div className="flex items-center gap-1 border-b border-[#DCE6F2] overflow-x-auto">
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.key} key={tab.key}
@@ -492,7 +494,7 @@ export default function AdminArticlesPage() {
{tab.label} {tab.label}
{tab.key !== 'all' && ( {tab.key !== 'all' && (
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${ <span className={`text-[10px] px-1.5 py-0.5 rounded-full ${
activeTab === tab.key ? 'bg-current/10' : 'bg-[#1a1a1a]' activeTab === tab.key ? 'bg-current/10' : 'bg-[#FFFFFF]'
}`}> }`}>
{counts[tab.key]} {counts[tab.key]}
</span> </span>
@@ -512,13 +514,13 @@ export default function AdminArticlesPage() {
placeholder="Search articles..." placeholder="Search articles..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#3b82f6] placeholder-[#444]" className="w-full bg-[#111] border border-[#DCE6F2] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#1D4ED8] placeholder-[#444]"
/> />
</div> </div>
<select <select
value={sourceFilter} value={sourceFilter}
onChange={(e) => setSourceFilter(e.target.value as SourceFilter)} onChange={(e) => setSourceFilter(e.target.value as SourceFilter)}
className="bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6]"> className="bg-[#111] border border-[#DCE6F2] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8]">
<option value="all">All Sources</option> <option value="all">All Sources</option>
<option value="imported">Imported (WP)</option> <option value="imported">Imported (WP)</option>
<option value="native">Native</option> <option value="native">Native</option>
@@ -527,13 +529,13 @@ export default function AdminArticlesPage() {
{/* Bulk Actions Bar */} {/* Bulk Actions Bar */}
{selectedIds.size > 0 && ( {selectedIds.size > 0 && (
<div className="flex items-center gap-3 bg-[#0d1a2e] border border-[#1e3a5f] rounded-lg px-4 py-3"> <div className="flex items-center gap-3 bg-[#0d1a2e] border border-[#DCE6F2] rounded-lg px-4 py-3">
<span className="text-[#3b82f6] text-sm font-bold">{selectedIds.size} selected</span> <span className="text-[#1D4ED8] text-sm font-bold">{selectedIds.size} selected</span>
<div className="flex-1" /> <div className="flex-1" />
<select <select
value={bulkAction} value={bulkAction}
onChange={(e) => setBulkAction(e.target.value as BulkAction | '')} onChange={(e) => setBulkAction(e.target.value as BulkAction | '')}
className="bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-1.5 focus:outline-none focus:border-[#3b82f6]"> className="bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-1.5 focus:outline-none focus:border-[#1D4ED8]">
<option value="">Bulk Action</option> <option value="">Bulk Action</option>
<option value="publish">Publish</option> <option value="publish">Publish</option>
<option value="pending">Submit for Review</option> <option value="pending">Submit for Review</option>
@@ -544,7 +546,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={handleBulkAction} onClick={handleBulkAction}
disabled={!bulkAction || bulkProcessing} disabled={!bulkAction || bulkProcessing}
className="flex items-center gap-1.5 bg-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] disabled:cursor-not-allowed text-white text-sm font-bold px-4 py-1.5 rounded transition-colors"> className="flex items-center gap-1.5 bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:bg-[#DCE6F2] disabled:cursor-not-allowed text-white text-sm font-bold px-4 py-1.5 rounded transition-colors">
{bulkProcessing ? ( {bulkProcessing ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : null} ) : null}
@@ -568,15 +570,15 @@ export default function AdminArticlesPage() {
<span className="text-orange-400 text-sm font-bold">Approval Queue</span> <span className="text-orange-400 text-sm font-bold">Approval Queue</span>
<span className="text-[#555] text-xs ml-1"> Review and approve or reject each submission</span> <span className="text-[#555] text-xs ml-1"> Review and approve or reject each submission</span>
</div> </div>
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#FFFFFF]">
{pendingArticles.map((article) => ( {pendingArticles.map((article) => (
<div key={article.id} className={`flex items-start gap-4 px-4 py-4 hover:bg-[#141414] transition-colors ${processingIds.has(article.id) ? 'opacity-50 pointer-events-none' : ''}`}> <div key={article.id} className={`flex items-start gap-4 px-4 py-4 hover:bg-[#141414] transition-colors ${processingIds.has(article.id) ? 'opacity-50 pointer-events-none' : ''}`}>
{article.featured_image ? ( {article.featured_image ? (
<div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#1a1a1a]"> <div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#FFFFFF]">
<AppImage src={article.featured_image} alt={article.featured_image_alt || article.title} width={64} height={48} className="w-full h-full object-cover" /> <AppImage src={article.featured_image} alt={article.featured_image_alt || article.title} width={64} height={48} className="w-full h-full object-cover" />
</div> </div>
) : ( ) : (
<div className="flex-shrink-0 w-16 h-12 rounded bg-[#1a1a1a] flex items-center justify-center"> <div className="flex-shrink-0 w-16 h-12 rounded bg-[#FFFFFF] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /> <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg> </svg>
@@ -587,9 +589,9 @@ export default function AdminArticlesPage() {
<div className="flex items-center gap-3 mt-1"> <div className="flex items-center gap-3 mt-1">
<span className="text-[11px] text-[#555]">{article.author_name || 'Unknown author'}</span> <span className="text-[11px] text-[#555]">{article.author_name || 'Unknown author'}</span>
{article.category && ( {article.category && (
<span className="text-[10px] font-bold uppercase tracking-wider text-[#3b82f6] bg-[#0d1a2e] px-1.5 py-0.5 rounded">{article.category}</span> <span className="text-[10px] font-bold uppercase tracking-wider text-[#1D4ED8] bg-[#0d1a2e] px-1.5 py-0.5 rounded">{article.category}</span>
)} )}
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#3b82f6] bg-[#3b82f6]/10'}`}> <span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#1D4ED8] bg-[#1D4ED8]/10'}`}>
{article.source === 'imported' ? 'WP' : 'Native'} {article.source === 'imported' ? 'WP' : 'Native'}
</span> </span>
</div> </div>
@@ -606,13 +608,13 @@ export default function AdminArticlesPage() {
</button> </button>
<button <button
onClick={() => handleStatusChange(article, 'draft')} onClick={() => handleStatusChange(article, 'draft')}
className="flex items-center gap-1.5 text-xs font-bold text-[#666] border border-[#333] hover:border-[#555] hover:bg-[#1a1a1a] px-3 py-1.5 rounded transition-colors"> className="flex items-center gap-1.5 text-xs font-bold text-[#666] border border-[#333] hover:border-[#555] hover:bg-[#FFFFFF] px-3 py-1.5 rounded transition-colors">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg> <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
Reject Reject
</button> </button>
<button <button
onClick={() => openEdit(article)} onClick={() => openEdit(article)}
className="p-1.5 text-[#555] hover:text-[#3b82f6] transition-colors rounded hover:bg-[#1a1a1a]" className="p-1.5 text-[#555] hover:text-[#1D4ED8] transition-colors rounded hover:bg-[#FFFFFF]"
title="Edit"> title="Edit">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /> <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
@@ -627,14 +629,14 @@ export default function AdminArticlesPage() {
)} )}
{/* Articles Table */} {/* Articles Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
{/* Table Header */} {/* Table Header */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-[#252525] bg-[#0d0d0d]"> <div className="flex items-center gap-3 px-4 py-3 border-b border-[#DCE6F2] bg-[#F8FAFC]">
<input <input
type="checkbox" type="checkbox"
checked={articles.length > 0 && selectedIds.size === articles.length} checked={articles.length > 0 && selectedIds.size === articles.length}
onChange={toggleSelectAll} onChange={toggleSelectAll}
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#3b82f6] cursor-pointer" className="w-4 h-4 rounded border-[#333] bg-[#FFFFFF] accent-[#1D4ED8] cursor-pointer"
/> />
<span className="text-[10px] text-[#555] uppercase tracking-wider font-bold flex-1">Article</span> <span className="text-[10px] text-[#555] uppercase tracking-wider font-bold flex-1">Article</span>
<span className="text-[10px] text-[#555] uppercase tracking-wider font-bold w-24 hidden md:block">Category</span> <span className="text-[10px] text-[#555] uppercase tracking-wider font-bold w-24 hidden md:block">Category</span>
@@ -646,7 +648,7 @@ export default function AdminArticlesPage() {
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : articles.length === 0 ? ( ) : articles.length === 0 ? (
<div className="text-center py-16"> <div className="text-center py-16">
@@ -656,7 +658,7 @@ export default function AdminArticlesPage() {
<p className="text-[#555] text-sm">No articles found</p> <p className="text-[#555] text-sm">No articles found</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#FFFFFF]">
{articles.map((article) => ( {articles.map((article) => (
<div <div
key={article.id} key={article.id}
@@ -668,13 +670,13 @@ export default function AdminArticlesPage() {
type="checkbox" type="checkbox"
checked={selectedIds.has(article.id)} checked={selectedIds.has(article.id)}
onChange={() => toggleSelect(article.id)} onChange={() => toggleSelect(article.id)}
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#3b82f6] cursor-pointer flex-shrink-0" className="w-4 h-4 rounded border-[#333] bg-[#FFFFFF] accent-[#1D4ED8] cursor-pointer flex-shrink-0"
/> />
{/* Thumbnail + Title */} {/* Thumbnail + Title */}
<div className="flex items-center gap-3 flex-1 min-w-0"> <div className="flex items-center gap-3 flex-1 min-w-0">
{article.featured_image ? ( {article.featured_image ? (
<div className="flex-shrink-0 w-12 h-9 rounded overflow-hidden bg-[#1a1a1a]"> <div className="flex-shrink-0 w-12 h-9 rounded overflow-hidden bg-[#FFFFFF]">
<AppImage <AppImage
src={article.featured_image} src={article.featured_image}
alt={article.featured_image_alt || article.title} alt={article.featured_image_alt || article.title}
@@ -684,7 +686,7 @@ export default function AdminArticlesPage() {
/> />
</div> </div>
) : ( ) : (
<div className="flex-shrink-0 w-12 h-9 rounded bg-[#1a1a1a] flex items-center justify-center"> <div className="flex-shrink-0 w-12 h-9 rounded bg-[#FFFFFF] flex items-center justify-center">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /> <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg> </svg>
@@ -699,7 +701,7 @@ export default function AdminArticlesPage() {
{/* Category */} {/* Category */}
<div className="w-24 hidden md:block"> <div className="w-24 hidden md:block">
{article.category ? ( {article.category ? (
<span className="text-[10px] font-bold uppercase tracking-wider text-[#3b82f6] bg-[#0d1a2e] px-1.5 py-0.5 rounded"> <span className="text-[10px] font-bold uppercase tracking-wider text-[#1D4ED8] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
{article.category} {article.category}
</span> </span>
) : ( ) : (
@@ -710,7 +712,7 @@ export default function AdminArticlesPage() {
{/* Source */} {/* Source */}
<div className="w-20 hidden lg:block"> <div className="w-20 hidden lg:block">
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${ <span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${
article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#3b82f6] bg-[#3b82f6]/10' article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#1D4ED8] bg-[#1D4ED8]/10'
}`}> }`}>
{article.source === 'imported' ? 'WP' : 'Native'} {article.source === 'imported' ? 'WP' : 'Native'}
</span> </span>
@@ -730,7 +732,7 @@ export default function AdminArticlesPage() {
{/* Status */} {/* Status */}
<div className="w-28"> <div className="w-28">
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${STATUS_COLORS[article.status] || 'text-[#555] bg-[#1a1a1a]'}`}> <span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${STATUS_COLORS[article.status] || 'text-[#555] bg-[#FFFFFF]'}`}>
{article.status === 'pending' ? 'Pending' : article.status} {article.status === 'pending' ? 'Pending' : article.status}
</span> </span>
</div> </div>
@@ -741,19 +743,32 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => openEdit(article)} onClick={() => openEdit(article)}
title="Edit" title="Edit"
className="p-1.5 text-[#555] hover:text-[#3b82f6] transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-[#1D4ED8] transition-colors rounded hover:bg-[#FFFFFF]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /> <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" /> <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg> </svg>
</button> </button>
{/* Promote — opens /admin/promote/<slug> share sheet */}
{article.status === 'published' && (article.slug || article.wp_slug) && (
<Link
href={`/admin/promote/${article.slug || article.wp_slug}`}
title="Promote on social"
className="p-1.5 text-[#555] hover:text-[#1D4ED8] transition-colors rounded hover:bg-[#FFFFFF]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
</Link>
)}
{/* Submit for Review (draft → pending) */} {/* Submit for Review (draft → pending) */}
{article.status === 'draft' && ( {article.status === 'draft' && (
<button <button
onClick={() => handleStatusChange(article, 'pending')} onClick={() => handleStatusChange(article, 'pending')}
title="Submit for Review" title="Submit for Review"
className="p-1.5 text-[#555] hover:text-orange-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-orange-400 transition-colors rounded hover:bg-[#FFFFFF]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" /> <circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
</svg> </svg>
@@ -765,7 +780,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleStatusChange(article, 'published')} onClick={() => handleStatusChange(article, 'published')}
title="Approve & Publish" title="Approve & Publish"
className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#FFFFFF]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" /> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
</svg> </svg>
@@ -777,7 +792,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleStatusChange(article, 'draft')} onClick={() => handleStatusChange(article, 'draft')}
title="Reject (back to draft)" title="Reject (back to draft)"
className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#FFFFFF]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /> <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
@@ -789,7 +804,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleStatusChange(article, 'published')} onClick={() => handleStatusChange(article, 'published')}
title="Publish" title="Publish"
className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#FFFFFF]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" /> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
</svg> </svg>
@@ -801,7 +816,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleStatusChange(article, 'archived')} onClick={() => handleStatusChange(article, 'archived')}
title="Archive" title="Archive"
className="p-1.5 text-[#555] hover:text-yellow-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-yellow-400 transition-colors rounded hover:bg-[#FFFFFF]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="21 8 21 21 3 21 3 8" /><rect x="1" y="3" width="22" height="5" /><line x1="10" y1="12" x2="14" y2="12" /> <polyline points="21 8 21 21 3 21 3 8" /><rect x="1" y="3" width="22" height="5" /><line x1="10" y1="12" x2="14" y2="12" />
</svg> </svg>
@@ -812,7 +827,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleDelete(article)} onClick={() => handleDelete(article)}
title="Delete" title="Delete"
className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#FFFFFF]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="3 6 5 6 21 6" /> <polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" /> <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
@@ -834,12 +849,12 @@ export default function AdminArticlesPage() {
{/* New Article Modal */} {/* New Article Modal */}
{showNewArticle && ( {showNewArticle && (
<div className="fixed inset-0 z-50 flex items-start justify-center p-4 bg-black/80 overflow-y-auto"> <div className="fixed inset-0 z-50 flex items-start justify-center p-4 bg-black/80 overflow-y-auto">
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-4xl shadow-2xl my-8"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl w-full max-w-4xl shadow-2xl my-8">
{/* Modal Header */} {/* Modal Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525] sticky top-0 bg-[#111] z-10 rounded-t-xl"> <div className="flex items-center justify-between px-6 py-4 border-b border-[#DCE6F2] sticky top-0 bg-[#111] z-10 rounded-t-xl">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h2 className="text-base font-bold text-white font-heading">New Article</h2> <h2 className="text-base font-bold text-white font-heading">New Article</h2>
<span className="text-[10px] font-bold uppercase tracking-wider text-[#3b82f6] bg-[#3b82f6]/10 px-1.5 py-0.5 rounded">Native</span> <span className="text-[10px] font-bold uppercase tracking-wider text-[#1D4ED8] bg-[#1D4ED8]/10 px-1.5 py-0.5 rounded">Native</span>
</div> </div>
<button <button
onClick={() => setShowNewArticle(false)} onClick={() => setShowNewArticle(false)}
@@ -859,7 +874,7 @@ export default function AdminArticlesPage() {
value={newTitle} value={newTitle}
onChange={(e) => setNewTitle(e.target.value)} onChange={(e) => setNewTitle(e.target.value)}
placeholder="Enter article title..." placeholder="Enter article title..."
className="w-full bg-[#1a1a1a] border border-[#333] text-white text-lg font-bold rounded px-4 py-3 focus:outline-none focus:border-[#3b82f6] placeholder-[#444]" className="w-full bg-[#FFFFFF] border border-[#333] text-white text-lg font-bold rounded px-4 py-3 focus:outline-none focus:border-[#1D4ED8] placeholder-[#444]"
/> />
</div> </div>
@@ -871,7 +886,7 @@ export default function AdminArticlesPage() {
onChange={(e) => setNewExcerpt(e.target.value)} onChange={(e) => setNewExcerpt(e.target.value)}
placeholder="Brief summary shown in article listings..." placeholder="Brief summary shown in article listings..."
rows={2} rows={2}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2.5 focus:outline-none focus:border-[#3b82f6] placeholder-[#444] resize-none" className="w-full bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8] placeholder-[#444] resize-none"
/> />
</div> </div>
@@ -894,7 +909,7 @@ export default function AdminArticlesPage() {
value={newAuthor} value={newAuthor}
onChange={(e) => setNewAuthor(e.target.value)} onChange={(e) => setNewAuthor(e.target.value)}
placeholder="Author name" placeholder="Author name"
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6] placeholder-[#444]" className="w-full bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8] placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -904,7 +919,7 @@ export default function AdminArticlesPage() {
value={newCategory} value={newCategory}
onChange={(e) => setNewCategory(e.target.value)} onChange={(e) => setNewCategory(e.target.value)}
placeholder="e.g. Technology" placeholder="e.g. Technology"
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6] placeholder-[#444]" className="w-full bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8] placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -912,7 +927,7 @@ export default function AdminArticlesPage() {
<select <select
value={newStatus} value={newStatus}
onChange={(e) => setNewStatus(e.target.value as 'draft' | 'pending' | 'published')} onChange={(e) => setNewStatus(e.target.value as 'draft' | 'pending' | 'published')}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6]"> className="w-full bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8]">
<option value="draft">Draft</option> <option value="draft">Draft</option>
<option value="pending">Submit for Review</option> <option value="pending">Submit for Review</option>
<option value="published">Publish Now</option> <option value="published">Publish Now</option>
@@ -929,7 +944,7 @@ export default function AdminArticlesPage() {
value={newFeaturedImage} value={newFeaturedImage}
onChange={(e) => setNewFeaturedImage(e.target.value)} onChange={(e) => setNewFeaturedImage(e.target.value)}
placeholder="https://..." placeholder="https://..."
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6] placeholder-[#444]" className="w-full bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8] placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -939,21 +954,21 @@ export default function AdminArticlesPage() {
value={newFeaturedImageAlt} value={newFeaturedImageAlt}
onChange={(e) => setNewFeaturedImageAlt(e.target.value)} onChange={(e) => setNewFeaturedImageAlt(e.target.value)}
placeholder="Describe the image..." placeholder="Describe the image..."
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6] placeholder-[#444]" className="w-full bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8] placeholder-[#444]"
/> />
</div> </div>
</div> </div>
{/* Featured image preview */} {/* Featured image preview */}
{newFeaturedImage && ( {newFeaturedImage && (
<div className="rounded-lg overflow-hidden border border-[#252525] max-h-48"> <div className="rounded-lg overflow-hidden border border-[#DCE6F2] max-h-48">
<img src={newFeaturedImage} alt={newFeaturedImageAlt || 'Featured image preview'} className="w-full h-48 object-cover" /> <img src={newFeaturedImage} alt={newFeaturedImageAlt || 'Featured image preview'} className="w-full h-48 object-cover" />
</div> </div>
)} )}
</div> </div>
{/* Modal Footer */} {/* Modal Footer */}
<div className="flex items-center justify-between px-6 py-4 border-t border-[#252525] bg-[#0d0d0d] rounded-b-xl"> <div className="flex items-center justify-between px-6 py-4 border-t border-[#DCE6F2] bg-[#F8FAFC] rounded-b-xl">
<p className="text-[11px] text-[#444]"> <p className="text-[11px] text-[#444]">
{newStatus === 'draft' ? 'Saved as draft — not publicly visible' : newStatus === 'pending' ? 'Submitted for editorial review' : 'Will be published immediately'} {newStatus === 'draft' ? 'Saved as draft — not publicly visible' : newStatus === 'pending' ? 'Submitted for editorial review' : 'Will be published immediately'}
</p> </p>
@@ -966,7 +981,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={handleNewArticleSave} onClick={handleNewArticleSave}
disabled={newSaving || !newTitle.trim()} disabled={newSaving || !newTitle.trim()}
className="flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors"> className="flex items-center gap-2 bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:bg-[#DCE6F2] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
{newSaving ? ( {newSaving ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : ( ) : (
@@ -983,8 +998,8 @@ export default function AdminArticlesPage() {
{/* Edit Modal */} {/* Edit Modal */}
{editingArticle && ( {editingArticle && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70">
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-lg shadow-2xl"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl w-full max-w-lg shadow-2xl">
<div className="flex items-center justify-between px-5 py-4 border-b border-[#252525]"> <div className="flex items-center justify-between px-5 py-4 border-b border-[#DCE6F2]">
<h2 className="text-base font-bold text-white font-heading">Edit Article</h2> <h2 className="text-base font-bold text-white font-heading">Edit Article</h2>
<button <button
onClick={() => setEditingArticle(null)} onClick={() => setEditingArticle(null)}
@@ -1002,7 +1017,7 @@ export default function AdminArticlesPage() {
type="text" type="text"
value={editTitle} value={editTitle}
onChange={(e) => setEditTitle(e.target.value)} onChange={(e) => setEditTitle(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -1012,7 +1027,7 @@ export default function AdminArticlesPage() {
type="text" type="text"
value={editAuthor} value={editAuthor}
onChange={(e) => setEditAuthor(e.target.value)} onChange={(e) => setEditAuthor(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<div> <div>
@@ -1021,7 +1036,7 @@ export default function AdminArticlesPage() {
type="text" type="text"
value={editCategory} value={editCategory}
onChange={(e) => setEditCategory(e.target.value)} onChange={(e) => setEditCategory(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
</div> </div>
@@ -1030,7 +1045,7 @@ export default function AdminArticlesPage() {
<select <select
value={editStatus} value={editStatus}
onChange={(e) => setEditStatus(e.target.value)} onChange={(e) => setEditStatus(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6]"> className="w-full bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8]">
<option value="draft">Draft</option> <option value="draft">Draft</option>
<option value="pending">Pending Review</option> <option value="pending">Pending Review</option>
<option value="published">Published</option> <option value="published">Published</option>
@@ -1040,19 +1055,19 @@ export default function AdminArticlesPage() {
{/* Scheduling — only shown for draft status */} {/* Scheduling — only shown for draft status */}
{editStatus === 'draft' && ( {editStatus === 'draft' && (
<div className="bg-[#0d1a2e] border border-[#1e3a5f] rounded-lg p-4"> <div className="bg-[#0d1a2e] border border-[#DCE6F2] rounded-lg p-4">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#3b82f6]"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#1D4ED8]">
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /> <circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
</svg> </svg>
<label className="text-xs text-[#3b82f6] font-bold uppercase tracking-wider">Schedule Publish</label> <label className="text-xs text-[#1D4ED8] font-bold uppercase tracking-wider">Schedule Publish</label>
</div> </div>
<input <input
type="datetime-local" type="datetime-local"
value={editScheduledAt} value={editScheduledAt}
onChange={(e) => setEditScheduledAt(e.target.value)} onChange={(e) => setEditScheduledAt(e.target.value)}
min={new Date().toISOString().slice(0, 16)} min={new Date().toISOString().slice(0, 16)}
className="w-full bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6] [color-scheme:dark]" className="w-full bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8] [color-scheme:dark]"
/> />
{editScheduledAt && ( {editScheduledAt && (
<div className="flex items-center justify-between mt-2"> <div className="flex items-center justify-between mt-2">
@@ -1074,14 +1089,14 @@ export default function AdminArticlesPage() {
<div className="flex items-center gap-2 pt-1"> <div className="flex items-center gap-2 pt-1">
<span className="text-xs text-[#555]">Source:</span> <span className="text-xs text-[#555]">Source:</span>
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${ <span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${
editingArticle.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#3b82f6] bg-[#3b82f6]/10' editingArticle.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#1D4ED8] bg-[#1D4ED8]/10'
}`}> }`}>
{editingArticle.source === 'imported' ? 'WordPress Import' : 'Native'} {editingArticle.source === 'imported' ? 'WordPress Import' : 'Native'}
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-3 px-5 py-4 border-t border-[#252525]"> <div className="flex items-center justify-end gap-3 px-5 py-4 border-t border-[#DCE6F2]">
<button <button
onClick={() => setEditingArticle(null)} onClick={() => setEditingArticle(null)}
className="text-[#666] hover:text-[#999] text-sm transition-colors px-4 py-2"> className="text-[#666] hover:text-[#999] text-sm transition-colors px-4 py-2">
@@ -1090,7 +1105,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={handleEditSave} onClick={handleEditSave}
disabled={editSaving || !editTitle.trim()} disabled={editSaving || !editTitle.trim()}
className="flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors"> className="flex items-center gap-2 bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:bg-[#DCE6F2] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
{editSaving ? ( {editSaving ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : null} ) : null}
@@ -1102,5 +1117,6 @@ export default function AdminArticlesPage() {
)} )}
</div> </div>
</div> </div>
</AdminShell>
); );
} }

View File

@@ -50,13 +50,13 @@ const ACTION_COLORS: Record<string, string> = {
article_published: 'text-green-400 bg-green-400/10', article_published: 'text-green-400 bg-green-400/10',
article_unpublished: 'text-yellow-400 bg-yellow-400/10', article_unpublished: 'text-yellow-400 bg-yellow-400/10',
article_deleted: 'text-red-400 bg-red-400/10', article_deleted: 'text-red-400 bg-red-400/10',
article_archived: 'text-[#888] bg-[#1a1a1a]', article_archived: 'text-[#888] bg-[#FFFFFF]',
article_status_changed: 'text-purple-400 bg-purple-400/10', article_status_changed: 'text-purple-400 bg-purple-400/10',
user_role_changed: 'text-orange-400 bg-orange-400/10', user_role_changed: 'text-orange-400 bg-orange-400/10',
user_activated: 'text-green-400 bg-green-400/10', user_activated: 'text-green-400 bg-green-400/10',
user_deactivated: 'text-red-400 bg-red-400/10', user_deactivated: 'text-red-400 bg-red-400/10',
user_invited: 'text-blue-400 bg-blue-400/10', user_invited: 'text-blue-400 bg-blue-400/10',
user_invitation_revoked: 'text-[#888] bg-[#1a1a1a]', user_invitation_revoked: 'text-[#888] bg-[#FFFFFF]',
import_completed: 'text-teal-400 bg-teal-400/10', import_completed: 'text-teal-400 bg-teal-400/10',
import_failed: 'text-red-400 bg-red-400/10', import_failed: 'text-red-400 bg-red-400/10',
}; };
@@ -150,7 +150,7 @@ interface StatCardProps {
function StatCard({ label, value, accent, icon }: StatCardProps) { function StatCard({ label, value, accent, icon }: StatCardProps) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-4 flex items-center gap-4"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 flex items-center gap-4">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${accent}`}> <div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${accent}`}>
{icon} {icon}
</div> </div>
@@ -170,16 +170,16 @@ interface DetailModalProps {
} }
function DetailModal({ log, onClose }: DetailModalProps) { function DetailModal({ log, onClose }: DetailModalProps) {
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#1a1a1a]'; const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#FFFFFF]';
const actionLabel = ACTION_LABELS[log.action] || log.action; const actionLabel = ACTION_LABELS[log.action] || log.action;
const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System'; const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System';
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/70" onClick={onClose} /> <div className="absolute inset-0 bg-black/70" onClick={onClose} />
<div className="relative bg-[#111] border border-[#252525] rounded-xl w-full max-w-lg shadow-2xl"> <div className="relative bg-[#111] border border-[#DCE6F2] rounded-xl w-full max-w-lg shadow-2xl">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-5 border-b border-[#252525]"> <div className="flex items-center justify-between p-5 border-b border-[#DCE6F2]">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${actionColor}`}> <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${actionColor}`}>
{ACTION_ICONS[log.action] || getDefaultIcon(log.action)} {ACTION_ICONS[log.action] || getDefaultIcon(log.action)}
@@ -247,7 +247,7 @@ function DetailModal({ log, onClose }: DetailModalProps) {
{log.metadata && Object.keys(log.metadata).length > 0 && ( {log.metadata && Object.keys(log.metadata).length > 0 && (
<div> <div>
<p className="text-[#555] text-xs font-body uppercase tracking-wider mb-1">Metadata</p> <p className="text-[#555] text-xs font-body uppercase tracking-wider mb-1">Metadata</p>
<div className="bg-[#0d0d0d] border border-[#1e1e1e] rounded-lg p-3"> <div className="bg-[#F8FAFC] border border-[#1e1e1e] rounded-lg p-3">
<pre className="text-[#aaa] text-xs font-mono whitespace-pre-wrap break-all"> <pre className="text-[#aaa] text-xs font-mono whitespace-pre-wrap break-all">
{JSON.stringify(log.metadata, null, 2)} {JSON.stringify(log.metadata, null, 2)}
</pre> </pre>
@@ -340,7 +340,7 @@ export default function AuditLogPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -368,7 +368,7 @@ export default function AuditLogPage() {
</div> </div>
<button <button
onClick={fetchLogs} onClick={fetchLogs}
className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#252525] rounded-lg text-[#888] hover:text-white hover:border-[#333] transition-all text-sm font-body" className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#DCE6F2] rounded-lg text-[#888] hover:text-white hover:border-[#333] transition-all text-sm font-body"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
@@ -382,7 +382,7 @@ export default function AuditLogPage() {
<StatCard <StatCard
label="Total Events" label="Total Events"
value={logs.length} value={logs.length}
accent="bg-[#3b82f6]/10 text-[#3b82f6]" accent="bg-[#1D4ED8]/10 text-[#1D4ED8]"
icon={ icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
@@ -433,19 +433,19 @@ export default function AuditLogPage() {
placeholder="Search by user, item, or action…" placeholder="Search by user, item, or action…"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#252525] rounded-lg pl-9 pr-4 py-2.5 text-sm text-white placeholder-[#444] focus:outline-none focus:border-[#3b82f6] font-body" className="w-full bg-[#111] border border-[#DCE6F2] rounded-lg pl-9 pr-4 py-2.5 text-sm text-white placeholder-[#444] focus:outline-none focus:border-[#1D4ED8] font-body"
/> />
</div> </div>
{/* Action Type Filter */} {/* Action Type Filter */}
<div className="flex items-center gap-1 bg-[#111] border border-[#252525] rounded-lg p-1"> <div className="flex items-center gap-1 bg-[#111] border border-[#DCE6F2] rounded-lg p-1">
{(['all', 'article', 'user', 'import'] as ActionFilter[]).map((f) => ( {(['all', 'article', 'user', 'import'] as ActionFilter[]).map((f) => (
<button <button
key={f} key={f}
onClick={() => setActionFilter(f)} onClick={() => setActionFilter(f)}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-all ${ className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-all ${
actionFilter === f actionFilter === f
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'text-[#555] hover:text-[#888]' : 'text-[#555] hover:text-[#888]'
}`} }`}
> >
@@ -466,9 +466,9 @@ export default function AuditLogPage() {
)} )}
{/* Table */} {/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-xl overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl overflow-hidden">
{/* Table Header */} {/* Table Header */}
<div className="hidden md:grid grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-4 px-5 py-3 border-b border-[#1e1e1e] bg-[#0d0d0d]"> <div className="hidden md:grid grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-4 px-5 py-3 border-b border-[#1e1e1e] bg-[#F8FAFC]">
<p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Action</p> <p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Action</p>
<p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Performed By</p> <p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Performed By</p>
<p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Affected Item</p> <p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Affected Item</p>
@@ -480,7 +480,7 @@ export default function AuditLogPage() {
{loadingData && ( {loadingData && (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading audit logs</p> <p className="text-[#555] text-sm font-body">Loading audit logs</p>
</div> </div>
</div> </div>
@@ -489,14 +489,14 @@ export default function AuditLogPage() {
{/* Empty */} {/* Empty */}
{!loadingData && filteredLogs.length === 0 && ( {!loadingData && filteredLogs.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 gap-3"> <div className="flex flex-col items-center justify-center py-16 gap-3">
<div className="w-12 h-12 rounded-full bg-[#1a1a1a] flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-[#FFFFFF] flex items-center justify-center">
<svg className="w-6 h-6 text-[#333]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-6 h-6 text-[#333]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg> </svg>
</div> </div>
<p className="text-[#555] text-sm font-body">No audit log entries found</p> <p className="text-[#555] text-sm font-body">No audit log entries found</p>
{searchQuery && ( {searchQuery && (
<button onClick={() => setSearchQuery('')} className="text-[#3b82f6] text-xs font-body hover:underline"> <button onClick={() => setSearchQuery('')} className="text-[#1D4ED8] text-xs font-body hover:underline">
Clear search Clear search
</button> </button>
)} )}
@@ -505,7 +505,7 @@ export default function AuditLogPage() {
{/* Rows */} {/* Rows */}
{!loadingData && filteredLogs.map((log, idx) => { {!loadingData && filteredLogs.map((log, idx) => {
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#1a1a1a]'; const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#FFFFFF]';
const actionLabel = ACTION_LABELS[log.action] || log.action; const actionLabel = ACTION_LABELS[log.action] || log.action;
const icon = ACTION_ICONS[log.action] || getDefaultIcon(log.action); const icon = ACTION_ICONS[log.action] || getDefaultIcon(log.action);
const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System'; const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System';
@@ -513,8 +513,8 @@ export default function AuditLogPage() {
return ( return (
<div <div
key={log.id} key={log.id}
className={`grid grid-cols-1 md:grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-3 md:gap-4 px-5 py-4 hover:bg-[#0d0d0d] transition-colors cursor-pointer ${ className={`grid grid-cols-1 md:grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-3 md:gap-4 px-5 py-4 hover:bg-[#F8FAFC] transition-colors cursor-pointer ${
idx !== filteredLogs.length - 1 ? 'border-b border-[#1a1a1a]' : '' idx !== filteredLogs.length - 1 ? 'border-b border-[#FFFFFF]' : ''
}`} }`}
onClick={() => setSelectedLog(log)} onClick={() => setSelectedLog(log)}
> >
@@ -530,7 +530,7 @@ export default function AuditLogPage() {
{/* Performed By */} {/* Performed By */}
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] border border-[#252525] flex items-center justify-center flex-shrink-0"> <div className="w-6 h-6 rounded-full bg-[#FFFFFF] border border-[#DCE6F2] flex items-center justify-center flex-shrink-0">
<span className="text-[#888] text-xs font-body font-bold"> <span className="text-[#888] text-xs font-body font-bold">
{performerName.charAt(0).toUpperCase()} {performerName.charAt(0).toUpperCase()}
</span> </span>
@@ -569,7 +569,7 @@ export default function AuditLogPage() {
<div className="flex items-center"> <div className="flex items-center">
<button <button
onClick={(e) => { e.stopPropagation(); setSelectedLog(log); }} onClick={(e) => { e.stopPropagation(); setSelectedLog(log); }}
className="text-[#444] hover:text-[#3b82f6] transition-colors p-1" className="text-[#444] hover:text-[#1D4ED8] transition-colors p-1"
aria-label="View details" aria-label="View details"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -593,7 +593,7 @@ export default function AuditLogPage() {
<button <button
onClick={() => setPage((p) => Math.max(0, p - 1))} onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0} disabled={page === 0}
className="px-3 py-1.5 bg-[#111] border border-[#252525] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all" className="px-3 py-1.5 bg-[#111] border border-[#DCE6F2] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
> >
Previous Previous
</button> </button>
@@ -601,7 +601,7 @@ export default function AuditLogPage() {
<button <button
onClick={() => setPage((p) => p + 1)} onClick={() => setPage((p) => p + 1)}
disabled={filteredLogs.length < PAGE_SIZE} disabled={filteredLogs.length < PAGE_SIZE}
className="px-3 py-1.5 bg-[#111] border border-[#252525] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all" className="px-3 py-1.5 bg-[#111] border border-[#DCE6F2] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
> >
Next Next
</button> </button>

View File

@@ -162,7 +162,7 @@ export default function BannedTermsPage() {
if (loading || loadingData) { if (loading || loadingData) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -190,17 +190,17 @@ export default function BannedTermsPage() {
<p className="text-[#555] text-sm font-body mt-1">Manage restricted company names and keywords visible to administrators only</p> <p className="text-[#555] text-sm font-body mt-1">Manage restricted company names and keywords visible to administrators only</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={exportCSV} className="flex items-center gap-2 px-3 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors"> <button onClick={exportCSV} className="flex items-center gap-2 px-3 py-2 bg-[#FFFFFF] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
Export CSV Export CSV
</button> </button>
<button onClick={handleRetroactiveScan} disabled={scanning} <button onClick={handleRetroactiveScan} disabled={scanning}
className="flex items-center gap-2 px-3 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors disabled:opacity-50"> className="flex items-center gap-2 px-3 py-2 bg-[#FFFFFF] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors disabled:opacity-50">
{scanning ? <div className="w-4 h-4 border-2 border-[#888]/30 border-t-[#888] rounded-full animate-spin" /> : <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>} {scanning ? <div className="w-4 h-4 border-2 border-[#888]/30 border-t-[#888] rounded-full animate-spin" /> : <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>}
Scan Published Posts Scan Published Posts
</button> </button>
<button onClick={() => { setForm(EMPTY_FORM); setEditingId(null); setShowAddForm(true); }} <button onClick={() => { setForm(EMPTY_FORM); setEditingId(null); setShowAddForm(true); }}
className="flex items-center gap-2 px-4 py-2 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors"> className="flex items-center gap-2 px-4 py-2 bg-[#1D4ED8] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg>
Add Term Add Term
</button> </button>
@@ -209,7 +209,7 @@ export default function BannedTermsPage() {
{/* Retroactive scan results */} {/* Retroactive scan results */}
{scanResults !== null && ( {scanResults !== null && (
<div className="mb-6 bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="mb-6 bg-[#111] border border-[#DCE6F2] rounded-lg p-5">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-display font-bold text-white">Retroactive Scan Results {scanResults.length} match{scanResults.length !== 1 ? 'es' : ''} found</h3> <h3 className="text-sm font-display font-bold text-white">Retroactive Scan Results {scanResults.length} match{scanResults.length !== 1 ? 'es' : ''} found</h3>
<button onClick={() => setScanResults(null)} className="text-[#555] hover:text-white text-xs font-body">Dismiss</button> <button onClick={() => setScanResults(null)} className="text-[#555] hover:text-white text-xs font-body">Dismiss</button>
@@ -234,18 +234,18 @@ export default function BannedTermsPage() {
{/* Add/Edit Form */} {/* Add/Edit Form */}
{showAddForm && ( {showAddForm && (
<div className="mb-6 bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="mb-6 bg-[#111] border border-[#DCE6F2] rounded-lg p-6">
<h3 className="text-sm font-display font-bold text-white mb-4">{editingId ? 'Edit Term' : 'Add Banned Term'}</h3> <h3 className="text-sm font-display font-bold text-white mb-4">{editingId ? 'Edit Term' : 'Add Banned Term'}</h3>
<form onSubmit={handleSave} className="grid grid-cols-1 md:grid-cols-2 gap-4"> <form onSubmit={handleSave} className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-body text-[#888] mb-1.5">Term / Company Name *</label> <label className="block text-xs font-body text-[#888] mb-1.5">Term / Company Name *</label>
<input type="text" value={form.term} onChange={e => setForm(f => ({ ...f, term: e.target.value }))} placeholder="e.g. Sony, Acme Corp" <input type="text" value={form.term} onChange={e => setForm(f => ({ ...f, term: e.target.value }))} placeholder="e.g. Sony, Acme Corp"
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]" required /> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]" required />
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Ban Level</label> <label className="block text-xs font-body text-[#888] mb-1.5">Ban Level</label>
<select value={form.ban_level} onChange={e => setForm(f => ({ ...f, ban_level: e.target.value as 'soft' | 'hard' }))} <select value={form.ban_level} onChange={e => setForm(f => ({ ...f, ban_level: e.target.value as 'soft' | 'hard' }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]"> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]">
<option value="soft">Soft Ban Flag for review (contributor doesn't know)</option> <option value="soft">Soft Ban Flag for review (contributor doesn't know)</option>
<option value="hard">Hard Ban — Block submission entirely</option> <option value="hard">Hard Ban — Block submission entirely</option>
</select> </select>
@@ -253,7 +253,7 @@ export default function BannedTermsPage() {
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Site Scope</label> <label className="block text-xs font-body text-[#888] mb-1.5">Site Scope</label>
<select value={form.site_scope} onChange={e => setForm(f => ({ ...f, site_scope: e.target.value as any }))} <select value={form.site_scope} onChange={e => setForm(f => ({ ...f, site_scope: e.target.value as any }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]"> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]">
<option value="both">Both Sites</option> <option value="both">Both Sites</option>
<option value="avbeat">AV Beat only</option> <option value="avbeat">AV Beat only</option>
<option value="avbeat">AV Beat only</option> <option value="avbeat">AV Beat only</option>
@@ -262,15 +262,15 @@ export default function BannedTermsPage() {
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-body text-[#888] mb-1.5">Internal Notes (not shown to contributors)</label> <label className="block text-xs font-body text-[#888] mb-1.5">Internal Notes (not shown to contributors)</label>
<textarea value={form.notes} onChange={e => setForm(f => ({ ...f, notes: e.target.value }))} rows={2} placeholder="Reason for ban, context..." <textarea value={form.notes} onChange={e => setForm(f => ({ ...f, notes: e.target.value }))} rows={2} placeholder="Reason for ban, context..."
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6] resize-none" /> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8] resize-none" />
</div> </div>
<div className="md:col-span-2 flex gap-3"> <div className="md:col-span-2 flex gap-3">
<button type="submit" disabled={saving} <button type="submit" disabled={saving}
className="px-5 py-2 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50"> className="px-5 py-2 bg-[#1D4ED8] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50">
{saving ? 'Saving...' : editingId ? 'Update Term' : 'Add Term'} {saving ? 'Saving...' : editingId ? 'Update Term' : 'Add Term'}
</button> </button>
<button type="button" onClick={() => { setShowAddForm(false); setEditingId(null); setForm(EMPTY_FORM); }} <button type="button" onClick={() => { setShowAddForm(false); setEditingId(null); setForm(EMPTY_FORM); }}
className="px-4 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors"> className="px-4 py-2 bg-[#FFFFFF] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">
Cancel Cancel
</button> </button>
</div> </div>
@@ -281,22 +281,22 @@ export default function BannedTermsPage() {
{/* Filters */} {/* Filters */}
<div className="mb-4 flex flex-wrap gap-3"> <div className="mb-4 flex flex-wrap gap-3">
<input type="text" value={searchTerm} onChange={e => setSearchTerm(e.target.value)} placeholder="Search terms..." <input type="text" value={searchTerm} onChange={e => setSearchTerm(e.target.value)} placeholder="Search terms..."
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6] w-48" /> className="bg-[#111] border border-[#DCE6F2] rounded-lg px-3 py-2 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8] w-48" />
<select value={filterBanLevel} onChange={e => setFilterBanLevel(e.target.value)} <select value={filterBanLevel} onChange={e => setFilterBanLevel(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#3b82f6]"> className="bg-[#111] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#1D4ED8]">
<option value="">All Ban Levels</option> <option value="">All Ban Levels</option>
<option value="soft">Soft Ban</option> <option value="soft">Soft Ban</option>
<option value="hard">Hard Ban</option> <option value="hard">Hard Ban</option>
</select> </select>
<select value={filterSiteScope} onChange={e => setFilterSiteScope(e.target.value)} <select value={filterSiteScope} onChange={e => setFilterSiteScope(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#3b82f6]"> className="bg-[#111] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#1D4ED8]">
<option value="">All Sites</option> <option value="">All Sites</option>
<option value="avbeat">AV Beat</option> <option value="avbeat">AV Beat</option>
<option value="avbeat">AV Beat</option> <option value="avbeat">AV Beat</option>
<option value="both">Both</option> <option value="both">Both</option>
</select> </select>
<select value={filterActive} onChange={e => setFilterActive(e.target.value)} <select value={filterActive} onChange={e => setFilterActive(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#3b82f6]"> className="bg-[#111] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#1D4ED8]">
<option value="">All Status</option> <option value="">All Status</option>
<option value="true">Active</option> <option value="true">Active</option>
<option value="false">Inactive</option> <option value="false">Inactive</option>
@@ -304,8 +304,8 @@ export default function BannedTermsPage() {
</div> </div>
{/* Terms Table */} {/* Terms Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-5 py-4 border-b border-[#FFFFFF] flex items-center justify-between">
<h2 className="text-sm font-display font-bold text-white">Banned Terms ({terms.length})</h2> <h2 className="text-sm font-display font-bold text-white">Banned Terms ({terms.length})</h2>
</div> </div>
{terms.length === 0 ? ( {terms.length === 0 ? (
@@ -316,15 +316,15 @@ export default function BannedTermsPage() {
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Term', 'Ban Level', 'Site Scope', 'Status', 'Date Added', 'Added By', 'Notes', 'Actions'].map(h => ( {['Term', 'Ban Level', 'Site Scope', 'Status', 'Date Added', 'Added By', 'Notes', 'Actions'].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th> <th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#1a1a1a]"> <tbody className="divide-y divide-[#FFFFFF]">
{terms.map(term => ( {terms.map(term => (
<tr key={term.id} className={`hover:bg-[#0d0d0d] transition-colors ${!term.is_active ? 'opacity-50' : ''}`}> <tr key={term.id} className={`hover:bg-[#F8FAFC] transition-colors ${!term.is_active ? 'opacity-50' : ''}`}>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className="text-sm font-body font-semibold text-white">{term.term}</span> <span className="text-sm font-body font-semibold text-white">{term.term}</span>
</td> </td>
@@ -339,7 +339,7 @@ export default function BannedTermsPage() {
<span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'avbeat' ? 'AV Beat' : 'AV Beat'}</span> <span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'avbeat' ? 'AV Beat' : 'AV Beat'}</span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}> <span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#FFFFFF] text-[#555]'}`}>
{term.is_active ? 'Active' : 'Inactive'} {term.is_active ? 'Active' : 'Inactive'}
</span> </span>
</td> </td>
@@ -348,7 +348,7 @@ export default function BannedTermsPage() {
<td className="px-4 py-3 text-xs text-[#555] font-body max-w-xs truncate">{term.notes || ''}</td> <td className="px-4 py-3 text-xs text-[#555] font-body max-w-xs truncate">{term.notes || ''}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={() => handleEdit(term)} className="text-xs text-[#3b82f6] hover:text-blue-300 font-body transition-colors">Edit</button> <button onClick={() => handleEdit(term)} className="text-xs text-[#1D4ED8] hover:text-blue-300 font-body transition-colors">Edit</button>
<button onClick={() => handleDeactivate(term.id, term.is_active)} disabled={actionLoading === term.id} <button onClick={() => handleDeactivate(term.id, term.is_active)} disabled={actionLoading === term.id}
className="text-xs text-[#888] hover:text-white font-body transition-colors disabled:opacity-50"> className="text-xs text-[#888] hover:text-white font-body transition-colors disabled:opacity-50">
{term.is_active ? 'Deactivate' : 'Reactivate'} {term.is_active ? 'Deactivate' : 'Reactivate'}
@@ -366,7 +366,7 @@ export default function BannedTermsPage() {
</div> </div>
{/* Legend */} {/* Legend */}
<div className="mt-6 bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="mt-6 bg-[#111] border border-[#DCE6F2] rounded-lg p-5">
<h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Ban Level Reference</h3> <h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Ban Level Reference</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>

View File

@@ -54,7 +54,7 @@ function StatBlock({ label, b }: { label: string; b: StatBucket }) {
<span className="font-semibold">{fmtNum(b.clicks)}</span> <span className="font-semibold">{fmtNum(b.clicks)}</span>
<span className="text-[#6b7280]"> clk</span> <span className="text-[#6b7280]"> clk</span>
<span className="mx-1 text-[#374151]">·</span> <span className="mx-1 text-[#374151]">·</span>
<span className="text-[#60a5fa]">{pct(b.clicks, b.impressions)}</span> <span className="text-[#1D4ED8]">{pct(b.clicks, b.impressions)}</span>
</div> </div>
</div> </div>
); );
@@ -140,7 +140,7 @@ export default function BannerRows({
<input <input
value={current.name} value={current.name}
onChange={(e) => field(b.id, "name", e.target.value)} onChange={(e) => field(b.id, "name", e.target.value)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#3b82f6] focus:outline-none" className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#1D4ED8] focus:outline-none"
/> />
</label> </label>
@@ -150,7 +150,7 @@ export default function BannerRows({
value={current.click_url || ""} value={current.click_url || ""}
onChange={(e) => field(b.id, "click_url", e.target.value)} onChange={(e) => field(b.id, "click_url", e.target.value)}
placeholder="https://…" placeholder="https://…"
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#3b82f6] focus:outline-none" className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#1D4ED8] focus:outline-none"
/> />
</label> </label>
@@ -160,7 +160,7 @@ export default function BannerRows({
type="date" type="date"
value={fmtDate(current.start_date)} value={fmtDate(current.start_date)}
onChange={(e) => field(b.id, "start_date", e.target.value ? new Date(e.target.value).toISOString() : null)} onChange={(e) => field(b.id, "start_date", e.target.value ? new Date(e.target.value).toISOString() : null)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#3b82f6] focus:outline-none" className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#1D4ED8] focus:outline-none"
/> />
</label> </label>
@@ -170,7 +170,7 @@ export default function BannerRows({
type="date" type="date"
value={fmtDate(current.end_date)} value={fmtDate(current.end_date)}
onChange={(e) => field(b.id, "end_date", e.target.value ? new Date(e.target.value).toISOString() : null)} onChange={(e) => field(b.id, "end_date", e.target.value ? new Date(e.target.value).toISOString() : null)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#3b82f6] focus:outline-none" className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#1D4ED8] focus:outline-none"
/> />
</label> </label>
@@ -179,7 +179,7 @@ export default function BannerRows({
<select <select
value={current.status} value={current.status}
onChange={(e) => field(b.id, "status", e.target.value)} onChange={(e) => field(b.id, "status", e.target.value)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#3b82f6] focus:outline-none" className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#1D4ED8] focus:outline-none"
> >
<option value="active">active</option> <option value="active">active</option>
<option value="scheduled">scheduled</option> <option value="scheduled">scheduled</option>
@@ -210,7 +210,7 @@ export default function BannerRows({
<button <button
onClick={() => save(b.id)} onClick={() => save(b.id)}
disabled={!dirty || pending} disabled={!dirty || pending}
className="rounded bg-[#1d4ed8] px-3 py-1.5 text-sm font-medium text-white hover:bg-[#2563eb] disabled:opacity-40 disabled:cursor-not-allowed" className="rounded bg-[#1d4ed8] px-3 py-1.5 text-sm font-medium text-white hover:bg-[#1E3A8A] disabled:opacity-40 disabled:cursor-not-allowed"
> >
Save changes Save changes
</button> </button>
@@ -219,7 +219,7 @@ export default function BannerRows({
</div> </div>
<Link <Link
href={`/admin/banners/${b.id}/analytics`} href={`/admin/banners/${b.id}/analytics`}
className="text-sm text-[#60a5fa] hover:underline" className="text-sm text-[#1D4ED8] hover:underline"
> >
View detailed analytics View detailed analytics
</Link> </Link>

View File

@@ -68,7 +68,7 @@ export default function AnalyticsCharts({
</div> </div>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> <section className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<ChartCard title="Impressions per day" data={imp} stroke="#60a5fa" /> <ChartCard title="Impressions per day" data={imp} stroke="#1D4ED8" />
<ChartCard title="Clicks per day" data={clk} stroke="#34d399" /> <ChartCard title="Clicks per day" data={clk} stroke="#34d399" />
</section> </section>
@@ -121,7 +121,7 @@ function TopListCard({ title, rows }: { title: string; rows: TopRow[] }) {
{rows.map((r) => ( {rows.map((r) => (
<tr key={r.label} className="border-b border-[#1f2937] last:border-0"> <tr key={r.label} className="border-b border-[#1f2937] last:border-0">
<td className="py-2 pr-3 text-[#e5e7eb] truncate max-w-[400px]" title={r.label}>{r.label}</td> <td className="py-2 pr-3 text-[#e5e7eb] truncate max-w-[400px]" title={r.label}>{r.label}</td>
<td className="py-2 text-right text-[#60a5fa]">{r.count.toLocaleString()}</td> <td className="py-2 text-right text-[#1D4ED8]">{r.count.toLocaleString()}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@@ -142,7 +142,7 @@ export default async function BannerAnalyticsPage({
return ( return (
<main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]"> <main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]">
<Link href="/admin/banners" className="text-sm text-[#60a5fa] hover:underline"> <Link href="/admin/banners" className="text-sm text-[#1D4ED8] hover:underline">
Back to banners Back to banners
</Link> </Link>
@@ -159,7 +159,7 @@ export default async function BannerAnalyticsPage({
<p className="text-sm text-[#9ca3af]"> <p className="text-sm text-[#9ca3af]">
{b.size} · {b.status} ·{" "} {b.size} · {b.status} ·{" "}
{b.click_url && ( {b.click_url && (
<a className="text-[#60a5fa] hover:underline" href={b.click_url} target="_blank" rel="noopener noreferrer"> <a className="text-[#1D4ED8] hover:underline" href={b.click_url} target="_blank" rel="noopener noreferrer">
{b.click_url.slice(0, 80)}{b.click_url.length > 80 ? "…" : ""} {b.click_url.slice(0, 80)}{b.click_url.length > 80 ? "…" : ""}
</a> </a>
)} )}

View File

@@ -113,7 +113,7 @@ export default async function AdminBannersPage() {
after the next refresh. after the next refresh.
</p> </p>
</div> </div>
<Link href="/admin" className="text-sm text-[#60a5fa] hover:underline"> <Link href="/admin" className="text-sm text-[#1D4ED8] hover:underline">
Admin home Admin home
</Link> </Link>
</header> </header>

View File

@@ -171,7 +171,7 @@ export default function CompanyCoveragePage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading company coverage</p> <p className="text-[#555] text-sm font-body">Loading company coverage</p>
</div> </div>
</div> </div>
@@ -218,7 +218,7 @@ export default function CompanyCoveragePage() {
<button <button
onClick={handleScan} onClick={handleScan}
disabled={scanning} disabled={scanning}
className="flex items-center gap-2 px-4 py-2 bg-[#3b82f6] hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-body font-semibold rounded-lg transition-colors" className="flex items-center gap-2 px-4 py-2 bg-[#1D4ED8] hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-body font-semibold rounded-lg transition-colors"
> >
{scanning ? ( {scanning ? (
<> <>
@@ -255,14 +255,14 @@ export default function CompanyCoveragePage() {
<div className="lg:col-span-2"> <div className="lg:col-span-2">
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 mb-4 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit"> <div className="flex gap-1 mb-4 bg-[#111] border border-[#DCE6F2] rounded-lg p-1 w-fit">
{(['queued', 'published', 'dismissed'] as const).map((tab) => ( {(['queued', 'published', 'dismissed'] as const).map((tab) => (
<button <button
key={tab} key={tab}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`px-4 py-1.5 rounded-md text-sm font-body font-semibold capitalize transition-colors ${ className={`px-4 py-1.5 rounded-md text-sm font-body font-semibold capitalize transition-colors ${
activeTab === tab activeTab === tab
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'text-[#888] hover:text-white' : 'text-[#888] hover:text-white'
}`} }`}
> >
@@ -272,7 +272,7 @@ export default function CompanyCoveragePage() {
</div> </div>
{/* Queue List */} {/* Queue List */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
{queue.length === 0 ? ( {queue.length === 0 ? (
<div className="px-5 py-16 text-center"> <div className="px-5 py-16 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"> <svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
@@ -282,13 +282,13 @@ export default function CompanyCoveragePage() {
{activeTab === 'queued' ? 'No stories queued — run a scan to detect companies' : `No ${activeTab} stories`} {activeTab === 'queued' ? 'No stories queued — run a scan to detect companies' : `No ${activeTab} stories`}
</p> </p>
{activeTab === 'queued' && ( {activeTab === 'queued' && (
<button onClick={handleScan} disabled={scanning} className="mt-3 text-xs text-[#3b82f6] hover:text-blue-300 font-body underline"> <button onClick={handleScan} disabled={scanning} className="mt-3 text-xs text-[#1D4ED8] hover:text-blue-300 font-body underline">
Run scan now Run scan now
</button> </button>
)} )}
</div> </div>
) : ( ) : (
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#FFFFFF]">
{queue.map((item) => ( {queue.map((item) => (
<div key={item.id} className="p-4"> <div key={item.id} className="p-4">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
@@ -332,7 +332,7 @@ export default function CompanyCoveragePage() {
</button> </button>
<button <button
onClick={() => setExpandedItem(expandedItem === item.id ? null : item.id)} onClick={() => setExpandedItem(expandedItem === item.id ? null : item.id)}
className="px-3 py-1.5 bg-[#1a1a1a] hover:bg-[#222] text-[#888] text-xs font-body rounded-md transition-colors" className="px-3 py-1.5 bg-[#FFFFFF] hover:bg-[#222] text-[#888] text-xs font-body rounded-md transition-colors"
> >
{expandedItem === item.id ? 'Hide' : 'Preview'} {expandedItem === item.id ? 'Hide' : 'Preview'}
</button> </button>
@@ -349,7 +349,7 @@ export default function CompanyCoveragePage() {
{/* Expanded Draft Preview */} {/* Expanded Draft Preview */}
{expandedItem === item.id && ( {expandedItem === item.id && (
<div className="mt-4 bg-[#0d0d0d] border border-[#1a1a1a] rounded-lg p-4"> <div className="mt-4 bg-[#F8FAFC] border border-[#FFFFFF] rounded-lg p-4">
<p className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">AI Draft Preview</p> <p className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">AI Draft Preview</p>
<h3 className="text-base font-display font-bold text-white mb-2">{item.ai_draft_title}</h3> <h3 className="text-base font-display font-bold text-white mb-2">{item.ai_draft_title}</h3>
<p className="text-sm text-[#888] font-body italic mb-3">{item.ai_draft_excerpt}</p> <p className="text-sm text-[#888] font-body italic mb-3">{item.ai_draft_excerpt}</p>
@@ -370,11 +370,11 @@ export default function CompanyCoveragePage() {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Auto-Pilot Toggle */} {/* Auto-Pilot Toggle */}
<div className={`bg-[#111] border rounded-lg p-5 transition-colors ${settings.autopilot_enabled ? 'border-green-500/40' : 'border-[#252525]'}`}> <div className={`bg-[#111] border rounded-lg p-5 transition-colors ${settings.autopilot_enabled ? 'border-green-500/40' : 'border-[#DCE6F2]'}`}>
<div className="flex items-start justify-between gap-3 mb-3"> <div className="flex items-start justify-between gap-3 mb-3">
<div> <div>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<svg className="w-4 h-4 text-[#3b82f6]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4 text-[#1D4ED8]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
<h3 className="text-sm font-display font-bold text-white">Auto-Pilot</h3> <h3 className="text-sm font-display font-bold text-white">Auto-Pilot</h3>
@@ -405,7 +405,7 @@ export default function CompanyCoveragePage() {
<p className="text-xs text-green-400 font-body font-semibold">Auto-Pilot is ON stories publish automatically</p> <p className="text-xs text-green-400 font-body font-semibold">Auto-Pilot is ON stories publish automatically</p>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-2 bg-[#1a1a1a] rounded-md px-3 py-2"> <div className="flex items-center gap-2 bg-[#FFFFFF] rounded-md px-3 py-2">
<div className="w-2 h-2 rounded-full bg-[#444] flex-shrink-0" /> <div className="w-2 h-2 rounded-full bg-[#444] flex-shrink-0" />
<p className="text-xs text-[#666] font-body">Manual review required before publishing</p> <p className="text-xs text-[#666] font-body">Manual review required before publishing</p>
</div> </div>
@@ -413,7 +413,7 @@ export default function CompanyCoveragePage() {
</div> </div>
{/* Reminder Settings */} {/* Reminder Settings */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<svg className="w-4 h-4 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
@@ -421,14 +421,14 @@ export default function CompanyCoveragePage() {
<h3 className="text-sm font-display font-bold text-white">Reminder Email</h3> <h3 className="text-sm font-display font-bold text-white">Reminder Email</h3>
</div> </div>
<p className="text-xs text-[#555] font-body mb-2">New story suggestions are emailed to:</p> <p className="text-xs text-[#555] font-body mb-2">New story suggestions are emailed to:</p>
<div className="bg-[#0d0d0d] border border-[#1a1a1a] rounded-md px-3 py-2"> <div className="bg-[#F8FAFC] border border-[#FFFFFF] rounded-md px-3 py-2">
<p className="text-xs text-[#3b82f6] font-body font-mono break-all">{settings.reminder_email}</p> <p className="text-xs text-[#1D4ED8] font-body font-mono break-all">{settings.reminder_email}</p>
</div> </div>
<p className="text-xs text-[#444] font-body mt-2">Reminders are sent when Auto-Pilot is off and new companies are detected.</p> <p className="text-xs text-[#444] font-body mt-2">Reminders are sent when Auto-Pilot is off and new companies are detected.</p>
</div> </div>
{/* Last Scan Info */} {/* Last Scan Info */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<svg className="w-4 h-4 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" /><path strokeLinecap="round" d="M12 6v6l4 2" /> <circle cx="12" cy="12" r="10" /><path strokeLinecap="round" d="M12 6v6l4 2" />
@@ -456,7 +456,7 @@ export default function CompanyCoveragePage() {
</div> </div>
{/* How It Works */} {/* How It Works */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5">
<h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">How It Works</h3> <h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">How It Works</h3>
<ol className="space-y-2"> <ol className="space-y-2">
{[ {[
@@ -467,7 +467,7 @@ export default function CompanyCoveragePage() {
'Email reminder sent to coverage contact', 'Email reminder sent to coverage contact',
].map((step, i) => ( ].map((step, i) => (
<li key={i} className="flex items-start gap-2"> <li key={i} className="flex items-start gap-2">
<span className="flex-shrink-0 w-4 h-4 rounded-full bg-[#1a1a1a] text-[#555] text-xs font-body font-bold flex items-center justify-center mt-0.5"> <span className="flex-shrink-0 w-4 h-4 rounded-full bg-[#FFFFFF] text-[#555] text-xs font-body font-bold flex items-center justify-center mt-0.5">
{i + 1} {i + 1}
</span> </span>
<span className="text-xs text-[#666] font-body leading-relaxed">{step}</span> <span className="text-xs text-[#666] font-body leading-relaxed">{step}</span>

View File

@@ -245,7 +245,7 @@ export default function CompanyTrackerPage() {
if (loading || loadingData) { if (loading || loadingData) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -279,7 +279,7 @@ export default function CompanyTrackerPage() {
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* Auto-Pilot toggle */} {/* Auto-Pilot toggle */}
<div className="flex items-center gap-3 bg-[#111] border border-[#252525] rounded-lg px-4 py-2.5"> <div className="flex items-center gap-3 bg-[#111] border border-[#DCE6F2] rounded-lg px-4 py-2.5">
<div> <div>
<div className="text-xs font-body font-bold text-white">Auto-Pilot</div> <div className="text-xs font-body font-bold text-white">Auto-Pilot</div>
<div className="text-xs text-[#555] font-body">{settings.autopilot_enabled ? 'Stories auto-publish' : 'Manual review'}</div> <div className="text-xs text-[#555] font-body">{settings.autopilot_enabled ? 'Stories auto-publish' : 'Manual review'}</div>
@@ -291,13 +291,13 @@ export default function CompanyTrackerPage() {
await fetch('/api/admin/company-coverage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update_settings', autopilot_enabled: newVal, reminder_email: settings.reminder_email }) }); await fetch('/api/admin/company-coverage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update_settings', autopilot_enabled: newVal, reminder_email: settings.reminder_email }) });
showToast(newVal ? '🚀 Auto-Pilot enabled' : '⏸ Auto-Pilot disabled'); showToast(newVal ? '🚀 Auto-Pilot enabled' : '⏸ Auto-Pilot disabled');
}} }}
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#3b82f6]' : 'bg-[#333]'}`} className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#1D4ED8]' : 'bg-[#333]'}`}
> >
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${settings.autopilot_enabled ? 'translate-x-5' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${settings.autopilot_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button> </button>
</div> </div>
<button onClick={handleScan} disabled={scanning} <button onClick={handleScan} disabled={scanning}
className="flex items-center gap-2 px-4 py-2 bg-[#3b82f6] hover:bg-blue-500 disabled:opacity-50 text-white text-sm font-body font-semibold rounded-lg transition-colors"> className="flex items-center gap-2 px-4 py-2 bg-[#1D4ED8] hover:bg-blue-500 disabled:opacity-50 text-white text-sm font-body font-semibold rounded-lg transition-colors">
{scanning ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>} {scanning ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>}
{scanning ? 'Scanning…' : 'Scan Articles Now'} {scanning ? 'Scanning…' : 'Scan Articles Now'}
</button> </button>
@@ -313,7 +313,7 @@ export default function CompanyTrackerPage() {
{ label: 'Companies Found', value: settings.last_scan_companies_found }, { label: 'Companies Found', value: settings.last_scan_companies_found },
{ label: 'Tracked Companies', value: companies.length || '—' }, { label: 'Tracked Companies', value: companies.length || '—' },
].map(stat => ( ].map(stat => (
<div key={stat.label} className="bg-[#111] border border-[#252525] rounded-lg px-4 py-3"> <div key={stat.label} className="bg-[#111] border border-[#DCE6F2] rounded-lg px-4 py-3">
<div className="text-lg font-display font-bold text-white">{stat.value}</div> <div className="text-lg font-display font-bold text-white">{stat.value}</div>
<div className="text-xs text-[#555] font-body">{stat.label}</div> <div className="text-xs text-[#555] font-body">{stat.label}</div>
</div> </div>
@@ -322,10 +322,10 @@ export default function CompanyTrackerPage() {
)} )}
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit"> <div className="flex gap-1 mb-6 bg-[#111] border border-[#DCE6F2] rounded-lg p-1 w-fit">
{tabs.map(tab => ( {tabs.map(tab => (
<button key={tab.id} onClick={() => setActiveTab(tab.id)} <button key={tab.id} onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 rounded-md text-sm font-body font-medium transition-colors ${activeTab === tab.id ? 'bg-[#1a1a1a] text-white' : 'text-[#555] hover:text-[#888]'}`}> className={`px-4 py-2 rounded-md text-sm font-body font-medium transition-colors ${activeTab === tab.id ? 'bg-[#FFFFFF] text-white' : 'text-[#555] hover:text-[#888]'}`}>
{tab.label} {tab.label}
</button> </button>
))} ))}
@@ -337,12 +337,12 @@ export default function CompanyTrackerPage() {
<div className="flex flex-wrap gap-3 mb-4"> <div className="flex flex-wrap gap-3 mb-4">
{(['generated','pending','approved','published','rejected'] as QueueStatus[]).map(s => ( {(['generated','pending','approved','published','rejected'] as QueueStatus[]).map(s => (
<button key={s} onClick={() => setQueueStatus(s)} <button key={s} onClick={() => setQueueStatus(s)}
className={`px-3 py-1.5 rounded-lg text-xs font-body font-medium transition-colors capitalize ${queueStatus === s ? 'bg-[#3b82f6] text-white' : 'bg-[#111] border border-[#252525] text-[#555] hover:text-white'}`}> className={`px-3 py-1.5 rounded-lg text-xs font-body font-medium transition-colors capitalize ${queueStatus === s ? 'bg-[#1D4ED8] text-white' : 'bg-[#111] border border-[#DCE6F2] text-[#555] hover:text-white'}`}>
{s === 'generated' ? 'Pending Review' : s} {s === 'generated' ? 'Pending Review' : s}
</button> </button>
))} ))}
<select value={queueSiteFilter} onChange={e => setQueueSiteFilter(e.target.value)} <select value={queueSiteFilter} onChange={e => setQueueSiteFilter(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none"> className="bg-[#111] border border-[#DCE6F2] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
<option value="1">AV Beat</option> <option value="1">AV Beat</option>
<option value="2">AV Beat</option> <option value="2">AV Beat</option>
@@ -350,18 +350,18 @@ export default function CompanyTrackerPage() {
</div> </div>
{queue.length === 0 ? ( {queue.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-12 text-center"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-12 text-center">
<p className="text-[#555] text-sm font-body">No stories in this queue. Run a scan to generate new story suggestions.</p> <p className="text-[#555] text-sm font-body">No stories in this queue. Run a scan to generate new story suggestions.</p>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{queue.map(item => ( {queue.map(item => (
<div key={item.id} className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div key={item.id} className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-5 py-4 flex items-start gap-4"> <div className="px-5 py-4 flex items-start gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap"> <div className="flex items-center gap-2 mb-1 flex-wrap">
<SiteBadge siteId={item.site_id} /> <SiteBadge siteId={item.site_id} />
<span className="text-xs px-2 py-0.5 rounded bg-[#1a1a1a] text-[#888] font-body capitalize">{item.story_type.replace(/_/g, ' ')}</span> <span className="text-xs px-2 py-0.5 rounded bg-[#FFFFFF] text-[#888] font-body capitalize">{item.story_type.replace(/_/g, ' ')}</span>
{item.quality_confidence && ( {item.quality_confidence && (
<span className={`text-xs px-2 py-0.5 rounded font-body ${item.quality_confidence >= 0.85 ? 'bg-green-500/10 text-green-400' : 'bg-yellow-500/10 text-yellow-400'}`}> <span className={`text-xs px-2 py-0.5 rounded font-body ${item.quality_confidence >= 0.85 ? 'bg-green-500/10 text-green-400' : 'bg-yellow-500/10 text-yellow-400'}`}>
{Math.round(item.quality_confidence * 100)}% confidence {Math.round(item.quality_confidence * 100)}% confidence
@@ -398,7 +398,7 @@ export default function CompanyTrackerPage() {
</div> </div>
</div> </div>
{expandedItem === item.id && item.generated_content && ( {expandedItem === item.id && item.generated_content && (
<div className="px-5 pb-5 border-t border-[#1a1a1a]"> <div className="px-5 pb-5 border-t border-[#FFFFFF]">
<div className="mt-4 bg-[#0a0a0a] rounded-lg p-4 text-sm text-[#aaa] font-body leading-relaxed max-h-64 overflow-y-auto" <div className="mt-4 bg-[#0a0a0a] rounded-lg p-4 text-sm text-[#aaa] font-body leading-relaxed max-h-64 overflow-y-auto"
dangerouslySetInnerHTML={{ __html: item.generated_content }} /> dangerouslySetInnerHTML={{ __html: item.generated_content }} />
<div className="mt-3 flex items-center gap-3"> <div className="mt-3 flex items-center gap-3">
@@ -430,30 +430,30 @@ export default function CompanyTrackerPage() {
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<p className="text-sm text-[#555] font-body">{companies.length} companies tracked</p> <p className="text-sm text-[#555] font-body">{companies.length} companies tracked</p>
<button onClick={() => setShowAddCompany(true)} className="flex items-center gap-2 px-4 py-2 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors"> <button onClick={() => setShowAddCompany(true)} className="flex items-center gap-2 px-4 py-2 bg-[#1D4ED8] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg>
Add Company Add Company
</button> </button>
</div> </div>
{showAddCompany && ( {showAddCompany && (
<div className="mb-4 bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="mb-4 bg-[#111] border border-[#DCE6F2] rounded-lg p-5">
<h3 className="text-sm font-display font-bold text-white mb-4">Add Company Manually</h3> <h3 className="text-sm font-display font-bold text-white mb-4">Add Company Manually</h3>
<form onSubmit={handleAddCompany} className="grid grid-cols-1 md:grid-cols-2 gap-4"> <form onSubmit={handleAddCompany} className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Company Name *</label> <label className="block text-xs font-body text-[#888] mb-1.5">Company Name *</label>
<input type="text" value={addCompanyForm.company_name} onChange={e => setAddCompanyForm(f => ({ ...f, company_name: e.target.value }))} required <input type="text" value={addCompanyForm.company_name} onChange={e => setAddCompanyForm(f => ({ ...f, company_name: e.target.value }))} required
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]" /> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Website URL</label> <label className="block text-xs font-body text-[#888] mb-1.5">Website URL</label>
<input type="text" value={addCompanyForm.company_website} onChange={e => setAddCompanyForm(f => ({ ...f, company_website: e.target.value }))} placeholder="example.com" <input type="text" value={addCompanyForm.company_website} onChange={e => setAddCompanyForm(f => ({ ...f, company_website: e.target.value }))} placeholder="example.com"
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]" /> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Site Scope</label> <label className="block text-xs font-body text-[#888] mb-1.5">Site Scope</label>
<select value={addCompanyForm.site_scope} onChange={e => setAddCompanyForm(f => ({ ...f, site_scope: e.target.value }))} <select value={addCompanyForm.site_scope} onChange={e => setAddCompanyForm(f => ({ ...f, site_scope: e.target.value }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]"> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]">
<option value="both">Both Sites</option> <option value="both">Both Sites</option>
<option value="avbeat">AV Beat only</option> <option value="avbeat">AV Beat only</option>
<option value="avbeat">AV Beat only</option> <option value="avbeat">AV Beat only</option>
@@ -462,51 +462,51 @@ export default function CompanyTrackerPage() {
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Crawl Priority</label> <label className="block text-xs font-body text-[#888] mb-1.5">Crawl Priority</label>
<select value={addCompanyForm.crawl_priority} onChange={e => setAddCompanyForm(f => ({ ...f, crawl_priority: e.target.value }))} <select value={addCompanyForm.crawl_priority} onChange={e => setAddCompanyForm(f => ({ ...f, crawl_priority: e.target.value }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]"> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]">
<option value="standard">Standard (weekly)</option> <option value="standard">Standard (weekly)</option>
<option value="high">High (every 3 days)</option> <option value="high">High (every 3 days)</option>
<option value="critical">Critical (daily)</option> <option value="critical">Critical (daily)</option>
</select> </select>
</div> </div>
<div className="md:col-span-2 flex gap-3"> <div className="md:col-span-2 flex gap-3">
<button type="submit" className="px-5 py-2 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">Add Company</button> <button type="submit" className="px-5 py-2 bg-[#1D4ED8] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">Add Company</button>
<button type="button" onClick={() => setShowAddCompany(false)} className="px-4 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">Cancel</button> <button type="button" onClick={() => setShowAddCompany(false)} className="px-4 py-2 bg-[#FFFFFF] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">Cancel</button>
</div> </div>
</form> </form>
</div> </div>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Company', 'Website', 'Mentions', 'Sites', 'Auto-Stories', 'Priority', 'Last Crawl', 'Actions'].map(h => ( {['Company', 'Website', 'Mentions', 'Sites', 'Auto-Stories', 'Priority', 'Last Crawl', 'Actions'].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th> <th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#1a1a1a]"> <tbody className="divide-y divide-[#FFFFFF]">
{companies.length === 0 ? ( {companies.length === 0 ? (
<tr><td colSpan={8} className="px-4 py-12 text-center text-[#555] text-sm font-body">No companies tracked yet. Run a scan to auto-detect companies from published articles.</td></tr> <tr><td colSpan={8} className="px-4 py-12 text-center text-[#555] text-sm font-body">No companies tracked yet. Run a scan to auto-detect companies from published articles.</td></tr>
) : companies.map(company => ( ) : companies.map(company => (
<tr key={company.id} className="hover:bg-[#0d0d0d] transition-colors"> <tr key={company.id} className="hover:bg-[#F8FAFC] transition-colors">
<td className="px-4 py-3 text-sm font-body font-semibold text-white">{company.company_name}</td> <td className="px-4 py-3 text-sm font-body font-semibold text-white">{company.company_name}</td>
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.company_website ? <a href={`https://${company.company_website}`} target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">{company.company_website}</a> : '—'}</td> <td className="px-4 py-3 text-xs text-[#555] font-body">{company.company_website ? <a href={`https://${company.company_website}`} target="_blank" rel="noopener noreferrer" className="text-[#1D4ED8] hover:underline">{company.company_website}</a> : '—'}</td>
<td className="px-4 py-3 text-sm font-body text-white font-bold">{company.mention_count}</td> <td className="px-4 py-3 text-sm font-body text-white font-bold">{company.mention_count}</td>
<td className="px-4 py-3 text-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'avbeat' ? 'BB' : 'AV'}</td> <td className="px-4 py-3 text-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'avbeat' ? 'BB' : 'AV'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<button onClick={() => handleToggleCompanyAutoStory(company.id, !company.auto_story_enabled)} disabled={actionLoading === company.id} <button onClick={() => handleToggleCompanyAutoStory(company.id, !company.auto_story_enabled)} disabled={actionLoading === company.id}
className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#3b82f6]' : 'bg-[#333]'}`}> className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#1D4ED8]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${company.auto_story_enabled ? 'translate-x-4' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${company.auto_story_enabled ? 'translate-x-4' : 'translate-x-0'}`} />
</button> </button>
</td> </td>
<td className="px-4 py-3"><span className={`text-xs px-2 py-0.5 rounded font-body capitalize ${company.crawl_priority === 'critical' ? 'bg-red-500/10 text-red-400' : company.crawl_priority === 'high' ? 'bg-yellow-500/10 text-yellow-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{company.crawl_priority}</span></td> <td className="px-4 py-3"><span className={`text-xs px-2 py-0.5 rounded font-body capitalize ${company.crawl_priority === 'critical' ? 'bg-red-500/10 text-red-400' : company.crawl_priority === 'high' ? 'bg-yellow-500/10 text-yellow-400' : 'bg-[#FFFFFF] text-[#555]'}`}>{company.crawl_priority}</span></td>
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.last_crawled ? timeAgo(company.last_crawled) : 'Never'}</td> <td className="px-4 py-3 text-xs text-[#555] font-body">{company.last_crawled ? timeAgo(company.last_crawled) : 'Never'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={async () => { await fetch('/api/admin/company-coverage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'force_crawl', company_id: company.id }) }); showToast('Crawl scheduled'); }} <button onClick={async () => { await fetch('/api/admin/company-coverage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'force_crawl', company_id: company.id }) }); showToast('Crawl scheduled'); }}
className="text-xs text-[#3b82f6] hover:text-blue-300 font-body transition-colors">Force Crawl</button> className="text-xs text-[#1D4ED8] hover:text-blue-300 font-body transition-colors">Force Crawl</button>
<button onClick={() => handleDeleteCompany(company.id)} disabled={actionLoading === company.id} <button onClick={() => handleDeleteCompany(company.id)} disabled={actionLoading === company.id}
className="text-xs text-red-500 hover:text-red-400 font-body transition-colors disabled:opacity-50">Remove</button> className="text-xs text-red-500 hover:text-red-400 font-body transition-colors disabled:opacity-50">Remove</button>
</div> </div>
@@ -522,21 +522,21 @@ export default function CompanyTrackerPage() {
{/* ── Crawl Log Tab ── */} {/* ── Crawl Log Tab ── */}
{activeTab === 'crawl-log' && ( {activeTab === 'crawl-log' && (
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Date', 'Company', 'Source URL', 'Items Found', 'Stories Queued', 'Status'].map(h => ( {['Date', 'Company', 'Source URL', 'Items Found', 'Stories Queued', 'Status'].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th> <th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#1a1a1a]"> <tbody className="divide-y divide-[#FFFFFF]">
{crawlLogs.length === 0 ? ( {crawlLogs.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-12 text-center text-[#555] text-sm font-body">No crawl history yet.</td></tr> <tr><td colSpan={6} className="px-4 py-12 text-center text-[#555] text-sm font-body">No crawl history yet.</td></tr>
) : crawlLogs.map(log => ( ) : crawlLogs.map(log => (
<tr key={log.id} className="hover:bg-[#0d0d0d] transition-colors"> <tr key={log.id} className="hover:bg-[#F8FAFC] transition-colors">
<td className="px-4 py-3 text-xs text-[#555] font-body">{new Date(log.crawled_at).toLocaleString()}</td> <td className="px-4 py-3 text-xs text-[#555] font-body">{new Date(log.crawled_at).toLocaleString()}</td>
<td className="px-4 py-3 text-sm text-white font-body">{log.company_name}</td> <td className="px-4 py-3 text-sm text-white font-body">{log.company_name}</td>
<td className="px-4 py-3 text-xs text-[#555] font-body truncate max-w-xs">{log.source_url || '—'}</td> <td className="px-4 py-3 text-xs text-[#555] font-body truncate max-w-xs">{log.source_url || '—'}</td>
@@ -558,7 +558,7 @@ export default function CompanyTrackerPage() {
{/* ── Settings Tab ── */} {/* ── Settings Tab ── */}
{activeTab === 'settings' && ( {activeTab === 'settings' && (
<div className="max-w-2xl space-y-6"> <div className="max-w-2xl space-y-6">
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-6">
<h3 className="text-sm font-display font-bold text-white mb-4">Auto-Pilot & Reminders</h3> <h3 className="text-sm font-display font-bold text-white mb-4">Auto-Pilot & Reminders</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -567,19 +567,19 @@ export default function CompanyTrackerPage() {
<div className="text-xs text-[#555] font-body mt-0.5">When enabled, high-confidence stories publish automatically without review</div> <div className="text-xs text-[#555] font-body mt-0.5">When enabled, high-confidence stories publish automatically without review</div>
</div> </div>
<button onClick={() => setSettings(s => ({ ...s, autopilot_enabled: !s.autopilot_enabled }))} <button onClick={() => setSettings(s => ({ ...s, autopilot_enabled: !s.autopilot_enabled }))}
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#3b82f6]' : 'bg-[#333]'}`}> className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#1D4ED8]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${settings.autopilot_enabled ? 'translate-x-5' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${settings.autopilot_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button> </button>
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Reminder Email</label> <label className="block text-xs font-body text-[#888] mb-1.5">Reminder Email</label>
<input type="email" value={settings.reminder_email} onChange={e => setSettings(s => ({ ...s, reminder_email: e.target.value }))} <input type="email" value={settings.reminder_email} onChange={e => setSettings(s => ({ ...s, reminder_email: e.target.value }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]" /> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</div> </div>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-6">
<h3 className="text-sm font-display font-bold text-white mb-4">Story Generation Settings</h3> <h3 className="text-sm font-display font-bold text-white mb-4">Story Generation Settings</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -588,7 +588,7 @@ export default function CompanyTrackerPage() {
<div className="text-xs text-[#555] font-body mt-0.5">Master switch for all automated story generation</div> <div className="text-xs text-[#555] font-body mt-0.5">Master switch for all automated story generation</div>
</div> </div>
<button onClick={() => setTrackerSettings(s => ({ ...s, auto_story_global_enabled: !s.auto_story_global_enabled }))} <button onClick={() => setTrackerSettings(s => ({ ...s, auto_story_global_enabled: !s.auto_story_global_enabled }))}
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_story_global_enabled ? 'bg-[#3b82f6]' : 'bg-[#333]'}`}> className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_story_global_enabled ? 'bg-[#1D4ED8]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${trackerSettings.auto_story_global_enabled ? 'translate-x-5' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${trackerSettings.auto_story_global_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button> </button>
</div> </div>
@@ -598,25 +598,25 @@ export default function CompanyTrackerPage() {
<div className="text-xs text-[#555] font-body mt-0.5">Publish stories with confidence {Math.round(trackerSettings.auto_publish_confidence_threshold * 100)}% automatically</div> <div className="text-xs text-[#555] font-body mt-0.5">Publish stories with confidence {Math.round(trackerSettings.auto_publish_confidence_threshold * 100)}% automatically</div>
</div> </div>
<button onClick={() => setTrackerSettings(s => ({ ...s, auto_publish_enabled: !s.auto_publish_enabled }))} <button onClick={() => setTrackerSettings(s => ({ ...s, auto_publish_enabled: !s.auto_publish_enabled }))}
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_publish_enabled ? 'bg-[#3b82f6]' : 'bg-[#333]'}`}> className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_publish_enabled ? 'bg-[#1D4ED8]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${trackerSettings.auto_publish_enabled ? 'translate-x-5' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${trackerSettings.auto_publish_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button> </button>
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Mention Threshold (min mentions to qualify for auto-stories)</label> <label className="block text-xs font-body text-[#888] mb-1.5">Mention Threshold (min mentions to qualify for auto-stories)</label>
<input type="number" min={1} max={20} value={trackerSettings.mention_threshold} onChange={e => setTrackerSettings(s => ({ ...s, mention_threshold: parseInt(e.target.value) }))} <input type="number" min={1} max={20} value={trackerSettings.mention_threshold} onChange={e => setTrackerSettings(s => ({ ...s, mention_threshold: parseInt(e.target.value) }))}
className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]" /> className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Max Stories Per Company Per Week</label> <label className="block text-xs font-body text-[#888] mb-1.5">Max Stories Per Company Per Week</label>
<input type="number" min={1} max={20} value={trackerSettings.max_stories_per_company_per_week} onChange={e => setTrackerSettings(s => ({ ...s, max_stories_per_company_per_week: parseInt(e.target.value) }))} <input type="number" min={1} max={20} value={trackerSettings.max_stories_per_company_per_week} onChange={e => setTrackerSettings(s => ({ ...s, max_stories_per_company_per_week: parseInt(e.target.value) }))}
className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]" /> className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</div> </div>
</div> </div>
<button onClick={handleSaveSettings} disabled={savingSettings} <button onClick={handleSaveSettings} disabled={savingSettings}
className="px-6 py-2.5 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50"> className="px-6 py-2.5 bg-[#1D4ED8] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50">
{savingSettings ? 'Saving...' : 'Save Settings'} {savingSettings ? 'Saving...' : 'Save Settings'}
</button> </button>
</div> </div>

View File

@@ -47,7 +47,7 @@ interface LaunchChecklist {
// ─── Site labels ────────────────────────────────────────────────────────────── // ─── Site labels ──────────────────────────────────────────────────────────────
const SITE_LABELS: Record<string, { label: string; color: string; target: string }> = { const SITE_LABELS: Record<string, { label: string; color: string; target: string }> = {
'broadcast-beat': { label: 'AV Beat', color: 'text-blue-400', target: '1525/day' }, 'avbeat': { label: 'AV Beat', color: 'text-blue-400', target: '1525/day' },
'av-beat': { label: 'AV Beat', color: 'text-purple-400', target: '1015/day' }, 'av-beat': { label: 'AV Beat', color: 'text-purple-400', target: '1015/day' },
'backlot-beat': { label: 'Backlot Beat', color: 'text-amber-400', target: '1015/day' }, 'backlot-beat': { label: 'Backlot Beat', color: 'text-amber-400', target: '1015/day' },
'broadcast-engineering': { label: 'BroadcastEngineering.com', color: 'text-green-400', target: '1015/day' }, 'broadcast-engineering': { label: 'BroadcastEngineering.com', color: 'text-green-400', target: '1015/day' },
@@ -97,7 +97,7 @@ function StatCard({
good?: boolean; good?: boolean;
}) { }) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-2"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5 flex flex-col gap-2">
<p className={`text-2xl font-display font-bold ${accent}`}>{value}</p> <p className={`text-2xl font-display font-bold ${accent}`}>{value}</p>
<p className="text-sm text-[#888] font-body">{label}</p> <p className="text-sm text-[#888] font-body">{label}</p>
{sub && <p className="text-xs text-[#555] font-body">{sub}</p>} {sub && <p className="text-xs text-[#555] font-body">{sub}</p>}
@@ -124,7 +124,7 @@ function ChecklistItem({ label, done }: { label: string; done: boolean }) {
type RMPSite = 'broadcast-beat' | 'av-beat' | 'backlot-beat' | 'broadcast-engineering'; type RMPSite = 'broadcast-beat' | 'av-beat' | 'backlot-beat' | 'broadcast-engineering';
function GeneratePanel({ onGenerated }: { onGenerated: () => void }) { function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
const [site, setSite] = useState<RMPSite>('broadcast-beat'); const [site, setSite] = useState<RMPSite>('avbeat');
const [topic, setTopic] = useState(''); const [topic, setTopic] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [result, setResult] = useState<string | null>(null); const [result, setResult] = useState<string | null>(null);
@@ -173,7 +173,7 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
}; };
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-4"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5 flex flex-col gap-4">
<h3 className="text-sm font-display font-bold text-white">Manual Controls</h3> <h3 className="text-sm font-display font-bold text-white">Manual Controls</h3>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div> <div>
@@ -181,7 +181,7 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
<select <select
value={site} value={site}
onChange={(e) => setSite(e.target.value as RMPSite)} onChange={(e) => setSite(e.target.value as RMPSite)}
className="w-full bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#FFFFFF] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#1D4ED8]"
> >
{Object.entries(SITE_LABELS).map(([key, val]) => ( {Object.entries(SITE_LABELS).map(([key, val]) => (
<option key={key} value={key}>{val.label}</option> <option key={key} value={key}>{val.label}</option>
@@ -195,21 +195,21 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
value={topic} value={topic}
onChange={(e) => setTopic(e.target.value)} onChange={(e) => setTopic(e.target.value)}
placeholder="e.g. ATSC 3.0 deployment update" placeholder="e.g. ATSC 3.0 deployment update"
className="w-full bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body placeholder-[#444] focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#FFFFFF] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body placeholder-[#444] focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={handleGenerate} onClick={handleGenerate}
disabled={loading} disabled={loading}
className="flex-1 bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors" className="flex-1 bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
> >
{loading ? 'Generating…' : 'Generate Article'} {loading ? 'Generating…' : 'Generate Article'}
</button> </button>
<button <button
onClick={handleSEOBatch} onClick={handleSEOBatch}
disabled={loading} disabled={loading}
className="flex-1 bg-[#1a1a1a] hover:bg-[#252525] disabled:opacity-50 border border-[#333] text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors" className="flex-1 bg-[#FFFFFF] hover:bg-[#DCE6F2] disabled:opacity-50 border border-[#333] text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
> >
{loading ? 'Running…' : 'Batch SEO Meta'} {loading ? 'Running…' : 'Batch SEO Meta'}
</button> </button>
@@ -279,7 +279,7 @@ export default function ContentStatusPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading content status</p> <p className="text-[#555] text-sm font-body">Loading content status</p>
</div> </div>
</div> </div>
@@ -296,7 +296,7 @@ export default function ContentStatusPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-white"> <div className="min-h-screen bg-[#0a0a0a] text-white">
{/* Header */} {/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0d0d0d] px-6 py-4"> <div className="border-b border-[#FFFFFF] bg-[#F8FAFC] px-6 py-4">
<div className="max-w-7xl mx-auto flex items-center justify-between"> <div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-white transition-colors text-sm font-body"> <Link href="/admin" className="text-[#555] hover:text-white transition-colors text-sm font-body">
@@ -327,7 +327,7 @@ export default function ContentStatusPage() {
</button> </button>
<button <button
onClick={fetchAll} onClick={fetchAll}
className="text-xs font-body px-3 py-1.5 rounded-lg border border-[#333] text-white bg-[#111] hover:bg-[#1a1a1a] transition-colors" className="text-xs font-body px-3 py-1.5 rounded-lg border border-[#333] text-white bg-[#111] hover:bg-[#FFFFFF] transition-colors"
> >
Refresh Now Refresh Now
</button> </button>
@@ -395,7 +395,7 @@ export default function ContentStatusPage() {
{Object.entries(SITE_LABELS).map(([key, cfg]) => { {Object.entries(SITE_LABELS).map(([key, cfg]) => {
const count = status?.siteBreakdown?.[key] ?? 0; const count = status?.siteBreakdown?.[key] ?? 0;
return ( return (
<div key={key} className="bg-[#111] border border-[#252525] rounded-lg p-4 flex flex-col gap-2"> <div key={key} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 flex flex-col gap-2">
<p className={`text-xs font-body font-semibold ${cfg.color}`}>{cfg.label}</p> <p className={`text-xs font-body font-semibold ${cfg.color}`}>{cfg.label}</p>
<p className="text-3xl font-display font-bold text-white">{count}</p> <p className="text-3xl font-display font-bold text-white">{count}</p>
<p className="text-xs text-[#555] font-body">Target: {cfg.target}</p> <p className="text-xs text-[#555] font-body">Target: {cfg.target}</p>
@@ -484,7 +484,7 @@ export default function ContentStatusPage() {
{/* API Reference */} {/* API Reference */}
<section> <section>
<h2 className="text-sm font-display font-bold text-[#888] uppercase tracking-wider mb-4">API Endpoints</h2> <h2 className="text-sm font-display font-bold text-[#888] uppercase tracking-wider mb-4">API Endpoints</h2>
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5 grid grid-cols-1 sm:grid-cols-2 gap-3">
{[ {[
{ method: 'GET', path: '/api/content/audit', desc: 'Full content audit report' }, { method: 'GET', path: '/api/content/audit', desc: 'Full content audit report' },
{ method: 'GET', path: '/api/content/pipeline-status', desc: 'Live pipeline + node status' }, { method: 'GET', path: '/api/content/pipeline-status', desc: 'Live pipeline + node status' },

View File

@@ -90,7 +90,7 @@ function StatCard({
label, label,
value, value,
sub, sub,
accent = '#3b82f6', accent = '#1D4ED8',
icon, icon,
trend, trend,
}: { }: {
@@ -102,7 +102,7 @@ function StatCard({
trend?: { value: number; label: string }; trend?: { value: number; label: string };
}) { }) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-xl p-5 flex flex-col gap-3"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl p-5 flex flex-col gap-3">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div <div
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0" className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
@@ -134,7 +134,7 @@ function StatCard({
function ChartTooltip({ active, payload, label }: any) { function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null; if (!active || !payload?.length) return null;
return ( return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl"> <div className="bg-[#FFFFFF] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1.5 font-semibold">{label}</p>} {label && <p className="text-[#888] mb-1.5 font-semibold">{label}</p>}
{payload.map((entry: any, i: number) => ( {payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5"> <p key={i} style={{ color: entry.color }} className="leading-5">
@@ -252,7 +252,7 @@ export default function EmailAnalyticsDashboard() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading analytics</p> <p className="text-[#555] text-sm font-body">Loading analytics</p>
</div> </div>
</div> </div>
@@ -262,7 +262,7 @@ export default function EmailAnalyticsDashboard() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-white"> <div className="min-h-screen bg-[#0a0a0a] text-white">
{/* Header */} {/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0d0d0d] sticky top-0 z-10"> <div className="border-b border-[#FFFFFF] bg-[#F8FAFC] sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between"> <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-white transition-colors"> <Link href="/admin" className="text-[#555] hover:text-white transition-colors">
@@ -270,7 +270,7 @@ export default function EmailAnalyticsDashboard() {
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /> <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg> </svg>
</Link> </Link>
<div className="w-px h-5 bg-[#252525]" /> <div className="w-px h-5 bg-[#DCE6F2]" />
<span className="text-white text-sm font-body font-semibold">Email Analytics</span> <span className="text-white text-sm font-body font-semibold">Email Analytics</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -280,17 +280,17 @@ export default function EmailAnalyticsDashboard() {
onClick={() => setTimeRange(r)} onClick={() => setTimeRange(r)}
className={`px-3 py-1.5 text-xs font-semibold font-body rounded-lg transition-colors ${ className={`px-3 py-1.5 text-xs font-semibold font-body rounded-lg transition-colors ${
timeRange === r timeRange === r
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'text-[#555] hover:text-white border border-[#252525] hover:border-[#444]' : 'text-[#555] hover:text-white border border-[#DCE6F2] hover:border-[#444]'
}`} }`}
> >
{r === '3m' ? '3 Mo' : r === '6m' ? '6 Mo' : 'All'} {r === '3m' ? '3 Mo' : r === '6m' ? '6 Mo' : 'All'}
</button> </button>
))} ))}
<div className="w-px h-6 bg-[#252525] mx-1" /> <div className="w-px h-6 bg-[#DCE6F2] mx-1" />
<Link <Link
href="/admin/newsletter/analytics" href="/admin/newsletter/analytics"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#DCE6F2] rounded-lg transition-colors"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 10h18M3 14h18M10 3v18M14 3v18" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 10h18M3 14h18M10 3v18M14 3v18" />
@@ -317,7 +317,7 @@ export default function EmailAnalyticsDashboard() {
label="Active Subscribers" label="Active Subscribers"
value={fmt(stats.activeSubscribers)} value={fmt(stats.activeSubscribers)}
sub={`${fmt(stats.unsubscribed)} unsubscribed`} sub={`${fmt(stats.unsubscribed)} unsubscribed`}
accent="#3b82f6" accent="#1D4ED8"
trend={{ value: 12, label: 'vs last month' }} trend={{ value: 12, label: 'vs last month' }}
icon={ icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -365,7 +365,7 @@ export default function EmailAnalyticsDashboard() {
</div> </div>
{/* Send Volume Chart */} {/* Send Volume Chart */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl p-6">
<SectionHeader <SectionHeader
title="Send Volume Over Time" title="Send Volume Over Time"
subtitle="Total emails sent, opens, and clicks per month" subtitle="Total emails sent, opens, and clicks per month"
@@ -378,7 +378,7 @@ export default function EmailAnalyticsDashboard() {
<YAxis tick={{ fill: '#555', fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => fmt(v)} /> <YAxis tick={{ fill: '#555', fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => fmt(v)} />
<Tooltip content={<ChartTooltip />} /> <Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11, color: '#888', paddingTop: 12 }} /> <Legend wrapperStyle={{ fontSize: 11, color: '#888', paddingTop: 12 }} />
<Bar dataKey="sent" name="Sent" fill="#3b82f620" stroke="#3b82f6" strokeWidth={1} radius={[3, 3, 0, 0]} /> <Bar dataKey="sent" name="Sent" fill="#3b82f620" stroke="#1D4ED8" strokeWidth={1} radius={[3, 3, 0, 0]} />
<Bar dataKey="opens" name="Opens" fill="#10b98120" stroke="#10b981" strokeWidth={1} radius={[3, 3, 0, 0]} /> <Bar dataKey="opens" name="Opens" fill="#10b98120" stroke="#10b981" strokeWidth={1} radius={[3, 3, 0, 0]} />
<Bar dataKey="clicks" name="Clicks" fill="#f59e0b20" stroke="#f59e0b" strokeWidth={1} radius={[3, 3, 0, 0]} /> <Bar dataKey="clicks" name="Clicks" fill="#f59e0b20" stroke="#f59e0b" strokeWidth={1} radius={[3, 3, 0, 0]} />
</ComposedChart> </ComposedChart>
@@ -387,7 +387,7 @@ export default function EmailAnalyticsDashboard() {
</div> </div>
{/* Open Rate & CTR Trend */} {/* Open Rate & CTR Trend */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl p-6">
<SectionHeader <SectionHeader
title="Open Rate & Click-Through Rate Trend" title="Open Rate & Click-Through Rate Trend"
subtitle="Monthly engagement rates as a percentage of recipients" subtitle="Monthly engagement rates as a percentage of recipients"
@@ -397,8 +397,8 @@ export default function EmailAnalyticsDashboard() {
<AreaChart data={filteredRates} margin={{ top: 4, right: 8, left: -10, bottom: 0 }}> <AreaChart data={filteredRates} margin={{ top: 4, right: 8, left: -10, bottom: 0 }}>
<defs> <defs>
<linearGradient id="openGrad" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="openGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.2} /> <stop offset="5%" stopColor="#1D4ED8" stopOpacity={0.2} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} /> <stop offset="95%" stopColor="#1D4ED8" stopOpacity={0} />
</linearGradient> </linearGradient>
<linearGradient id="clickGrad" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="clickGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.2} /> <stop offset="5%" stopColor="#10b981" stopOpacity={0.2} />
@@ -418,10 +418,10 @@ export default function EmailAnalyticsDashboard() {
<Area <Area
type="monotone" type="monotone"
dataKey="Open Rate" dataKey="Open Rate"
stroke="#3b82f6" stroke="#1D4ED8"
strokeWidth={2} strokeWidth={2}
fill="url(#openGrad)" fill="url(#openGrad)"
dot={{ r: 3, fill: '#3b82f6' }} dot={{ r: 3, fill: '#1D4ED8' }}
activeDot={{ r: 5 }} activeDot={{ r: 5 }}
/> />
<Area <Area
@@ -441,7 +441,7 @@ export default function EmailAnalyticsDashboard() {
{/* Subscriber Growth */} {/* Subscriber Growth */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Growth Over Time */} {/* Growth Over Time */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl p-6">
<SectionHeader <SectionHeader
title="Subscriber Growth" title="Subscriber Growth"
subtitle="Total subscribers and monthly net change" subtitle="Total subscribers and monthly net change"
@@ -463,14 +463,14 @@ export default function EmailAnalyticsDashboard() {
fill="#8b5cf618" fill="#8b5cf618"
dot={false} dot={false}
/> />
<Bar dataKey="net" name="Net New" fill="#3b82f6" radius={[3, 3, 0, 0]} /> <Bar dataKey="net" name="Net New" fill="#1D4ED8" radius={[3, 3, 0, 0]} />
</ComposedChart> </ComposedChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</div> </div>
{/* New vs Unsubscribed */} {/* New vs Unsubscribed */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl p-6">
<SectionHeader <SectionHeader
title="New Subscribers vs Unsubscribes" title="New Subscribers vs Unsubscribes"
subtitle="Monthly acquisition vs churn" subtitle="Monthly acquisition vs churn"
@@ -492,7 +492,7 @@ export default function EmailAnalyticsDashboard() {
</div> </div>
{/* Benchmark Comparison */} {/* Benchmark Comparison */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl p-6">
<SectionHeader <SectionHeader
title="Performance vs Industry Benchmarks" title="Performance vs Industry Benchmarks"
subtitle="How your metrics compare to email marketing industry averages" subtitle="How your metrics compare to email marketing industry averages"
@@ -503,7 +503,7 @@ export default function EmailAnalyticsDashboard() {
label: 'Open Rate', label: 'Open Rate',
yours: stats.avgOpenRate, yours: stats.avgOpenRate,
benchmark: 21.5, benchmark: 21.5,
color: '#3b82f6', color: '#1D4ED8',
unit: '%', unit: '%',
}, },
{ {
@@ -584,7 +584,7 @@ export default function EmailAnalyticsDashboard() {
<div className="flex flex-wrap gap-3 pb-4"> <div className="flex flex-wrap gap-3 pb-4">
<Link <Link
href="/admin/newsletter" href="/admin/newsletter"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#252525] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors" className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#DCE6F2] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /> <path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
@@ -593,7 +593,7 @@ export default function EmailAnalyticsDashboard() {
</Link> </Link>
<Link <Link
href="/admin/newsletter/analytics" href="/admin/newsletter/analytics"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#252525] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors" className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#DCE6F2] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
@@ -602,7 +602,7 @@ export default function EmailAnalyticsDashboard() {
</Link> </Link>
<Link <Link
href="/admin/newsletter/templates" href="/admin/newsletter/templates"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#252525] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors" className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#DCE6F2] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />

View File

@@ -75,7 +75,7 @@ function DomainsContent() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -86,41 +86,41 @@ function DomainsContent() {
<h1 className="text-xl font-bold">Sending Domains</h1> <h1 className="text-xl font-bold">Sending Domains</h1>
<Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link> <Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link>
</div> </div>
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button> <button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
{showAddForm && ( {showAddForm && (
<form onSubmit={handleAddDomain} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleAddDomain} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Add Rotation Domain</h3> <h3 className="text-sm font-semibold">Add Rotation Domain</h3>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Site *</label> <label className="text-xs text-[#888] block mb-1">Site *</label>
<select required value={addForm.site} onChange={e => setAddForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select required value={addForm.site} onChange={e => setAddForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
<option value="">Select</option> <option value="">Select</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Domain *</label> <label className="text-xs text-[#888] block mb-1">Domain *</label>
<input required value={addForm.domain} onChange={e => setAddForm(p => ({ ...p, domain: e.target.value }))} placeholder="mail.example.com" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input required value={addForm.domain} onChange={e => setAddForm(p => ({ ...p, domain: e.target.value }))} placeholder="mail.example.com" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Type</label> <label className="text-xs text-[#888] block mb-1">Type</label>
<select value={addForm.domain_type} onChange={e => setAddForm(p => ({ ...p, domain_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={addForm.domain_type} onChange={e => setAddForm(p => ({ ...p, domain_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
{['subdomain','sister_domain','primary'].map(t => <option key={t} value={t}>{t}</option>)} {['subdomain','sister_domain','primary'].map(t => <option key={t} value={t}>{t}</option>)}
</select> </select>
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Domain'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Domain'}</button>
<button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
@@ -131,10 +131,10 @@ function DomainsContent() {
<div className="bg-[#111] border border-yellow-400/30 rounded-lg p-5 w-80 space-y-3"> <div className="bg-[#111] border border-yellow-400/30 rounded-lg p-5 w-80 space-y-3">
<h3 className="text-sm font-semibold text-yellow-400"> Override DNS Verification</h3> <h3 className="text-sm font-semibold text-yellow-400"> Override DNS Verification</h3>
<p className="text-xs text-[#888]">This allows sending before DNS is verified. Use with caution.</p> <p className="text-xs text-[#888]">This allows sending before DNS is verified. Use with caution.</p>
<textarea value={bypassReason} onChange={e => setBypassReason(e.target.value)} placeholder="Reason for bypass…" rows={3} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <textarea value={bypassReason} onChange={e => setBypassReason(e.target.value)} placeholder="Reason for bypass…" rows={3} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none" />
<div className="flex gap-2"> <div className="flex gap-2">
<button onClick={() => setBypass(showBypass)} disabled={!bypassReason} className="px-3 py-1.5 bg-yellow-600 rounded text-xs font-medium hover:bg-yellow-500 disabled:opacity-50">Set Bypass</button> <button onClick={() => setBypass(showBypass)} disabled={!bypassReason} className="px-3 py-1.5 bg-yellow-600 rounded text-xs font-medium hover:bg-yellow-500 disabled:opacity-50">Set Bypass</button>
<button onClick={() => setShowBypass(null)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button onClick={() => setShowBypass(null)} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</div> </div>
</div> </div>
@@ -142,12 +142,12 @@ function DomainsContent() {
<div className="space-y-2"> <div className="space-y-2">
{loadingData ? ( {loadingData ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-8 text-center text-[#555] text-xs">Loading</div> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-8 text-center text-[#555] text-xs">Loading</div>
) : domains.length === 0 ? ( ) : domains.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-8 text-center text-[#555] text-xs">No domains found</div> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-8 text-center text-[#555] text-xs">No domains found</div>
) : domains.map((d: any) => ( ) : domains.map((d: any) => (
<div key={d.id} className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div key={d.id} className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-4 py-3 flex items-center gap-3 cursor-pointer hover:bg-[#161616]" onClick={() => setExpanded(expanded === d.id ? null : d.id)}> <div className="px-4 py-3 flex items-center gap-3 cursor-pointer hover:bg-[#FFFFFF]" onClick={() => setExpanded(expanded === d.id ? null : d.id)}>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-white">{d.domain}</span> <span className="text-sm font-medium text-white">{d.domain}</span>
@@ -156,7 +156,7 @@ function DomainsContent() {
<span className={`px-1.5 py-0.5 rounded text-xs ${d.dns_fully_verified ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{d.dns_fully_verified ? 'DNS OK' : 'DNS Pending'}</span> <span className={`px-1.5 py-0.5 rounded text-xs ${d.dns_fully_verified ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{d.dns_fully_verified ? 'DNS OK' : 'DNS Pending'}</span>
{d.blacklisted && <span className="px-1.5 py-0.5 bg-red-400/10 text-red-400 rounded text-xs">Blacklisted</span>} {d.blacklisted && <span className="px-1.5 py-0.5 bg-red-400/10 text-red-400 rounded text-xs">Blacklisted</span>}
{d.bypass_verification && <span className="px-1.5 py-0.5 bg-yellow-400/10 text-yellow-400 rounded text-xs">Bypass Active</span>} {d.bypass_verification && <span className="px-1.5 py-0.5 bg-yellow-400/10 text-yellow-400 rounded text-xs">Bypass Active</span>}
<span className={`px-1.5 py-0.5 rounded text-xs ${d.is_active ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{d.is_active ? 'Active' : 'Inactive'}</span> <span className={`px-1.5 py-0.5 rounded text-xs ${d.is_active ? 'bg-green-400/10 text-green-400' : 'bg-[#FFFFFF] text-[#555]'}`}>{d.is_active ? 'Active' : 'Inactive'}</span>
</div> </div>
<div className="flex gap-3 mt-1 text-xs text-[#555]"> <div className="flex gap-3 mt-1 text-xs text-[#555]">
<span>Health: {d.health_score}/100</span> <span>Health: {d.health_score}/100</span>
@@ -167,7 +167,7 @@ function DomainsContent() {
</div> </div>
{expanded === d.id && ( {expanded === d.id && (
<div className="border-t border-[#1a1a1a] px-4 py-3 space-y-3"> <div className="border-t border-[#FFFFFF] px-4 py-3 space-y-3">
{/* DNS checks */} {/* DNS checks */}
<div className="grid grid-cols-2 gap-x-6"> <div className="grid grid-cols-2 gap-x-6">
<DnsRow label="SPF" valid={d.spf_valid} checkedAt={d.spf_checked_at} /> <DnsRow label="SPF" valid={d.spf_valid} checkedAt={d.spf_checked_at} />
@@ -178,7 +178,7 @@ function DomainsContent() {
</div> </div>
{/* DNS helper panel */} {/* DNS helper panel */}
<div className="bg-[#0a0a0a] border border-[#1a1a1a] rounded p-3 space-y-2"> <div className="bg-[#0a0a0a] border border-[#FFFFFF] rounded p-3 space-y-2">
<p className="text-xs font-semibold text-[#888]">DNS Record Helper (GoDaddy)</p> <p className="text-xs font-semibold text-[#888]">DNS Record Helper (GoDaddy)</p>
<div className="space-y-1 text-xs text-[#555] font-mono"> <div className="space-y-1 text-xs text-[#555] font-mono">
<p>SPF: <span className="text-[#888]">v=spf1 include:sendgrid.net ~all</span></p> <p>SPF: <span className="text-[#888]">v=spf1 include:sendgrid.net ~all</span></p>
@@ -197,10 +197,10 @@ function DomainsContent() {
{/* Actions */} {/* Actions */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<button onClick={() => runVerify(d.id)} disabled={verifying === d.id} className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] disabled:opacity-50"> <button onClick={() => runVerify(d.id)} disabled={verifying === d.id} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#2a3a50] rounded text-xs text-[#1D4ED8] hover:bg-[#0d1a2d] disabled:opacity-50">
{verifying === d.id ? 'Verifying…' : 'Run Full Verification'} {verifying === d.id ? 'Verifying…' : 'Run Full Verification'}
</button> </button>
<button onClick={() => toggleActive(d.id, d.is_active)} disabled={!d.dns_fully_verified && !d.bypass_verification && !d.is_active} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white disabled:opacity-30"> <button onClick={() => toggleActive(d.id, d.is_active)} disabled={!d.dns_fully_verified && !d.bypass_verification && !d.is_active} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white disabled:opacity-30">
{d.is_active ? 'Deactivate' : 'Activate'} {d.is_active ? 'Deactivate' : 'Activate'}
</button> </button>
<button onClick={() => setShowBypass(d.id)} className="px-3 py-1.5 bg-yellow-900/30 border border-yellow-400/20 rounded text-xs text-yellow-400 hover:bg-yellow-900/50"> <button onClick={() => setShowBypass(d.id)} className="px-3 py-1.5 bg-yellow-900/30 border border-yellow-400/20 rounded text-xs text-yellow-400 hover:bg-yellow-900/50">
@@ -219,7 +219,7 @@ function DomainsContent() {
export default function EmailDomainsPage() { export default function EmailDomainsPage() {
return ( return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>}> <Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>}>
<DomainsContent /> <DomainsContent />
</Suspense> </Suspense>
); );

View File

@@ -8,7 +8,7 @@ const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
function DnsCheck({ label, valid }: { label: string; valid: boolean | null }) { function DnsCheck({ label, valid }: { label: string; valid: boolean | null }) {
return ( return (
<span className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded ${valid === true ? 'bg-green-400/10 text-green-400' : valid === false ? 'bg-red-400/10 text-red-400' : 'bg-[#1a1a1a] text-[#555]'}`}> <span className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded ${valid === true ? 'bg-green-400/10 text-green-400' : valid === false ? 'bg-red-400/10 text-red-400' : 'bg-[#FFFFFF] text-[#555]'}`}>
{valid === true ? '✓' : valid === false ? '✗' : '?'} {label} {valid === true ? '✓' : valid === false ? '✗' : '?'} {label}
</span> </span>
); );
@@ -42,7 +42,7 @@ export default function EmailDashboard() {
domains: domains.filter((d: any) => d.site === site), domains: domains.filter((d: any) => d.site === site),
})); }));
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -61,7 +61,7 @@ export default function EmailDashboard() {
{ label: 'Total Suppressions', value: stats?.total_suppressions ?? 0, color: 'text-red-400' }, { label: 'Total Suppressions', value: stats?.total_suppressions ?? 0, color: 'text-red-400' },
{ label: 'Blacklisted Domains', value: stats?.blacklisted ?? 0, color: 'text-red-400' }, { label: 'Blacklisted Domains', value: stats?.blacklisted ?? 0, color: 'text-red-400' },
].map(c => ( ].map(c => (
<div key={c.label} className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div key={c.label} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4">
<p className={`text-2xl font-bold ${c.color}`}>{c.value}</p> <p className={`text-2xl font-bold ${c.color}`}>{c.value}</p>
<p className="text-xs text-[#888] mt-1">{c.label}</p> <p className="text-xs text-[#888] mt-1">{c.label}</p>
</div> </div>
@@ -74,10 +74,10 @@ export default function EmailDashboard() {
const primary = siteDomains.find((d: any) => d.is_primary); const primary = siteDomains.find((d: any) => d.is_primary);
const suppCount = stats?.suppressions_by_site?.[site] ?? 0; const suppCount = stats?.suppressions_by_site?.[site] ?? 0;
return ( return (
<div key={site} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <div key={site} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-semibold capitalize">{site}</h3> <h3 className="text-sm font-semibold capitalize">{site}</h3>
<Link href={`/admin/email/domains?site=${site}`} className="text-xs text-[#3b82f6] hover:underline">Manage </Link> <Link href={`/admin/email/domains?site=${site}`} className="text-xs text-[#1D4ED8] hover:underline">Manage </Link>
</div> </div>
<div className="text-xs text-[#888]"> <div className="text-xs text-[#888]">
<span className="text-white">{primary?.domain ?? 'No primary'}</span> <span className="text-white">{primary?.domain ?? 'No primary'}</span>
@@ -113,7 +113,7 @@ export default function EmailDashboard() {
{ label: 'Suppressions', href: '/admin/email/suppressions' }, { label: 'Suppressions', href: '/admin/email/suppressions' },
{ label: 'Send Log', href: '/admin/email/send-log' }, { label: 'Send Log', href: '/admin/email/send-log' },
].map(l => ( ].map(l => (
<Link key={l.href} href={l.href} className="bg-[#111] border border-[#252525] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#3b82f6] transition-colors"> <Link key={l.href} href={l.href} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#1D4ED8] transition-colors">
{l.label} {l.label}
</Link> </Link>
))} ))}

View File

@@ -29,7 +29,7 @@ export default function SendLogPage() {
useEffect(() => { if (user) load(); }, [user, load]); useEffect(() => { if (user) load(); }, [user, load]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -41,20 +41,20 @@ export default function SendLogPage() {
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option> <option value="">All Statuses</option>
{['queued','sent','failed','suppressed'].map(s => <option key={s} value={s}>{s}</option>)} {['queued','sent','failed','suppressed'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Site','Recipient','Subject','Type','Domain','Status','Sent At','Opens','Clicks'].map(h => ( {['Site','Recipient','Subject','Type','Domain','Status','Sent At','Opens','Clicks'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))} ))}
@@ -66,14 +66,14 @@ export default function SendLogPage() {
) : logs.length === 0 ? ( ) : logs.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No send log entries</td></tr> <tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No send log entries</td></tr>
) : logs.map((l: any) => ( ) : logs.map((l: any) => (
<tr key={l.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={l.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-[#888] text-xs capitalize">{l.site}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{l.site}</td>
<td className="px-3 py-2 text-white text-xs">{l.recipient_email}</td> <td className="px-3 py-2 text-white text-xs">{l.recipient_email}</td>
<td className="px-3 py-2 text-[#ccc] text-xs max-w-xs truncate">{l.subject ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs max-w-xs truncate">{l.subject ?? '—'}</td>
<td className="px-3 py-2 text-[#555] text-xs">{l.send_type}</td> <td className="px-3 py-2 text-[#555] text-xs">{l.send_type}</td>
<td className="px-3 py-2 text-[#555] text-xs">{l.rmp_email_sending_domains?.domain ?? '—'}</td> <td className="px-3 py-2 text-[#555] text-xs">{l.rmp_email_sending_domains?.domain ?? '—'}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${l.status === 'sent' ? 'bg-green-400/10 text-green-400' : l.status === 'failed' ? 'bg-red-400/10 text-red-400' : l.status === 'suppressed' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{l.status}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${l.status === 'sent' ? 'bg-green-400/10 text-green-400' : l.status === 'failed' ? 'bg-red-400/10 text-red-400' : l.status === 'suppressed' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#FFFFFF] text-[#555]'}`}>{l.status}</span>
</td> </td>
<td className="px-3 py-2 text-[#555] text-xs">{l.sent_at ? new Date(l.sent_at).toLocaleString() : '—'}</td> <td className="px-3 py-2 text-[#555] text-xs">{l.sent_at ? new Date(l.sent_at).toLocaleString() : '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{l.opens}</td> <td className="px-3 py-2 text-[#888] text-xs">{l.opens}</td>

View File

@@ -72,7 +72,7 @@ export default function SuppressionsPage() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -84,29 +84,29 @@ export default function SuppressionsPage() {
<Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link> <Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Suppression</button> <button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500">+ Add Suppression</button>
</div> </div>
</div> </div>
{/* Site selector */} {/* Site selector */}
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
{SITES.map(s => ( {SITES.map(s => (
<button key={s} onClick={() => setSite(s)} className={`px-3 py-1 rounded text-xs capitalize ${site === s ? 'bg-[#3b82f6] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>{s}</button> <button key={s} onClick={() => setSite(s)} className={`px-3 py-1 rounded text-xs capitalize ${site === s ? 'bg-[#1D4ED8] text-white' : 'bg-[#111] border border-[#DCE6F2] text-[#888]'}`}>{s}</button>
))} ))}
</div> </div>
{/* Stats */} {/* Stats */}
{stats && ( {stats && (
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div className="bg-[#111] border border-[#252525] rounded-lg p-3"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-3">
<p className="text-xl font-bold text-red-400">{stats.total ?? 0}</p> <p className="text-xl font-bold text-red-400">{stats.total ?? 0}</p>
<p className="text-xs text-[#888] mt-0.5">Total Suppressed</p> <p className="text-xs text-[#888] mt-0.5">Total Suppressed</p>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-3"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-3">
<p className="text-xl font-bold text-yellow-400">{stats.added_this_month ?? 0}</p> <p className="text-xl font-bold text-yellow-400">{stats.added_this_month ?? 0}</p>
<p className="text-xs text-[#888] mt-0.5">Added This Month</p> <p className="text-xs text-[#888] mt-0.5">Added This Month</p>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-3"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-3">
<p className="text-xl font-bold text-orange-400">{stats.hostile_replies ?? 0}</p> <p className="text-xl font-bold text-orange-400">{stats.hostile_replies ?? 0}</p>
<p className="text-xs text-[#888] mt-0.5">Hostile Replies</p> <p className="text-xs text-[#888] mt-0.5">Hostile Replies</p>
</div> </div>
@@ -115,44 +115,44 @@ export default function SuppressionsPage() {
{/* Filters */} {/* Filters */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Types</option> <option value="">All Types</option>
{SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)} {SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select> </select>
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search email…" className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#3b82f6]" /> <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search email…" className="px-2 py-1 bg-[#111] border border-[#DCE6F2] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
{showAddForm && ( {showAddForm && (
<form onSubmit={handleAddSuppression} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleAddSuppression} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Add Manual Suppression</h3> <h3 className="text-sm font-semibold">Add Manual Suppression</h3>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Email *</label> <label className="text-xs text-[#888] block mb-1">Email *</label>
<input required type="email" value={addForm.email} onChange={e => setAddForm(p => ({ ...p, email: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input required type="email" value={addForm.email} onChange={e => setAddForm(p => ({ ...p, email: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Type</label> <label className="text-xs text-[#888] block mb-1">Type</label>
<select value={addForm.suppression_type} onChange={e => setAddForm(p => ({ ...p, suppression_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={addForm.suppression_type} onChange={e => setAddForm(p => ({ ...p, suppression_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
{SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)} {SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select> </select>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Notes</label> <label className="text-xs text-[#888] block mb-1">Notes</label>
<input value={addForm.notes} onChange={e => setAddForm(p => ({ ...p, notes: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input value={addForm.notes} onChange={e => setAddForm(p => ({ ...p, notes: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add'}</button>
<button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
{/* Suppressions table */} {/* Suppressions table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
{['Email','Type','Source','Reply Text','Domain','Date',''].map(h => ( {['Email','Type','Source','Reply Text','Domain','Date',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -164,10 +164,10 @@ export default function SuppressionsPage() {
) : suppressions.length === 0 ? ( ) : suppressions.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No suppressions found</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No suppressions found</td></tr>
) : suppressions.map((s: any) => ( ) : suppressions.map((s: any) => (
<tr key={s.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={s.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-3 py-2 text-white text-xs">{s.email}</td> <td className="px-3 py-2 text-white text-xs">{s.email}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${s.suppression_type === 'hostile_reply' ? 'bg-red-400/10 text-red-400' : s.suppression_type === 'unsubscribe' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{s.suppression_type?.replace(/_/g, ' ')}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${s.suppression_type === 'hostile_reply' ? 'bg-red-400/10 text-red-400' : s.suppression_type === 'unsubscribe' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#FFFFFF] text-[#555]'}`}>{s.suppression_type?.replace(/_/g, ' ')}</span>
</td> </td>
<td className="px-3 py-2 text-[#555] text-xs">{s.source}</td> <td className="px-3 py-2 text-[#555] text-xs">{s.source}</td>
<td className="px-3 py-2 text-[#555] text-xs max-w-xs truncate">{s.reply_text ?? '—'}</td> <td className="px-3 py-2 text-[#555] text-xs max-w-xs truncate">{s.reply_text ?? '—'}</td>
@@ -183,40 +183,40 @@ export default function SuppressionsPage() {
</div> </div>
{/* Keyword Management */} {/* Keyword Management */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525] flex items-center justify-between"> <div className="px-4 py-3 border-b border-[#DCE6F2] flex items-center justify-between">
<h2 className="text-sm font-semibold">Suppression Keywords</h2> <h2 className="text-sm font-semibold">Suppression Keywords</h2>
<button onClick={() => setShowKeywordForm(true)} className="text-xs text-[#3b82f6] hover:underline">+ Add Keyword</button> <button onClick={() => setShowKeywordForm(true)} className="text-xs text-[#1D4ED8] hover:underline">+ Add Keyword</button>
</div> </div>
{showKeywordForm && ( {showKeywordForm && (
<form onSubmit={handleAddKeyword} className="px-4 py-3 border-b border-[#1a1a1a] flex gap-2 items-end"> <form onSubmit={handleAddKeyword} className="px-4 py-3 border-b border-[#FFFFFF] flex gap-2 items-end">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Keyword *</label> <label className="text-xs text-[#888] block mb-1">Keyword *</label>
<input required value={keywordForm.keyword} onChange={e => setKeywordForm(p => ({ ...p, keyword: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" /> <input required value={keywordForm.keyword} onChange={e => setKeywordForm(p => ({ ...p, keyword: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none focus:border-[#1D4ED8]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Type</label> <label className="text-xs text-[#888] block mb-1">Type</label>
<select value={keywordForm.keyword_type} onChange={e => setKeywordForm(p => ({ ...p, keyword_type: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={keywordForm.keyword_type} onChange={e => setKeywordForm(p => ({ ...p, keyword_type: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#DCE6F2] rounded text-xs text-white focus:outline-none">
<option value="unsubscribe">unsubscribe</option> <option value="unsubscribe">unsubscribe</option>
<option value="hostile">hostile</option> <option value="hostile">hostile</option>
</select> </select>
</div> </div>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Add</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#1D4ED8] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Add</button>
<button type="button" onClick={() => setShowKeywordForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888]">Cancel</button> <button type="button" onClick={() => setShowKeywordForm(false)} className="px-3 py-1.5 bg-[#FFFFFF] border border-[#DCE6F2] rounded text-xs text-[#888]">Cancel</button>
</form> </form>
)} )}
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Keyword','Type','Active',''].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead> <thead><tr className="border-b border-[#FFFFFF]">{['Keyword','Type','Active',''].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody> <tbody>
{keywords.map((k: any) => ( {keywords.map((k: any) => (
<tr key={k.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={k.id} className="border-b border-[#FFFFFF] hover:bg-[#FFFFFF]">
<td className="px-4 py-2 text-white text-xs">{k.keyword}</td> <td className="px-4 py-2 text-white text-xs">{k.keyword}</td>
<td className="px-4 py-2 text-[#888] text-xs">{k.keyword_type}</td> <td className="px-4 py-2 text-[#888] text-xs">{k.keyword_type}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${k.active ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{k.active ? 'Active' : 'Inactive'}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${k.active ? 'bg-green-400/10 text-green-400' : 'bg-[#FFFFFF] text-[#555]'}`}>{k.active ? 'Active' : 'Inactive'}</span>
</td> </td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<button onClick={() => toggleKeyword(k.id, k.active)} className="text-xs text-[#3b82f6] hover:underline">{k.active ? 'Deactivate' : 'Activate'}</button> <button onClick={() => toggleKeyword(k.id, k.active)} className="text-xs text-[#1D4ED8] hover:underline">{k.active ? 'Deactivate' : 'Activate'}</button>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -91,7 +91,7 @@ interface StatCardProps {
function StatCard({ label, value, sub, accentColor, bgColor, icon, badge }: StatCardProps) { function StatCard({ label, value, sub, accentColor, bgColor, icon, badge }: StatCardProps) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${bgColor}`}> <div className={`w-10 h-10 rounded-lg flex items-center justify-center ${bgColor}`}>
<span className={accentColor}>{icon}</span> <span className={accentColor}>{icon}</span>
@@ -116,7 +116,7 @@ function StatCard({ label, value, sub, accentColor, bgColor, icon, badge }: Stat
function ChartTooltip({ active, payload, label }: any) { function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null; if (!active || !payload?.length) return null;
return ( return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl"> <div className="bg-[#FFFFFF] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1">{label}</p>} {label && <p className="text-[#888] mb-1">{label}</p>}
{payload.map((entry: any, i: number) => ( {payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5"> <p key={i} style={{ color: entry.color }} className="leading-5">
@@ -167,7 +167,7 @@ const EMPTY_STATS: ForumStats = {
}, },
}; };
const CATEGORY_COLORS = ['#3b82f6', '#f59e0b', '#10b981', '#8b5cf6', '#ef4444', '#06b6d4', '#f97316', '#84cc16']; const CATEGORY_COLORS = ['#1D4ED8', '#f59e0b', '#10b981', '#8b5cf6', '#ef4444', '#06b6d4', '#f97316', '#84cc16'];
export default function ForumDashboardPage() { export default function ForumDashboardPage() {
const { user, loading } = useAuth(); const { user, loading } = useAuth();
@@ -204,7 +204,7 @@ export default function ForumDashboardPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading forum dashboard</p> <p className="text-[#555] text-sm font-body">Loading forum dashboard</p>
</div> </div>
</div> </div>
@@ -236,7 +236,7 @@ export default function ForumDashboardPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
onClick={fetchStats} onClick={fetchStats}
className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#252525] rounded-lg text-xs text-[#888] hover:text-white hover:border-[#333] transition-all font-body" className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#DCE6F2] rounded-lg text-xs text-[#888] hover:text-white hover:border-[#333] transition-all font-body"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
@@ -245,7 +245,7 @@ export default function ForumDashboardPage() {
</button> </button>
<Link <Link
href="/admin/forum-moderation" href="/admin/forum-moderation"
className="flex items-center gap-2 px-3 py-2 bg-[#3b82f6] rounded-lg text-xs text-white hover:bg-blue-500 transition-colors font-body font-semibold" className="flex items-center gap-2 px-3 py-2 bg-[#1D4ED8] rounded-lg text-xs text-white hover:bg-blue-500 transition-colors font-body font-semibold"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
@@ -386,7 +386,7 @@ export default function ForumDashboardPage() {
{/* Thread Growth Chart */} {/* Thread Growth Chart */}
<section className="mb-8"> <section className="mb-8">
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-6">
<SectionHeader <SectionHeader
title="Thread Growth" title="Thread Growth"
subtitle="Weekly new threads and replies over the past 8 weeks" subtitle="Weekly new threads and replies over the past 8 weeks"
@@ -398,7 +398,7 @@ export default function ForumDashboardPage() {
) : ( ) : (
<ResponsiveContainer width="100%" height={240}> <ResponsiveContainer width="100%" height={240}>
<LineChart data={weeklyGrowth} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}> <LineChart data={weeklyGrowth} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1a1a1a" /> <CartesianGrid strokeDasharray="3 3" stroke="#FFFFFF" />
<XAxis dataKey="week" tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} /> <XAxis dataKey="week" tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} /> <YAxis tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} />
<Tooltip content={<ChartTooltip />} /> <Tooltip content={<ChartTooltip />} />
@@ -407,7 +407,7 @@ export default function ForumDashboardPage() {
iconType="circle" iconType="circle"
iconSize={8} iconSize={8}
/> />
<Line type="monotone" dataKey="threads" stroke="#3b82f6" strokeWidth={2} dot={{ fill: '#3b82f6', r: 3 }} name="New Threads" /> <Line type="monotone" dataKey="threads" stroke="#1D4ED8" strokeWidth={2} dot={{ fill: '#1D4ED8', r: 3 }} name="New Threads" />
<Line type="monotone" dataKey="replies" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 3 }} name="New Replies" /> <Line type="monotone" dataKey="replies" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 3 }} name="New Replies" />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
@@ -419,33 +419,33 @@ export default function ForumDashboardPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Top Categories */} {/* Top Categories */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-5 py-4 border-b border-[#FFFFFF] flex items-center justify-between">
<div> <div>
<h2 className="text-sm font-display font-bold text-white">Top Categories</h2> <h2 className="text-sm font-display font-bold text-white">Top Categories</h2>
<p className="text-xs text-[#555] font-body mt-0.5">By thread count</p> <p className="text-xs text-[#555] font-body mt-0.5">By thread count</p>
</div> </div>
<Link href="/forum" className="text-xs text-[#3b82f6] hover:text-blue-300 font-body transition-colors">View forum </Link> <Link href="/forum" className="text-xs text-[#1D4ED8] hover:text-blue-300 font-body transition-colors">View forum </Link>
</div> </div>
{topCategories.length === 0 ? ( {topCategories.length === 0 ? (
<div className="px-5 py-10 text-center"> <div className="px-5 py-10 text-center">
<p className="text-[#555] text-sm font-body">No categories found</p> <p className="text-[#555] text-sm font-body">No categories found</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#FFFFFF]">
{topCategories.map((cat, idx) => { {topCategories.map((cat, idx) => {
const maxThreads = topCategories[0]?.thread_count ?? 1; const maxThreads = topCategories[0]?.thread_count ?? 1;
const pct = maxThreads > 0 ? Math.round((cat.thread_count / maxThreads) * 100) : 0; const pct = maxThreads > 0 ? Math.round((cat.thread_count / maxThreads) * 100) : 0;
const color = CATEGORY_COLORS[idx % CATEGORY_COLORS.length]; const color = CATEGORY_COLORS[idx % CATEGORY_COLORS.length];
return ( return (
<div key={cat.id} className="px-5 py-3 hover:bg-[#0d0d0d] transition-colors"> <div key={cat.id} className="px-5 py-3 hover:bg-[#F8FAFC] transition-colors">
<div className="flex items-center gap-3 mb-1.5"> <div className="flex items-center gap-3 mb-1.5">
<span className="text-base leading-none">{cat.icon}</span> <span className="text-base leading-none">{cat.icon}</span>
<span className="text-sm text-white font-body flex-1 truncate">{cat.name}</span> <span className="text-sm text-white font-body flex-1 truncate">{cat.name}</span>
<span className="text-xs text-[#888] font-body font-semibold">{cat.thread_count} threads</span> <span className="text-xs text-[#888] font-body font-semibold">{cat.thread_count} threads</span>
<span className="text-xs text-[#555] font-body">{cat.post_count} posts</span> <span className="text-xs text-[#555] font-body">{cat.post_count} posts</span>
</div> </div>
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden ml-7"> <div className="h-1.5 bg-[#FFFFFF] rounded-full overflow-hidden ml-7">
<div <div
className="h-full rounded-full transition-all duration-500" className="h-full rounded-full transition-all duration-500"
style={{ width: `${pct}%`, backgroundColor: color }} style={{ width: `${pct}%`, backgroundColor: color }}
@@ -459,8 +459,8 @@ export default function ForumDashboardPage() {
</div> </div>
{/* Active Users */} {/* Active Users */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a]"> <div className="px-5 py-4 border-b border-[#FFFFFF]">
<h2 className="text-sm font-display font-bold text-white">Active Users</h2> <h2 className="text-sm font-display font-bold text-white">Active Users</h2>
<p className="text-xs text-[#555] font-body mt-0.5">Most active in the last 30 days</p> <p className="text-xs text-[#555] font-body mt-0.5">Most active in the last 30 days</p>
</div> </div>
@@ -469,13 +469,13 @@ export default function ForumDashboardPage() {
<p className="text-[#555] text-sm font-body">No active users found</p> <p className="text-[#555] text-sm font-body">No active users found</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#FFFFFF]">
{activeUsers.map((u, idx) => ( {activeUsers.map((u, idx) => (
<div key={u.author_id} className="px-5 py-3 flex items-center gap-3 hover:bg-[#0d0d0d] transition-colors"> <div key={u.author_id} className="px-5 py-3 flex items-center gap-3 hover:bg-[#F8FAFC] transition-colors">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 text-xs font-bold font-body text-[#555]"> <div className="w-6 h-6 rounded-full bg-[#FFFFFF] flex items-center justify-center flex-shrink-0 text-xs font-bold font-body text-[#555]">
{idx + 1} {idx + 1}
</div> </div>
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#3b82f6] to-[#8b5cf6] flex items-center justify-center flex-shrink-0"> <div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#1D4ED8] to-[#8b5cf6] flex items-center justify-center flex-shrink-0">
<span className="text-xs font-bold text-white font-body"> <span className="text-xs font-bold text-white font-body">
{u.author_name.charAt(0).toUpperCase()} {u.author_name.charAt(0).toUpperCase()}
</span> </span>
@@ -498,14 +498,14 @@ export default function ForumDashboardPage() {
{/* Category Bar Chart */} {/* Category Bar Chart */}
{topCategories.length > 0 && ( {topCategories.length > 0 && (
<section className="mb-8"> <section className="mb-8">
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-6">
<SectionHeader <SectionHeader
title="Category Activity" title="Category Activity"
subtitle="Thread and post distribution across categories" subtitle="Thread and post distribution across categories"
/> />
<ResponsiveContainer width="100%" height={220}> <ResponsiveContainer width="100%" height={220}>
<BarChart data={topCategories} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}> <BarChart data={topCategories} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1a1a1a" /> <CartesianGrid strokeDasharray="3 3" stroke="#FFFFFF" />
<XAxis <XAxis
dataKey="name" dataKey="name"
tick={{ fill: '#555', fontSize: 10 }} tick={{ fill: '#555', fontSize: 10 }}
@@ -523,7 +523,7 @@ export default function ForumDashboardPage() {
iconType="circle" iconType="circle"
iconSize={8} iconSize={8}
/> />
<Bar dataKey="thread_count" fill="#3b82f6" radius={[3, 3, 0, 0]} name="Threads" /> <Bar dataKey="thread_count" fill="#1D4ED8" radius={[3, 3, 0, 0]} name="Threads" />
<Bar dataKey="post_count" fill="#10b981" radius={[3, 3, 0, 0]} name="Posts" /> <Bar dataKey="post_count" fill="#10b981" radius={[3, 3, 0, 0]} name="Posts" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
@@ -533,22 +533,22 @@ export default function ForumDashboardPage() {
{/* Moderation Stats */} {/* Moderation Stats */}
<section className="mb-8"> <section className="mb-8">
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-5 py-4 border-b border-[#FFFFFF] flex items-center justify-between">
<div> <div>
<h2 className="text-sm font-display font-bold text-white">Moderation Stats</h2> <h2 className="text-sm font-display font-bold text-white">Moderation Stats</h2>
<p className="text-xs text-[#555] font-body mt-0.5">Current moderation state across all forum content</p> <p className="text-xs text-[#555] font-body mt-0.5">Current moderation state across all forum content</p>
</div> </div>
<Link href="/admin/forum-moderation" className="text-xs text-[#3b82f6] hover:text-blue-300 font-body transition-colors">Open moderation </Link> <Link href="/admin/forum-moderation" className="text-xs text-[#1D4ED8] hover:text-blue-300 font-body transition-colors">Open moderation </Link>
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 divide-x divide-y sm:divide-y-0 divide-[#1a1a1a]"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 divide-x divide-y sm:divide-y-0 divide-[#FFFFFF]">
{[ {[
{ label: 'Flagged Threads', value: moderation.flaggedThreads, color: 'text-red-400', bg: 'bg-red-500/10' }, { label: 'Flagged Threads', value: moderation.flaggedThreads, color: 'text-red-400', bg: 'bg-red-500/10' },
{ label: 'Flagged Replies', value: moderation.flaggedReplies, color: 'text-red-400', bg: 'bg-red-500/10' }, { label: 'Flagged Replies', value: moderation.flaggedReplies, color: 'text-red-400', bg: 'bg-red-500/10' },
{ label: 'Locked Threads', value: moderation.lockedThreads, color: 'text-orange-400', bg: 'bg-orange-500/10' }, { label: 'Locked Threads', value: moderation.lockedThreads, color: 'text-orange-400', bg: 'bg-orange-500/10' },
{ label: 'Pinned Threads', value: moderation.pinnedThreads, color: 'text-yellow-400', bg: 'bg-yellow-500/10' }, { label: 'Pinned Threads', value: moderation.pinnedThreads, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
{ label: 'Featured Threads', value: moderation.featuredThreads, color: 'text-blue-400', bg: 'bg-blue-500/10' }, { label: 'Featured Threads', value: moderation.featuredThreads, color: 'text-blue-400', bg: 'bg-blue-500/10' },
{ label: 'Hidden Replies', value: moderation.hiddenReplies, color: 'text-[#888]', bg: 'bg-[#1a1a1a]' }, { label: 'Hidden Replies', value: moderation.hiddenReplies, color: 'text-[#888]', bg: 'bg-[#FFFFFF]' },
].map(({ label, value, color, bg }) => ( ].map(({ label, value, color, bg }) => (
<div key={label} className="px-4 py-5 flex flex-col items-center gap-1 text-center"> <div key={label} className="px-4 py-5 flex flex-col items-center gap-1 text-center">
<p className={`text-2xl font-bold font-display ${color}`}>{value}</p> <p className={`text-2xl font-bold font-display ${color}`}>{value}</p>

View File

@@ -62,7 +62,7 @@ function FlagModal({ onConfirm, onCancel }: FlagModalProps) {
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
return ( return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 px-4"> <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 px-4">
<div className="bg-[#1a1a1a] border border-[#333] rounded-xl p-6 w-full max-w-md"> <div className="bg-[#FFFFFF] border border-[#333] rounded-xl p-6 w-full max-w-md">
<h3 className="text-white font-heading font-bold text-lg mb-2">Flag Content</h3> <h3 className="text-white font-heading font-bold text-lg mb-2">Flag Content</h3>
<p className="text-[#888] font-body text-sm mb-4">Provide a reason for flagging this content (optional).</p> <p className="text-[#888] font-body text-sm mb-4">Provide a reason for flagging this content (optional).</p>
<textarea <textarea
@@ -70,10 +70,10 @@ function FlagModal({ onConfirm, onCancel }: FlagModalProps) {
onChange={e => setReason(e.target.value)} onChange={e => setReason(e.target.value)}
rows={3} rows={3}
placeholder="e.g. Spam, harassment, off-topic..." placeholder="e.g. Spam, harassment, off-topic..."
className="w-full bg-[#0d0d0d] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ef4444] resize-none mb-4" className="w-full bg-[#F8FAFC] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ef4444] resize-none mb-4"
/> />
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<button onClick={onCancel} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white bg-[#252525] hover:bg-[#333] rounded-lg transition-colors"> <button onClick={onCancel} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white bg-[#DCE6F2] hover:bg-[#333] rounded-lg transition-colors">
Cancel Cancel
</button> </button>
<button onClick={() => onConfirm(reason)} className="px-4 py-2 text-sm font-body font-semibold text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors"> <button onClick={() => onConfirm(reason)} className="px-4 py-2 text-sm font-body font-semibold text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors">
@@ -119,14 +119,14 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onCancel={() => setShowFlagModal(false)} onCancel={() => setShowFlagModal(false)}
/> />
)} )}
<div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : thread.is_flagged ? 'border-red-500/40' : 'border-[#252525]'}`}> <div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : thread.is_flagged ? 'border-red-500/40' : 'border-[#DCE6F2]'}`}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{/* Title + badges */} {/* Title + badges */}
<div className="flex flex-wrap items-center gap-2 mb-1"> <div className="flex flex-wrap items-center gap-2 mb-1">
<Link <Link
href={`/forum/thread/${thread.id}`} href={`/forum/thread/${thread.id}`}
className="text-white font-body font-semibold text-sm hover:text-[#3b82f6] transition-colors truncate max-w-xs" className="text-white font-body font-semibold text-sm hover:text-[#1D4ED8] transition-colors truncate max-w-xs"
target="_blank" target="_blank"
> >
{thread.title} {thread.title}
@@ -159,7 +159,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onClick={() => onAction(thread.id, thread.is_pinned ? 'unpin' : 'pin')} onClick={() => onAction(thread.id, thread.is_pinned ? 'unpin' : 'pin')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${ className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
thread.is_pinned thread.is_pinned
? 'bg-amber-400/20 text-amber-400 hover:bg-amber-400/30' :'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-amber-400' ? 'bg-amber-400/20 text-amber-400 hover:bg-amber-400/30' :'bg-[#FFFFFF] text-[#888] hover:bg-[#DCE6F2] hover:text-amber-400'
}`} }`}
> >
{thread.is_pinned ? 'Unpin' : 'Pin'} {thread.is_pinned ? 'Unpin' : 'Pin'}
@@ -169,7 +169,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onClick={() => onAction(thread.id, thread.is_featured ? 'unfeature' : 'feature')} onClick={() => onAction(thread.id, thread.is_featured ? 'unfeature' : 'feature')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${ className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
thread.is_featured thread.is_featured
? 'bg-yellow-400/20 text-yellow-400 hover:bg-yellow-400/30' :'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-yellow-400' ? 'bg-yellow-400/20 text-yellow-400 hover:bg-yellow-400/30' :'bg-[#FFFFFF] text-[#888] hover:bg-[#DCE6F2] hover:text-yellow-400'
}`} }`}
> >
{thread.is_featured ? 'Unfeature' : 'Feature'} {thread.is_featured ? 'Unfeature' : 'Feature'}
@@ -179,7 +179,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onClick={() => onAction(thread.id, thread.is_locked ? 'unlock' : 'lock')} onClick={() => onAction(thread.id, thread.is_locked ? 'unlock' : 'lock')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${ className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
thread.is_locked thread.is_locked
? 'bg-orange-400/20 text-orange-400 hover:bg-orange-400/30' :'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-orange-400' ? 'bg-orange-400/20 text-orange-400 hover:bg-orange-400/30' :'bg-[#FFFFFF] text-[#888] hover:bg-[#DCE6F2] hover:text-orange-400'
}`} }`}
> >
{thread.is_locked ? 'Unlock' : 'Lock'} {thread.is_locked ? 'Unlock' : 'Lock'}
@@ -199,7 +199,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
<button <button
disabled={isLoading} disabled={isLoading}
onClick={() => setShowFlagModal(true)} onClick={() => setShowFlagModal(true)}
className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-red-400 transition-colors disabled:opacity-50" className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#FFFFFF] text-[#888] hover:bg-[#DCE6F2] hover:text-red-400 transition-colors disabled:opacity-50"
> >
Flag Flag
</button> </button>
@@ -235,7 +235,7 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
onCancel={() => setShowFlagModal(false)} onCancel={() => setShowFlagModal(false)}
/> />
)} )}
<div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : reply.is_flagged ? 'border-red-500/40' : reply.is_hidden ? 'border-[#333] opacity-60' : 'border-[#252525]'}`}> <div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : reply.is_flagged ? 'border-red-500/40' : reply.is_hidden ? 'border-[#333] opacity-60' : 'border-[#DCE6F2]'}`}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{/* Thread link + badges */} {/* Thread link + badges */}
@@ -243,13 +243,13 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
{reply.forum_threads && ( {reply.forum_threads && (
<Link <Link
href={`/forum/thread/${reply.forum_threads.id}`} href={`/forum/thread/${reply.forum_threads.id}`}
className="text-[#3b82f6] font-body text-xs hover:underline truncate max-w-xs" className="text-[#1D4ED8] font-body text-xs hover:underline truncate max-w-xs"
target="_blank" target="_blank"
> >
{reply.forum_threads.title} {reply.forum_threads.title}
</Link> </Link>
)} )}
{reply.is_hidden && <StatusBadge label="👁 Hidden" color="text-[#888] bg-[#252525]" />} {reply.is_hidden && <StatusBadge label="👁 Hidden" color="text-[#888] bg-[#DCE6F2]" />}
{isAiFlagged && <StatusBadge label="🤖 AI Flagged" color="text-purple-400 bg-purple-400/10" />} {isAiFlagged && <StatusBadge label="🤖 AI Flagged" color="text-purple-400 bg-purple-400/10" />}
{reply.is_flagged && !isAiFlagged && <StatusBadge label="🚩 Flagged" color="text-red-400 bg-red-400/10" />} {reply.is_flagged && !isAiFlagged && <StatusBadge label="🚩 Flagged" color="text-red-400 bg-red-400/10" />}
</div> </div>
@@ -273,8 +273,8 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
onClick={() => onAction(reply.id, reply.is_hidden ? 'unhide' : 'hide')} onClick={() => onAction(reply.id, reply.is_hidden ? 'unhide' : 'hide')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${ className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
reply.is_hidden reply.is_hidden
? 'bg-[#252525] text-[#aaa] hover:bg-[#333]' ? 'bg-[#DCE6F2] text-[#aaa] hover:bg-[#333]'
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white' : 'bg-[#FFFFFF] text-[#888] hover:bg-[#DCE6F2] hover:text-white'
}`} }`}
> >
{reply.is_hidden ? 'Unhide' : 'Hide'} {reply.is_hidden ? 'Unhide' : 'Hide'}
@@ -294,7 +294,7 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
<button <button
disabled={isLoading} disabled={isLoading}
onClick={() => setShowFlagModal(true)} onClick={() => setShowFlagModal(true)}
className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-red-400 transition-colors disabled:opacity-50" className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#FFFFFF] text-[#888] hover:bg-[#DCE6F2] hover:text-red-400 transition-colors disabled:opacity-50"
> >
Flag Flag
</button> </button>
@@ -398,7 +398,7 @@ export default function ForumModerationPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -435,10 +435,10 @@ export default function ForumModerationPage() {
)} )}
{/* Page Header */} {/* Page Header */}
<div className="bg-[#1a2535] border-b border-[#2a3a50]"> <div className="bg-[#FFFFFF] border-b border-[#2a3a50]">
<div className="max-w-7xl mx-auto px-4 py-6"> <div className="max-w-7xl mx-auto px-4 py-6">
<nav className="text-sm font-body text-[#666] mb-2"> <nav className="text-sm font-body text-[#666] mb-2">
<Link href="/admin" className="hover:text-[#3b82f6] transition-colors">Admin</Link> <Link href="/admin" className="hover:text-[#1D4ED8] transition-colors">Admin</Link>
<span className="mx-2"></span> <span className="mx-2"></span>
<span className="text-[#aab4c4]">Forum Moderation</span> <span className="text-[#aab4c4]">Forum Moderation</span>
</nav> </nav>
@@ -453,7 +453,7 @@ export default function ForumModerationPage() {
<span className="text-xs text-purple-400 font-body bg-purple-400/10 border border-purple-400/20 px-3 py-1.5 rounded-lg"> <span className="text-xs text-purple-400 font-body bg-purple-400/10 border border-purple-400/20 px-3 py-1.5 rounded-lg">
🤖 Claude AI Auto-Moderation Active 🤖 Claude AI Auto-Moderation Active
</span> </span>
<span className="text-xs text-[#555] font-body bg-[#1a1a1a] border border-[#252525] px-3 py-1.5 rounded-lg"> <span className="text-xs text-[#555] font-body bg-[#FFFFFF] border border-[#DCE6F2] px-3 py-1.5 rounded-lg">
🛡 Moderator View 🛡 Moderator View
</span> </span>
</div> </div>
@@ -463,14 +463,14 @@ export default function ForumModerationPage() {
<div className="max-w-7xl mx-auto px-4 py-6"> <div className="max-w-7xl mx-auto px-4 py-6">
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit mb-6"> <div className="flex gap-1 bg-[#111] border border-[#DCE6F2] rounded-lg p-1 w-fit mb-6">
{(['threads', 'replies'] as Tab[]).map(tab => ( {(['threads', 'replies'] as Tab[]).map(tab => (
<button <button
key={tab} key={tab}
onClick={() => { setActiveTab(tab); setPage(1); }} onClick={() => { setActiveTab(tab); setPage(1); }}
className={`px-5 py-2 text-sm font-body font-semibold rounded-md transition-colors capitalize ${ className={`px-5 py-2 text-sm font-body font-semibold rounded-md transition-colors capitalize ${
activeTab === tab activeTab === tab
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'text-[#888] hover:text-white' : 'text-[#888] hover:text-white'
}`} }`}
> >
@@ -488,8 +488,8 @@ export default function ForumModerationPage() {
onClick={() => { setThreadFilter(f.value); setPage(1); }} onClick={() => { setThreadFilter(f.value); setPage(1); }}
className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${ className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${
threadFilter === f.value threadFilter === f.value
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]' : 'bg-[#FFFFFF] text-[#888] hover:bg-[#DCE6F2] hover:text-white border border-[#DCE6F2]'
}`} }`}
> >
{f.label} {f.label}
@@ -501,8 +501,8 @@ export default function ForumModerationPage() {
onClick={() => { setReplyFilter(f.value); setPage(1); }} onClick={() => { setReplyFilter(f.value); setPage(1); }}
className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${ className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${
replyFilter === f.value replyFilter === f.value
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]' : 'bg-[#FFFFFF] text-[#888] hover:bg-[#DCE6F2] hover:text-white border border-[#DCE6F2]'
}`} }`}
> >
{f.label} {f.label}
@@ -515,7 +515,7 @@ export default function ForumModerationPage() {
{/* Content */} {/* Content */}
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<div className="w-7 h-7 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-7 h-7 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : activeTab === 'threads' ? ( ) : activeTab === 'threads' ? (
<> <>
@@ -559,7 +559,7 @@ export default function ForumModerationPage() {
<button <button
disabled={page === 1} disabled={page === 1}
onClick={() => setPage(p => p - 1)} onClick={() => setPage(p => p - 1)}
className="px-4 py-2 text-sm font-body text-[#888] bg-[#1a1a1a] border border-[#252525] rounded-lg hover:bg-[#252525] disabled:opacity-40 transition-colors" className="px-4 py-2 text-sm font-body text-[#888] bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg hover:bg-[#DCE6F2] disabled:opacity-40 transition-colors"
> >
Prev Prev
</button> </button>
@@ -567,7 +567,7 @@ export default function ForumModerationPage() {
<button <button
disabled={page === totalPages} disabled={page === totalPages}
onClick={() => setPage(p => p + 1)} onClick={() => setPage(p => p + 1)}
className="px-4 py-2 text-sm font-body text-[#888] bg-[#1a1a1a] border border-[#252525] rounded-lg hover:bg-[#252525] disabled:opacity-40 transition-colors" className="px-4 py-2 text-sm font-body text-[#888] bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg hover:bg-[#DCE6F2] disabled:opacity-40 transition-colors"
> >
Next Next
</button> </button>

View File

@@ -136,13 +136,13 @@ export default function AdminForumSeedPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#F8FAFC]">
<div className="bg-[#1a2535] border-b border-[#2a3a50]"> <div className="bg-[#FFFFFF] border-b border-[#2a3a50]">
<div className="max-w-container mx-auto px-4 py-6"> <div className="max-w-container mx-auto px-4 py-6">
<nav className="text-sm font-body text-[#666] mb-2"> <nav className="text-sm font-body text-[#666] mb-2">
<Link href="/admin" className="hover:text-[#3b82f6] transition-colors">Admin</Link> <Link href="/admin" className="hover:text-[#1D4ED8] transition-colors">Admin</Link>
<span className="mx-2"></span> <span className="mx-2"></span>
<Link href="/forum" className="hover:text-[#3b82f6] transition-colors">Forum</Link> <Link href="/forum" className="hover:text-[#1D4ED8] transition-colors">Forum</Link>
<span className="mx-2"></span> <span className="mx-2"></span>
<span className="text-[#aab4c4]">Seed Tool</span> <span className="text-[#aab4c4]">Seed Tool</span>
</nav> </nav>
@@ -157,17 +157,17 @@ export default function AdminForumSeedPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Editor */} {/* Left: Editor */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="flex border-b border-[#252525]"> <div className="flex border-b border-[#DCE6F2]">
<button <button
onClick={() => setActiveTab('import')} onClick={() => setActiveTab('import')}
className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'import' ? 'text-white border-b-2 border-[#3b82f6]' : 'text-[#666] hover:text-[#aaa]'}`} className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'import' ? 'text-white border-b-2 border-[#1D4ED8]' : 'text-[#666] hover:text-[#aaa]'}`}
> >
Import Editor Import Editor
</button> </button>
<button <button
onClick={() => setActiveTab('json')} onClick={() => setActiveTab('json')}
className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'json' ? 'text-white border-b-2 border-[#3b82f6]' : 'text-[#666] hover:text-[#aaa]'}`} className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'json' ? 'text-white border-b-2 border-[#1D4ED8]' : 'text-[#666] hover:text-[#aaa]'}`}
> >
JSON Schema JSON Schema
</button> </button>
@@ -182,20 +182,20 @@ export default function AdminForumSeedPage() {
value={seedData} value={seedData}
onChange={e => setSeedData(e.target.value)} onChange={e => setSeedData(e.target.value)}
rows={24} rows={24}
className="w-full bg-[#0d0d0d] border border-[#333] rounded-lg px-3 py-3 text-[#d0d0d0] font-mono text-xs focus:outline-none focus:border-[#3b82f6] resize-none" className="w-full bg-[#F8FAFC] border border-[#333] rounded-lg px-3 py-3 text-[#d0d0d0] font-mono text-xs focus:outline-none focus:border-[#1D4ED8] resize-none"
spellCheck={false} spellCheck={false}
/> />
<div className="flex gap-3 mt-3"> <div className="flex gap-3 mt-3">
<button <button
onClick={handleImport} onClick={handleImport}
disabled={importing} disabled={importing}
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors" className="bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
> >
{importing ? 'Importing...' : 'Import Threads'} {importing ? 'Importing...' : 'Import Threads'}
</button> </button>
<button <button
onClick={() => setSeedData(JSON.stringify(SEED_TEMPLATES, null, 2))} onClick={() => setSeedData(JSON.stringify(SEED_TEMPLATES, null, 2))}
className="bg-[#252525] hover:bg-[#333] text-[#aaa] font-body text-sm px-4 py-2 rounded-lg transition-colors" className="bg-[#DCE6F2] hover:bg-[#333] text-[#aaa] font-body text-sm px-4 py-2 rounded-lg transition-colors"
> >
Reset to Templates Reset to Templates
</button> </button>
@@ -206,7 +206,7 @@ export default function AdminForumSeedPage() {
{activeTab === 'json' && ( {activeTab === 'json' && (
<div className="p-4"> <div className="p-4">
<p className="text-[#888] font-body text-xs mb-3">Required JSON structure for seed data:</p> <p className="text-[#888] font-body text-xs mb-3">Required JSON structure for seed data:</p>
<pre className="bg-[#0d0d0d] border border-[#333] rounded-lg p-4 text-[#d0d0d0] font-mono text-xs overflow-auto"> <pre className="bg-[#F8FAFC] border border-[#333] rounded-lg p-4 text-[#d0d0d0] font-mono text-xs overflow-auto">
{`[ {`[
{ {
"category_slug": "live-production", "category_slug": "live-production",
@@ -226,7 +226,7 @@ export default function AdminForumSeedPage() {
<h4 className="text-white font-body font-semibold text-sm mb-2">Available Category Slugs</h4> <h4 className="text-white font-body font-semibold text-sm mb-2">Available Category Slugs</h4>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{['live-production','ip-cloud','audio','cameras','storage-mam','streaming','ai-automation','post-production','nab-ibc','career-jobs','gear-reviews','general'].map(slug => ( {['live-production','ip-cloud','audio','cameras','storage-mam','streaming','ai-automation','post-production','nab-ibc','career-jobs','gear-reviews','general'].map(slug => (
<code key={slug} className="bg-[#0d0d0d] border border-[#333] text-[#3b82f6] font-mono text-xs px-2 py-1 rounded">{slug}</code> <code key={slug} className="bg-[#F8FAFC] border border-[#333] text-[#1D4ED8] font-mono text-xs px-2 py-1 rounded">{slug}</code>
))} ))}
</div> </div>
</div> </div>
@@ -239,7 +239,7 @@ export default function AdminForumSeedPage() {
<div className="space-y-4"> <div className="space-y-4">
{/* Import Results */} {/* Import Results */}
{results.length > 0 && ( {results.length > 0 && (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-4"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-3">Import Results</h3> <h3 className="text-white font-heading font-semibold mb-3">Import Results</h3>
<div className="flex gap-4 mb-3"> <div className="flex gap-4 mb-3">
<div className="text-center"> <div className="text-center">
@@ -265,20 +265,20 @@ export default function AdminForumSeedPage() {
)} )}
{/* Quick Links */} {/* Quick Links */}
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-4"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-3">Quick Links</h3> <h3 className="text-white font-heading font-semibold mb-3">Quick Links</h3>
<div className="space-y-2"> <div className="space-y-2">
<Link href="/forum" className="flex items-center gap-2 text-[#3b82f6] hover:underline font-body text-sm"> <Link href="/forum" className="flex items-center gap-2 text-[#1D4ED8] hover:underline font-body text-sm">
<span></span> View Forum <span></span> View Forum
</Link> </Link>
<Link href="/admin" className="flex items-center gap-2 text-[#3b82f6] hover:underline font-body text-sm"> <Link href="/admin" className="flex items-center gap-2 text-[#1D4ED8] hover:underline font-body text-sm">
<span></span> Admin Dashboard <span></span> Admin Dashboard
</Link> </Link>
</div> </div>
</div> </div>
{/* Tips */} {/* Tips */}
<div className="bg-[#1a2535] border border-[#2a3a50] rounded-lg p-4"> <div className="bg-[#FFFFFF] border border-[#2a3a50] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-2 text-sm">Tips</h3> <h3 className="text-white font-heading font-semibold mb-2 text-sm">Tips</h3>
<ul className="text-[#aab4c4] font-body text-xs space-y-1.5"> <ul className="text-[#aab4c4] font-body text-xs space-y-1.5">
<li> Threads are imported as real posts they appear immediately in the forum</li> <li> Threads are imported as real posts they appear immediately in the forum</li>

View File

@@ -134,7 +134,7 @@ export default function WPImportPage() {
if (loading || (!user && !loading)) { if (loading || (!user && !loading)) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -142,10 +142,10 @@ export default function WPImportPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-[#cccccc]"> <div className="min-h-screen bg-[#0a0a0a] text-[#cccccc]">
{/* Page Header */} {/* Page Header */}
<div className="bg-[#111111] border-b border-[#252525]"> <div className="bg-[#F8FAFC] border-b border-[#DCE6F2]">
<div className="max-w-6xl mx-auto px-4 py-5"> <div className="max-w-6xl mx-auto px-4 py-5">
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-3 mb-1">
<Link href="/home-page" className="text-[#555] hover:text-[#3b82f6] transition-colors text-sm"> <Link href="/home-page" className="text-[#555] hover:text-[#1D4ED8] transition-colors text-sm">
Home Home
</Link> </Link>
<span className="text-[#333]">/</span> <span className="text-[#333]">/</span>
@@ -158,14 +158,14 @@ export default function WPImportPage() {
<h1 className="text-2xl font-bold text-white font-heading">WordPress Post Importer</h1> <h1 className="text-2xl font-bold text-white font-heading">WordPress Post Importer</h1>
<p className="text-[#666] text-sm mt-1"> <p className="text-[#666] text-sm mt-1">
Import posts from{' '} Import posts from{' '}
<a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline"> <a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#1D4ED8] hover:underline">
avbeat.com avbeat.com
</a>{' '} </a>{' '}
via WordPress REST API via WordPress REST API
</p> </p>
</div> </div>
<div className="flex items-center gap-2 text-xs text-[#555] bg-[#1a1a1a] border border-[#252525] rounded px-3 py-2"> <div className="flex items-center gap-2 text-xs text-[#555] bg-[#FFFFFF] border border-[#DCE6F2] rounded px-3 py-2">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#3b82f6]"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#1D4ED8]">
<circle cx="12" cy="12" r="10" /><path d="M12 8v4l3 3" /> <circle cx="12" cy="12" r="10" /><path d="M12 8v4l3 3" />
</svg> </svg>
{posts.length} posts imported {posts.length} posts imported
@@ -176,9 +176,9 @@ export default function WPImportPage() {
<div className="max-w-6xl mx-auto px-4 py-6 space-y-6"> <div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
{/* Import Controls */} {/* Import Controls */}
<div className="bg-[#111111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg p-5">
<h2 className="text-base font-bold text-white font-heading mb-4 flex items-center gap-2"> <h2 className="text-base font-bold text-white font-heading mb-4 flex items-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#3b82f6]"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#1D4ED8]">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" />
</svg> </svg>
Run Import Run Import
@@ -191,7 +191,7 @@ export default function WPImportPage() {
value={maxPages} value={maxPages}
onChange={(e) => setMaxPages(Number(e.target.value))} onChange={(e) => setMaxPages(Number(e.target.value))}
disabled={isImporting} disabled={isImporting}
className="bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6] disabled:opacity-50"> className="bg-[#FFFFFF] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8] disabled:opacity-50">
<option value={1}>1 page (~20 posts)</option> <option value={1}>1 page (~20 posts)</option>
<option value={3}>3 pages (~60 posts)</option> <option value={3}>3 pages (~60 posts)</option>
<option value={5}>5 pages (~100 posts)</option> <option value={5}>5 pages (~100 posts)</option>
@@ -204,7 +204,7 @@ export default function WPImportPage() {
<button <button
onClick={handleImport} onClick={handleImport}
disabled={isImporting} disabled={isImporting}
className="flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors"> className="flex items-center gap-2 bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:bg-[#DCE6F2] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
{isImporting ? ( {isImporting ? (
<> <>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
@@ -223,16 +223,16 @@ export default function WPImportPage() {
{/* Import Progress / Result */} {/* Import Progress / Result */}
{isImporting && ( {isImporting && (
<div className="mt-4 bg-[#0d1a2e] border border-[#1e3a5f] rounded p-4"> <div className="mt-4 bg-[#0d1a2e] border border-[#DCE6F2] rounded p-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-5 h-5 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin flex-shrink-0" /> <div className="w-5 h-5 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin flex-shrink-0" />
<div> <div>
<p className="text-[#3b82f6] text-sm font-bold">Import in progress</p> <p className="text-[#1D4ED8] text-sm font-bold">Import in progress</p>
<p className="text-[#666] text-xs mt-0.5">Fetching posts from avbeat.com and saving to database...</p> <p className="text-[#666] text-xs mt-0.5">Fetching posts from avbeat.com and saving to database...</p>
</div> </div>
</div> </div>
<div className="mt-3 h-1.5 bg-[#1a2535] rounded-full overflow-hidden"> <div className="mt-3 h-1.5 bg-[#FFFFFF] rounded-full overflow-hidden">
<div className="h-full bg-[#3b82f6] rounded-full animate-pulse w-2/3" /> <div className="h-full bg-[#1D4ED8] rounded-full animate-pulse w-2/3" />
</div> </div>
</div> </div>
)} )}
@@ -261,7 +261,7 @@ export default function WPImportPage() {
{[ {[
{ label: 'Fetched', value: importResult.totalFetched, color: 'text-[#cccccc]' }, { label: 'Fetched', value: importResult.totalFetched, color: 'text-[#cccccc]' },
{ label: 'Imported', value: importResult.totalImported, color: 'text-green-400' }, { label: 'Imported', value: importResult.totalImported, color: 'text-green-400' },
{ label: 'Pages', value: importResult.pagesProcessed, color: 'text-[#3b82f6]' }, { label: 'Pages', value: importResult.pagesProcessed, color: 'text-[#1D4ED8]' },
{ label: 'Errors', value: importResult.totalErrors, color: importResult.totalErrors ? 'text-red-400' : 'text-[#555]' }, { label: 'Errors', value: importResult.totalErrors, color: importResult.totalErrors ? 'text-red-400' : 'text-[#555]' },
].map((stat) => ( ].map((stat) => (
<div key={stat.label} className="bg-[#111] rounded p-2.5 text-center"> <div key={stat.label} className="bg-[#111] rounded p-2.5 text-center">
@@ -277,14 +277,14 @@ export default function WPImportPage() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-0 border-b border-[#252525]"> <div className="flex gap-0 border-b border-[#DCE6F2]">
{(['posts', 'logs'] as const).map((tab) => ( {(['posts', 'logs'] as const).map((tab) => (
<button <button
key={tab} key={tab}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`px-5 py-2.5 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${ className={`px-5 py-2.5 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${
activeTab === tab activeTab === tab
? 'border-[#3b82f6] text-[#3b82f6]' ? 'border-[#1D4ED8] text-[#1D4ED8]'
: 'border-transparent text-[#666] hover:text-[#999]' : 'border-transparent text-[#666] hover:text-[#999]'
}`}> }`}>
{tab === 'posts' ? `Imported Posts (${posts.length})` : `Import History (${logs.length})`} {tab === 'posts' ? `Imported Posts (${posts.length})` : `Import History (${logs.length})`}
@@ -306,17 +306,17 @@ export default function WPImportPage() {
placeholder="Search posts..." placeholder="Search posts..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#3b82f6] placeholder-[#444]" className="w-full bg-[#111] border border-[#DCE6F2] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#1D4ED8] placeholder-[#444]"
/> />
</div> </div>
</div> </div>
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : filteredPosts.length === 0 ? ( ) : filteredPosts.length === 0 ? (
<div className="text-center py-16 bg-[#111] border border-[#252525] rounded-lg"> <div className="text-center py-16 bg-[#111] border border-[#DCE6F2] rounded-lg">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333] mx-auto mb-3"> <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333] mx-auto mb-3">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><polyline points="10 9 9 9 8 9" /> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><polyline points="10 9 9 9 8 9" />
</svg> </svg>
@@ -327,10 +327,10 @@ export default function WPImportPage() {
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{filteredPosts.map((post) => ( {filteredPosts.map((post) => (
<div key={post.id} className="bg-[#111] border border-[#252525] rounded-lg p-4 flex items-start gap-4 hover:border-[#333] transition-colors"> <div key={post.id} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 flex items-start gap-4 hover:border-[#333] transition-colors">
{/* Thumbnail */} {/* Thumbnail */}
{post.featured_image ? ( {post.featured_image ? (
<div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#1a1a1a]"> <div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#FFFFFF]">
<AppImage <AppImage
src={post.featured_image} src={post.featured_image}
alt={post.featured_image_alt || post.title} alt={post.featured_image_alt || post.title}
@@ -340,7 +340,7 @@ export default function WPImportPage() {
/> />
</div> </div>
) : ( ) : (
<div className="flex-shrink-0 w-16 h-12 rounded bg-[#1a1a1a] flex items-center justify-center"> <div className="flex-shrink-0 w-16 h-12 rounded bg-[#FFFFFF] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /> <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg> </svg>
@@ -354,7 +354,7 @@ export default function WPImportPage() {
<h3 className="text-sm font-bold text-[#cccccc] leading-snug line-clamp-1">{post.title}</h3> <h3 className="text-sm font-bold text-[#cccccc] leading-snug line-clamp-1">{post.title}</h3>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1"> <div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1">
{post.category && ( {post.category && (
<span className="text-[10px] font-bold uppercase tracking-wider text-[#3b82f6] bg-[#0d1a2e] px-1.5 py-0.5 rounded"> <span className="text-[10px] font-bold uppercase tracking-wider text-[#1D4ED8] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
{post.category} {post.category}
</span> </span>
)} )}
@@ -368,7 +368,7 @@ export default function WPImportPage() {
href={`https://www.avbeat.com/${post.wp_slug}`} href={`https://www.avbeat.com/${post.wp_slug}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[#555] hover:text-[#3b82f6] transition-colors p-1" className="text-[#555] hover:text-[#1D4ED8] transition-colors p-1"
title="View on avbeat.com"> title="View on avbeat.com">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" />
@@ -402,16 +402,16 @@ export default function WPImportPage() {
<div> <div>
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : logs.length === 0 ? ( ) : logs.length === 0 ? (
<div className="text-center py-16 bg-[#111] border border-[#252525] rounded-lg"> <div className="text-center py-16 bg-[#111] border border-[#DCE6F2] rounded-lg">
<p className="text-[#555] text-sm">No import history yet.</p> <p className="text-[#555] text-sm">No import history yet.</p>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{logs.map((log) => ( {logs.map((log) => (
<div key={log.id} className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div key={log.id} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{log.status === 'completed' ? ( {log.status === 'completed' ? (
@@ -439,7 +439,7 @@ export default function WPImportPage() {
{ label: 'Skipped', value: log.total_skipped }, { label: 'Skipped', value: log.total_skipped },
{ label: 'Errors', value: log.total_errors }, { label: 'Errors', value: log.total_errors },
].map((stat) => ( ].map((stat) => (
<div key={stat.label} className="bg-[#0d0d0d] rounded p-2 text-center"> <div key={stat.label} className="bg-[#F8FAFC] rounded p-2 text-center">
<p className="text-base font-bold text-[#cccccc]">{stat.value}</p> <p className="text-base font-bold text-[#cccccc]">{stat.value}</p>
<p className="text-[10px] text-[#444] uppercase tracking-wider">{stat.label}</p> <p className="text-[10px] text-[#444] uppercase tracking-wider">{stat.label}</p>
</div> </div>

View File

@@ -67,11 +67,11 @@ function fmtDate(dateStr: string): string {
// ─── Sub-components ─────────────────────────────────────────────────────────── // ─── Sub-components ───────────────────────────────────────────────────────────
function StatCard({ label, value, sub, accent = '#3b82f6', icon }: { function StatCard({ label, value, sub, accent = '#1D4ED8', icon }: {
label: string; value: string; sub: string; accent?: string; icon: React.ReactNode; label: string; value: string; sub: string; accent?: string; icon: React.ReactNode;
}) { }) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex items-start gap-4"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-5 flex items-start gap-4">
<div <div
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0" className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
style={{ background: `${accent}18` }} style={{ background: `${accent}18` }}
@@ -90,7 +90,7 @@ function StatCard({ label, value, sub, accent = '#3b82f6', icon }: {
function ChartTooltip({ active, payload, label }: any) { function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null; if (!active || !payload?.length) return null;
return ( return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl"> <div className="bg-[#FFFFFF] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1">{label}</p>} {label && <p className="text-[#888] mb-1">{label}</p>}
{payload.map((entry: any, i: number) => ( {payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5"> <p key={i} style={{ color: entry.color }} className="leading-5">
@@ -243,7 +243,7 @@ export default function NewsletterAnalyticsPage() {
if (loading || loadingData) { if (loading || loadingData) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -258,7 +258,7 @@ export default function NewsletterAnalyticsPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-white"> <div className="min-h-screen bg-[#0a0a0a] text-white">
{/* Header */} {/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0d0d0d] sticky top-0 z-10"> <div className="border-b border-[#FFFFFF] bg-[#F8FAFC] sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between"> <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-white transition-colors"> <Link href="/admin" className="text-[#555] hover:text-white transition-colors">
@@ -266,7 +266,7 @@ export default function NewsletterAnalyticsPage() {
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /> <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg> </svg>
</Link> </Link>
<div className="w-px h-5 bg-[#252525]" /> <div className="w-px h-5 bg-[#DCE6F2]" />
<Link href="/admin/newsletter" className="text-[#555] hover:text-white text-sm font-body transition-colors"> <Link href="/admin/newsletter" className="text-[#555] hover:text-white text-sm font-body transition-colors">
Newsletter Newsletter
</Link> </Link>
@@ -278,14 +278,14 @@ export default function NewsletterAnalyticsPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link <Link
href="/admin/newsletter/templates" href="/admin/newsletter/templates"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#DCE6F2] rounded-lg transition-colors"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg> </svg>
Templates Templates
</Link> </Link>
<div className="w-px h-8 bg-[#252525]" /> <div className="w-px h-8 bg-[#DCE6F2]" />
<div className="text-right"> <div className="text-right">
<p className="text-white text-sm font-bold font-heading">{fmt(totalSubscribers)}</p> <p className="text-white text-sm font-bold font-heading">{fmt(totalSubscribers)}</p>
<p className="text-[#555] text-xs font-body">active subscribers</p> <p className="text-[#555] text-xs font-body">active subscribers</p>
@@ -308,7 +308,7 @@ export default function NewsletterAnalyticsPage() {
label="Avg Open Rate" label="Avg Open Rate"
value={pct(avgOpenRate)} value={pct(avgOpenRate)}
sub={`across ${digestData.length} digests`} sub={`across ${digestData.length} digests`}
accent="#3b82f6" accent="#1D4ED8"
icon={ icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
@@ -352,7 +352,7 @@ export default function NewsletterAnalyticsPage() {
</div> </div>
{/* Open & Click Rate Trend */} {/* Open & Click Rate Trend */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-6">
<SectionHeader title="Open & Click Rate Trend" subtitle="Rate per digest over time" /> <SectionHeader title="Open & Click Rate Trend" subtitle="Rate per digest over time" />
<div className="h-64"> <div className="h-64">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
@@ -362,7 +362,7 @@ export default function NewsletterAnalyticsPage() {
<YAxis tick={{ fill: '#555', fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => `${v}%`} /> <YAxis tick={{ fill: '#555', fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => `${v}%`} />
<Tooltip content={<ChartTooltip />} /> <Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11, color: '#888' }} /> <Legend wrapperStyle={{ fontSize: 11, color: '#888' }} />
<Line type="monotone" dataKey="Open Rate" stroke="#3b82f6" strokeWidth={2} dot={{ r: 3, fill: '#3b82f6' }} activeDot={{ r: 5 }} /> <Line type="monotone" dataKey="Open Rate" stroke="#1D4ED8" strokeWidth={2} dot={{ r: 3, fill: '#1D4ED8' }} activeDot={{ r: 5 }} />
<Line type="monotone" dataKey="Click Rate" stroke="#10b981" strokeWidth={2} dot={{ r: 3, fill: '#10b981' }} activeDot={{ r: 5 }} /> <Line type="monotone" dataKey="Click Rate" stroke="#10b981" strokeWidth={2} dot={{ r: 3, fill: '#10b981' }} activeDot={{ r: 5 }} />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
@@ -370,7 +370,7 @@ export default function NewsletterAnalyticsPage() {
</div> </div>
{/* Subscriber Growth Chart */} {/* Subscriber Growth Chart */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-6">
<SectionHeader title="Subscriber Growth" subtitle="Monthly net new subscribers vs unsubscribes" /> <SectionHeader title="Subscriber Growth" subtitle="Monthly net new subscribers vs unsubscribes" />
<div className="h-64"> <div className="h-64">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
@@ -380,7 +380,7 @@ export default function NewsletterAnalyticsPage() {
<YAxis tick={{ fill: '#555', fontSize: 11 }} tickLine={false} axisLine={false} /> <YAxis tick={{ fill: '#555', fontSize: 11 }} tickLine={false} axisLine={false} />
<Tooltip content={<ChartTooltip />} /> <Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11, color: '#888' }} /> <Legend wrapperStyle={{ fontSize: 11, color: '#888' }} />
<Bar dataKey="subscribers" name="Total Subscribers" fill="#3b82f6" radius={[3, 3, 0, 0]} /> <Bar dataKey="subscribers" name="Total Subscribers" fill="#1D4ED8" radius={[3, 3, 0, 0]} />
<Bar dataKey="unsubscribes" name="Unsubscribes" fill="#ef4444" radius={[3, 3, 0, 0]} /> <Bar dataKey="unsubscribes" name="Unsubscribes" fill="#ef4444" radius={[3, 3, 0, 0]} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
@@ -388,7 +388,7 @@ export default function NewsletterAnalyticsPage() {
</div> </div>
{/* Per-Digest Performance Table */} {/* Per-Digest Performance Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-6 py-5 border-b border-[#1e1e1e]"> <div className="px-6 py-5 border-b border-[#1e1e1e]">
<SectionHeader title="Per-Digest Performance" subtitle="Click column headers to sort" /> <SectionHeader title="Per-Digest Performance" subtitle="Click column headers to sort" />
</div> </div>
@@ -421,7 +421,7 @@ export default function NewsletterAnalyticsPage() {
{sortedDigests.map((digest, i) => ( {sortedDigests.map((digest, i) => (
<tr <tr
key={digest.id} key={digest.id}
className={`border-b border-[#1a1a1a] hover:bg-[#161616] transition-colors ${i % 2 === 0 ? '' : 'bg-[#0d0d0d]'}`} className={`border-b border-[#FFFFFF] hover:bg-[#FFFFFF] transition-colors ${i % 2 === 0 ? '' : 'bg-[#F8FAFC]'}`}
> >
<td className="px-4 py-3 text-white max-w-xs"> <td className="px-4 py-3 text-white max-w-xs">
<p className="truncate font-medium" title={digest.subject}>{digest.subject}</p> <p className="truncate font-medium" title={digest.subject}>{digest.subject}</p>

View File

@@ -252,7 +252,7 @@ export default function CampaignEditorPage() {
article_blocks: payload.article_blocks, article_blocks: payload.article_blocks,
layout: 'featured', layout: 'featured',
style: 'dark', style: 'dark',
accent_color: '#3b82f6', accent_color: '#1D4ED8',
is_preset: false, is_preset: false,
}), }),
}); });
@@ -296,7 +296,7 @@ export default function CampaignEditorPage() {
)} )}
{/* ── Top Bar ── */} {/* ── Top Bar ── */}
<div className="sticky top-0 z-40 bg-[#0a0a0a]/95 backdrop-blur border-b border-[#1a1a1a]"> <div className="sticky top-0 z-40 bg-[#0a0a0a]/95 backdrop-blur border-b border-[#FFFFFF]">
<div className="max-w-screen-xl mx-auto px-4 h-14 flex items-center justify-between gap-4"> <div className="max-w-screen-xl mx-auto px-4 h-14 flex items-center justify-between gap-4">
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<Link href="/admin/newsletter" className="text-[#555] hover:text-white transition-colors flex-shrink-0"> <Link href="/admin/newsletter" className="text-[#555] hover:text-white transition-colors flex-shrink-0">
@@ -313,7 +313,7 @@ export default function CampaignEditorPage() {
className="bg-transparent text-sm font-medium text-white placeholder-[#444] outline-none min-w-0 w-48" className="bg-transparent text-sm font-medium text-white placeholder-[#444] outline-none min-w-0 w-48"
/> />
<span className={`text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0 ${ <span className={`text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0 ${
campaign.status === 'draft' ? 'bg-[#1a1a1a] text-[#666]' : campaign.status === 'draft' ? 'bg-[#FFFFFF] text-[#666]' :
campaign.status === 'scheduled'? 'bg-yellow-400/10 text-yellow-400' : 'bg-green-400/10 text-green-400' campaign.status === 'scheduled'? 'bg-yellow-400/10 text-yellow-400' : 'bg-green-400/10 text-green-400'
}`}> }`}>
{campaign.status} {campaign.status}
@@ -339,7 +339,7 @@ export default function CampaignEditorPage() {
<button <button
onClick={() => handleSave('draft')} onClick={() => handleSave('draft')}
disabled={saving} disabled={saving}
className="px-3 py-1.5 text-xs font-medium bg-[#1a1a1a] hover:bg-[#222] border border-[#2a2a2a] text-[#aaa] rounded-lg transition-colors disabled:opacity-50" className="px-3 py-1.5 text-xs font-medium bg-[#FFFFFF] hover:bg-[#222] border border-[#DCE6F2] text-[#aaa] rounded-lg transition-colors disabled:opacity-50"
> >
{saving ? 'Saving…' : 'Save Draft'} {saving ? 'Saving…' : 'Save Draft'}
</button> </button>
@@ -393,9 +393,9 @@ export default function CampaignEditorPage() {
<div className="space-y-2"> <div className="space-y-2">
{loadingArticles ? ( {loadingArticles ? (
Array.from({ length: 5 }).map((_, i) => ( Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4 animate-pulse"> <div key={i} className="bg-[#111] border border-[#FFFFFF] rounded-xl p-4 animate-pulse">
<div className="h-3 bg-[#1e1e1e] rounded w-3/4 mb-2" /> <div className="h-3 bg-[#1e1e1e] rounded w-3/4 mb-2" />
<div className="h-2.5 bg-[#1a1a1a] rounded w-1/2" /> <div className="h-2.5 bg-[#FFFFFF] rounded w-1/2" />
</div> </div>
)) ))
) : articles.length === 0 ? ( ) : articles.length === 0 ? (
@@ -409,7 +409,7 @@ export default function CampaignEditorPage() {
onClick={() => toggleArticle(article)} onClick={() => toggleArticle(article)}
className={`group relative bg-[#111] border rounded-xl p-4 cursor-pointer transition-all ${ className={`group relative bg-[#111] border rounded-xl p-4 cursor-pointer transition-all ${
selected selected
? 'border-white/20 bg-white/5' :'border-[#1a1a1a] hover:border-[#2a2a2a]' ? 'border-white/20 bg-white/5' :'border-[#FFFFFF] hover:border-[#DCE6F2]'
}`} }`}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
@@ -427,7 +427,7 @@ export default function CampaignEditorPage() {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
{article.category && ( {article.category && (
<span className="text-[10px] font-semibold uppercase tracking-wider text-[#555] bg-[#1a1a1a] px-1.5 py-0.5 rounded"> <span className="text-[10px] font-semibold uppercase tracking-wider text-[#555] bg-[#FFFFFF] px-1.5 py-0.5 rounded">
{article.category} {article.category}
</span> </span>
)} )}
@@ -454,7 +454,7 @@ export default function CampaignEditorPage() {
{(activePanel === 'settings' || activePanel === 'articles') && ( {(activePanel === 'settings' || activePanel === 'articles') && (
<> <>
{/* Subject Line */} {/* Subject Line */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4"> <div className="bg-[#111] border border-[#FFFFFF] rounded-xl p-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Subject Line</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider">Subject Line</h3>
<span className={`text-[10px] font-mono ${subjectCharCount > 60 ? 'text-yellow-400' : 'text-[#444]'}`}> <span className={`text-[10px] font-mono ${subjectCharCount > 60 ? 'text-yellow-400' : 'text-[#444]'}`}>
@@ -477,7 +477,7 @@ export default function CampaignEditorPage() {
</div> </div>
{/* Preview Text */} {/* Preview Text */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4"> <div className="bg-[#111] border border-[#FFFFFF] rounded-xl p-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Preview Text</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider">Preview Text</h3>
<span className={`text-[10px] font-mono ${previewCharCount > 90 ? 'text-yellow-400' : 'text-[#444]'}`}> <span className={`text-[10px] font-mono ${previewCharCount > 90 ? 'text-yellow-400' : 'text-[#444]'}`}>
@@ -497,7 +497,7 @@ export default function CampaignEditorPage() {
</div> </div>
{/* Recipient Segment */} {/* Recipient Segment */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4"> <div className="bg-[#111] border border-[#FFFFFF] rounded-xl p-4">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Recipient Segment</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Recipient Segment</h3>
<div className="space-y-2"> <div className="space-y-2">
{SEGMENTS.map((seg) => { {SEGMENTS.map((seg) => {
@@ -509,7 +509,7 @@ export default function CampaignEditorPage() {
onClick={() => setCampaign((p) => ({ ...p, segment: seg.id }))} onClick={() => setCampaign((p) => ({ ...p, segment: seg.id }))}
className={`w-full text-left px-3 py-2.5 rounded-lg border transition-all ${ className={`w-full text-left px-3 py-2.5 rounded-lg border transition-all ${
active active
? 'bg-white/5 border-white/20 text-white' :'bg-[#0a0a0a] border-[#1a1a1a] text-[#888] hover:border-[#2a2a2a] hover:text-white' ? 'bg-white/5 border-white/20 text-white' :'bg-[#0a0a0a] border-[#FFFFFF] text-[#888] hover:border-[#DCE6F2] hover:text-white'
}`} }`}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -529,7 +529,7 @@ export default function CampaignEditorPage() {
})} })}
</div> </div>
{recipientCount > 0 && ( {recipientCount > 0 && (
<div className="mt-3 pt-3 border-t border-[#1a1a1a] flex items-center gap-2"> <div className="mt-3 pt-3 border-t border-[#FFFFFF] flex items-center gap-2">
<svg className="w-3.5 h-3.5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg> </svg>
@@ -543,7 +543,7 @@ export default function CampaignEditorPage() {
)} )}
{/* ── Selected Articles ── */} {/* ── Selected Articles ── */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4"> <div className="bg-[#111] border border-[#FFFFFF] rounded-xl p-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Selected Articles</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider">Selected Articles</h3>
<span className="text-[10px] text-[#555]">{campaign.articles.length} article{campaign.articles.length !== 1 ? 's' : ''}</span> <span className="text-[10px] text-[#555]">{campaign.articles.length} article{campaign.articles.length !== 1 ? 's' : ''}</span>
@@ -559,7 +559,7 @@ export default function CampaignEditorPage() {
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{campaign.articles.map((article, idx) => ( {campaign.articles.map((article, idx) => (
<div key={article.id} className="bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg p-3"> <div key={article.id} className="bg-[#0a0a0a] border border-[#FFFFFF] rounded-lg p-3">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
{/* Order controls */} {/* Order controls */}
<div className="flex flex-col gap-0.5 flex-shrink-0 mt-0.5"> <div className="flex flex-col gap-0.5 flex-shrink-0 mt-0.5">
@@ -618,16 +618,16 @@ export default function CampaignEditorPage() {
{/* ── Preview Panel ── */} {/* ── Preview Panel ── */}
{activePanel === 'preview' && ( {activePanel === 'preview' && (
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl overflow-hidden"> <div className="bg-[#111] border border-[#FFFFFF] rounded-xl overflow-hidden">
<div className="px-4 py-3 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-4 py-3 border-b border-[#FFFFFF] flex items-center justify-between">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Email Preview</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider">Email Preview</h3>
<span className="text-[10px] text-[#555]">Inbox view</span> <span className="text-[10px] text-[#555]">Inbox view</span>
</div> </div>
{/* Inbox row preview */} {/* Inbox row preview */}
<div className="px-4 py-3 border-b border-[#1a1a1a] bg-[#0d0d0d]"> <div className="px-4 py-3 border-b border-[#FFFFFF] bg-[#F8FAFC]">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0"> <div className="w-6 h-6 rounded-full bg-[#FFFFFF] flex items-center justify-center flex-shrink-0">
<span className="text-[8px] font-bold text-[#666]">BB</span> <span className="text-[8px] font-bold text-[#666]">BB</span>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -644,7 +644,7 @@ export default function CampaignEditorPage() {
{/* Email body preview */} {/* Email body preview */}
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto"> <div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
{/* Header */} {/* Header */}
<div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#1a1a1a]"> <div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#FFFFFF]">
<p className="text-xs font-bold text-white tracking-widest uppercase">AV Beat</p> <p className="text-xs font-bold text-white tracking-widest uppercase">AV Beat</p>
{campaign.previewText && ( {campaign.previewText && (
<p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p> <p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p>
@@ -657,7 +657,7 @@ export default function CampaignEditorPage() {
) : ( ) : (
campaign.articles.map((article, idx) => ( campaign.articles.map((article, idx) => (
<div key={article.id} className={`rounded-lg border p-3 ${ <div key={article.id} className={`rounded-lg border p-3 ${
article.featured ? 'border-white/10 bg-white/3' : 'border-[#1a1a1a] bg-[#0a0a0a]' article.featured ? 'border-white/10 bg-white/3' : 'border-[#FFFFFF] bg-[#0a0a0a]'
}`}> }`}>
{article.featured && ( {article.featured && (
<span className="text-[9px] font-semibold uppercase tracking-wider text-yellow-400 mb-1 block">Featured Story</span> <span className="text-[9px] font-semibold uppercase tracking-wider text-yellow-400 mb-1 block">Featured Story</span>
@@ -679,7 +679,7 @@ export default function CampaignEditorPage() {
)} )}
{/* Footer */} {/* Footer */}
<div className="text-center pt-2 border-t border-[#1a1a1a]"> <div className="text-center pt-2 border-t border-[#FFFFFF]">
<p className="text-[9px] text-[#333]">AV Beat · Unsubscribe · View in browser</p> <p className="text-[9px] text-[#333]">AV Beat · Unsubscribe · View in browser</p>
</div> </div>
</div> </div>
@@ -687,7 +687,7 @@ export default function CampaignEditorPage() {
)} )}
{/* ── Publish Summary ── */} {/* ── Publish Summary ── */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4"> <div className="bg-[#111] border border-[#FFFFFF] rounded-xl p-4">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Campaign Summary</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Campaign Summary</h3>
<div className="space-y-2"> <div className="space-y-2">
{[ {[
@@ -710,7 +710,7 @@ export default function CampaignEditorPage() {
<button <button
onClick={() => handleSave('draft')} onClick={() => handleSave('draft')}
disabled={saving} disabled={saving}
className="flex-1 py-2 text-xs font-medium bg-[#1a1a1a] hover:bg-[#222] border border-[#2a2a2a] text-[#aaa] rounded-lg transition-colors disabled:opacity-50" className="flex-1 py-2 text-xs font-medium bg-[#FFFFFF] hover:bg-[#222] border border-[#DCE6F2] text-[#aaa] rounded-lg transition-colors disabled:opacity-50"
> >
{saving ? 'Saving…' : 'Save Draft'} {saving ? 'Saving…' : 'Save Draft'}
</button> </button>

View File

@@ -356,7 +356,7 @@ export default function AdminNewsletterPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -373,7 +373,7 @@ export default function AdminNewsletterPage() {
)} )}
{/* Header */} {/* Header */}
<div className="bg-[#111] border-b border-[#252525] px-6 py-4"> <div className="bg-[#111] border-b border-[#DCE6F2] px-6 py-4">
<div className="max-w-6xl mx-auto flex items-center justify-between"> <div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-[#888] transition-colors"> <Link href="/admin" className="text-[#555] hover:text-[#888] transition-colors">
@@ -389,7 +389,7 @@ export default function AdminNewsletterPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link <Link
href="/admin/newsletter/templates" href="/admin/newsletter/templates"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors"> className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#DCE6F2] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg> </svg>
@@ -397,7 +397,7 @@ export default function AdminNewsletterPage() {
</Link> </Link>
<Link <Link
href="/admin/newsletter/campaign-editor" href="/admin/newsletter/campaign-editor"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-white bg-[#3b82f6] hover:bg-[#2563eb] rounded-lg transition-colors"> className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-white bg-[#1D4ED8] hover:bg-[#1E3A8A] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /> <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg> </svg>
@@ -405,18 +405,18 @@ export default function AdminNewsletterPage() {
</Link> </Link>
<Link <Link
href="/admin/newsletter/analytics" href="/admin/newsletter/analytics"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors"> className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#DCE6F2] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
</svg> </svg>
Analytics Analytics
</Link> </Link>
<div className="w-px h-8 bg-[#252525]" /> <div className="w-px h-8 bg-[#DCE6F2]" />
<div className="text-right"> <div className="text-right">
<p className="text-white font-display font-bold text-xl">{activeCount}</p> <p className="text-white font-display font-bold text-xl">{activeCount}</p>
<p className="text-[#555] text-xs font-body">active subscribers</p> <p className="text-[#555] text-xs font-body">active subscribers</p>
</div> </div>
<div className="w-px h-8 bg-[#252525]" /> <div className="w-px h-8 bg-[#DCE6F2]" />
<div className="text-right"> <div className="text-right">
<p className="text-white font-display font-bold text-xl">{totalCount}</p> <p className="text-white font-display font-bold text-xl">{totalCount}</p>
<p className="text-[#555] text-xs font-body">total</p> <p className="text-[#555] text-xs font-body">total</p>
@@ -427,15 +427,15 @@ export default function AdminNewsletterPage() {
<div className="max-w-6xl mx-auto px-6 py-6"> <div className="max-w-6xl mx-auto px-6 py-6">
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit"> <div className="flex gap-1 mb-6 bg-[#111] border border-[#DCE6F2] rounded-lg p-1 w-fit">
<button <button
onClick={() => setActiveTab('subscribers')} onClick={() => setActiveTab('subscribers')}
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'subscribers' ? 'bg-[#3b82f6] text-white' : 'text-[#888] hover:text-white'}`}> className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'subscribers' ? 'bg-[#1D4ED8] text-white' : 'text-[#888] hover:text-white'}`}>
Subscribers Subscribers
</button> </button>
<button <button
onClick={() => setActiveTab('send-digest')} onClick={() => setActiveTab('send-digest')}
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'send-digest' ? 'bg-[#3b82f6] text-white' : 'text-[#888] hover:text-white'}`}> className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'send-digest' ? 'bg-[#1D4ED8] text-white' : 'text-[#888] hover:text-white'}`}>
Send Digest Send Digest
</button> </button>
</div> </div>
@@ -452,12 +452,12 @@ export default function AdminNewsletterPage() {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="search-input flex-1 py-2 px-3 text-sm" className="search-input flex-1 py-2 px-3 text-sm"
/> />
<div className="flex gap-1 bg-[#111] border border-[#252525] rounded-lg p-1"> <div className="flex gap-1 bg-[#111] border border-[#DCE6F2] rounded-lg p-1">
{(['all', 'active', 'unsubscribed'] as const).map((s) => ( {(['all', 'active', 'unsubscribed'] as const).map((s) => (
<button <button
key={s} key={s}
onClick={() => setStatusFilter(s)} onClick={() => setStatusFilter(s)}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-colors ${statusFilter === s ? 'bg-[#3b82f6] text-white' : 'text-[#888] hover:text-white'}`} className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-colors ${statusFilter === s ? 'bg-[#1D4ED8] text-white' : 'text-[#888] hover:text-white'}`}
> >
{s} {s}
</button> </button>
@@ -467,7 +467,7 @@ export default function AdminNewsletterPage() {
<button <button
onClick={handleExportCSV} onClick={handleExportCSV}
disabled={subscribers.length === 0} disabled={subscribers.length === 0}
className="flex items-center gap-1.5 px-3 py-2 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap" className="flex items-center gap-1.5 px-3 py-2 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#DCE6F2] rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 015.25 21h13.5A2.25 2.25 0 0121 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 015.25 21h13.5A2.25 2.25 0 0121 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
@@ -478,9 +478,9 @@ export default function AdminNewsletterPage() {
{/* Bulk Actions Bar */} {/* Bulk Actions Bar */}
{someSelected && ( {someSelected && (
<div className="flex items-center justify-between bg-[#1a2535] border border-[#1e3a5f] rounded-lg px-4 py-3 mb-4"> <div className="flex items-center justify-between bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg px-4 py-3 mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-sm font-body font-semibold text-[#3b82f6]"> <span className="text-sm font-body font-semibold text-[#1D4ED8]">
{selectedIds.size} selected {selectedIds.size} selected
</span> </span>
<button <button
@@ -512,17 +512,17 @@ export default function AdminNewsletterPage() {
Delete Delete
</button> </button>
{bulkLoading && ( {bulkLoading && (
<div className="w-4 h-4 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
)} )}
</div> </div>
</div> </div>
)} )}
{/* Table */} {/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : subscribers.length === 0 ? ( ) : subscribers.length === 0 ? (
<div className="text-center py-16"> <div className="text-center py-16">
@@ -534,13 +534,13 @@ export default function AdminNewsletterPage() {
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-[#252525]"> <tr className="border-b border-[#DCE6F2]">
<th className="px-4 py-3 w-10"> <th className="px-4 py-3 w-10">
<input <input
type="checkbox" type="checkbox"
checked={allSelected} checked={allSelected}
onChange={toggleSelectAll} onChange={toggleSelectAll}
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#3b82f6] cursor-pointer" className="w-4 h-4 rounded border-[#444] bg-[#FFFFFF] accent-[#1D4ED8] cursor-pointer"
/> />
</th> </th>
<th className="text-left px-4 py-3 text-xs font-body font-bold text-[#555] uppercase tracking-wider">Email</th> <th className="text-left px-4 py-3 text-xs font-body font-bold text-[#555] uppercase tracking-wider">Email</th>
@@ -554,21 +554,21 @@ export default function AdminNewsletterPage() {
{subscribers.map((sub) => ( {subscribers.map((sub) => (
<tr <tr
key={sub.id} key={sub.id}
className={`border-b border-[#1a1a1a] transition-colors ${selectedIds.has(sub.id) ? 'bg-[#0d1a2d]' : 'hover:bg-[#0d0d0d]'}`} className={`border-b border-[#FFFFFF] transition-colors ${selectedIds.has(sub.id) ? 'bg-[#0d1a2d]' : 'hover:bg-[#F8FAFC]'}`}
> >
<td className="px-4 py-3 w-10"> <td className="px-4 py-3 w-10">
<input <input
type="checkbox" type="checkbox"
checked={selectedIds.has(sub.id)} checked={selectedIds.has(sub.id)}
onChange={() => toggleSelect(sub.id)} onChange={() => toggleSelect(sub.id)}
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#3b82f6] cursor-pointer" className="w-4 h-4 rounded border-[#444] bg-[#FFFFFF] accent-[#1D4ED8] cursor-pointer"
/> />
</td> </td>
<td className="px-4 py-3 text-sm font-body text-[#e0e0e0]">{sub.email}</td> <td className="px-4 py-3 text-sm font-body text-[#e0e0e0]">{sub.email}</td>
<td className="px-4 py-3 hidden md:table-cell"> <td className="px-4 py-3 hidden md:table-cell">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{sub.topics?.slice(0, 3).map((t) => ( {sub.topics?.slice(0, 3).map((t) => (
<span key={t} className="text-xs bg-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{t}</span> <span key={t} className="text-xs bg-[#FFFFFF] text-[#1D4ED8] px-2 py-0.5 rounded-full font-body">{t}</span>
))} ))}
{sub.topics?.length > 3 && ( {sub.topics?.length > 3 && (
<span className="text-xs text-[#555] font-body">+{sub.topics.length - 3}</span> <span className="text-xs text-[#555] font-body">+{sub.topics.length - 3}</span>
@@ -576,7 +576,7 @@ export default function AdminNewsletterPage() {
</div> </div>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`text-xs font-body font-bold px-2 py-1 rounded-full ${sub.status === 'active' ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#1a1a1a]'}`}> <span className={`text-xs font-body font-bold px-2 py-1 rounded-full ${sub.status === 'active' ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#FFFFFF]'}`}>
{sub.status} {sub.status}
</span> </span>
</td> </td>
@@ -616,21 +616,21 @@ export default function AdminNewsletterPage() {
{activeTab === 'send-digest' && ( {activeTab === 'send-digest' && (
<div className="max-w-2xl"> <div className="max-w-2xl">
{/* SMTP warning if not configured */} {/* SMTP warning if not configured */}
<div className="bg-[#1a2535] border border-[#1e3a5f] rounded-lg p-4 mb-6 flex gap-3"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-4 mb-6 flex gap-3">
<svg className="w-5 h-5 text-[#3b82f6] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5 text-[#1D4ED8] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<div> <div>
<p className="text-[#e0e0e0] text-sm font-body font-semibold">SMTP Required</p> <p className="text-[#e0e0e0] text-sm font-body font-semibold">SMTP Required</p>
<p className="text-[#777] text-xs font-body mt-0.5"> <p className="text-[#777] text-xs font-body mt-0.5">
Set <code className="text-[#3b82f6]">SMTP_HOST</code>, <code className="text-[#3b82f6]">SMTP_USER</code>, <code className="text-[#3b82f6]">SMTP_PASS</code>, and <code className="text-[#3b82f6]">SMTP_FROM_EMAIL</code> in your environment variables to enable sending. Set <code className="text-[#1D4ED8]">SMTP_HOST</code>, <code className="text-[#1D4ED8]">SMTP_USER</code>, <code className="text-[#1D4ED8]">SMTP_PASS</code>, and <code className="text-[#1D4ED8]">SMTP_FROM_EMAIL</code> in your environment variables to enable sending.
</p> </p>
</div> </div>
</div> </div>
{/* Applied template badge */} {/* Applied template badge */}
{appliedTemplate && ( {appliedTemplate && (
<div className="flex items-center justify-between bg-[#1a2535] border border-[#1e3a5f] rounded-lg px-4 py-3 mb-4"> <div className="flex items-center justify-between bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg px-4 py-3 mb-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5" style={{ backgroundColor: appliedTemplate.accent_color + '20' }}> <div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5" style={{ backgroundColor: appliedTemplate.accent_color + '20' }}>
<span style={{ color: appliedTemplate.accent_color }}> <span style={{ color: appliedTemplate.accent_color }}>
@@ -662,7 +662,7 @@ export default function AdminNewsletterPage() {
<p className="text-xs font-body text-[#555]">Fill in the fields below or start from a template</p> <p className="text-xs font-body text-[#555]">Fill in the fields below or start from a template</p>
<button <button
onClick={() => { fetchTemplates(); setShowTemplateDrawer(true); }} onClick={() => { fetchTemplates(); setShowTemplateDrawer(true); }}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#3b82f6] border border-[#1e3a5f] rounded-lg hover:bg-[#1a2535] transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#1D4ED8] border border-[#DCE6F2] rounded-lg hover:bg-[#FFFFFF] transition-colors"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
@@ -725,14 +725,14 @@ export default function AdminNewsletterPage() {
<button <button
type="button" type="button"
onClick={addArticle} onClick={addArticle}
className="text-xs text-[#3b82f6] hover:text-blue-400 font-body font-semibold transition-colors" className="text-xs text-[#1D4ED8] hover:text-blue-400 font-body font-semibold transition-colors"
> >
+ Add Article + Add Article
</button> </button>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{digestArticles.map((article, index) => ( {digestArticles.map((article, index) => (
<div key={index} className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div key={index} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<span className="text-xs font-body text-[#555]">Article {index + 1}</span> <span className="text-xs font-body text-[#555]">Article {index + 1}</span>
{digestArticles.length > 1 && ( {digestArticles.length > 1 && (
@@ -803,11 +803,11 @@ export default function AdminNewsletterPage() {
{/* Template Picker Drawer */} {/* Template Picker Drawer */}
{showTemplateDrawer && ( {showTemplateDrawer && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-end sm:items-center justify-center p-0 sm:p-4" onClick={() => setShowTemplateDrawer(false)}> <div className="fixed inset-0 z-50 bg-black/70 flex items-end sm:items-center justify-center p-0 sm:p-4" onClick={() => setShowTemplateDrawer(false)}>
<div className="bg-[#111] border border-[#252525] rounded-t-2xl sm:rounded-xl w-full sm:max-w-2xl max-h-[80vh] overflow-hidden flex flex-col" onClick={(e) => e.stopPropagation()}> <div className="bg-[#111] border border-[#DCE6F2] rounded-t-2xl sm:rounded-xl w-full sm:max-w-2xl max-h-[80vh] overflow-hidden flex flex-col" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525] flex-shrink-0"> <div className="flex items-center justify-between px-6 py-4 border-b border-[#DCE6F2] flex-shrink-0">
<h2 className="font-display font-bold text-white text-base">Choose a Template</h2> <h2 className="font-display font-bold text-white text-base">Choose a Template</h2>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin/newsletter/templates" className="text-xs text-[#3b82f6] font-body hover:text-blue-400 transition-colors"> <Link href="/admin/newsletter/templates" className="text-xs text-[#1D4ED8] font-body hover:text-blue-400 transition-colors">
Manage templates Manage templates
</Link> </Link>
<button onClick={() => setShowTemplateDrawer(false)} className="text-[#555] hover:text-white transition-colors"> <button onClick={() => setShowTemplateDrawer(false)} className="text-[#555] hover:text-white transition-colors">
@@ -820,12 +820,12 @@ export default function AdminNewsletterPage() {
<div className="overflow-y-auto flex-1 p-4"> <div className="overflow-y-auto flex-1 p-4">
{loadingTemplates ? ( {loadingTemplates ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : templates.length === 0 ? ( ) : templates.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-[#555] font-body text-sm">No templates found.</p> <p className="text-[#555] font-body text-sm">No templates found.</p>
<Link href="/admin/newsletter/templates" className="text-[#3b82f6] text-sm font-body hover:text-blue-400 mt-2 inline-block"> <Link href="/admin/newsletter/templates" className="text-[#1D4ED8] text-sm font-body hover:text-blue-400 mt-2 inline-block">
Create your first template Create your first template
</Link> </Link>
</div> </div>
@@ -835,7 +835,7 @@ export default function AdminNewsletterPage() {
<button <button
key={tpl.id} key={tpl.id}
onClick={() => applyTemplate(tpl)} onClick={() => applyTemplate(tpl)}
className="flex items-start gap-3 p-4 bg-[#0d0d0d] border border-[#252525] rounded-xl text-left hover:border-[#3b82f6] hover:bg-[#1a2535] transition-colors group" className="flex items-start gap-3 p-4 bg-[#F8FAFC] border border-[#DCE6F2] rounded-xl text-left hover:border-[#1D4ED8] hover:bg-[#FFFFFF] transition-colors group"
> >
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5" style={{ backgroundColor: tpl.accent_color + '20' }}> <div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5" style={{ backgroundColor: tpl.accent_color + '20' }}>
<span style={{ color: tpl.accent_color }}> <span style={{ color: tpl.accent_color }}>
@@ -851,8 +851,8 @@ export default function AdminNewsletterPage() {
</div> </div>
{tpl.description && <p className="text-xs text-[#777] font-body line-clamp-1">{tpl.description}</p>} {tpl.description && <p className="text-xs text-[#777] font-body line-clamp-1">{tpl.description}</p>}
<div className="flex gap-1.5 mt-1.5"> <div className="flex gap-1.5 mt-1.5">
<span className="text-xs bg-[#1a1a1a] text-[#888] px-1.5 py-0.5 rounded font-body capitalize">{tpl.layout}</span> <span className="text-xs bg-[#FFFFFF] text-[#888] px-1.5 py-0.5 rounded font-body capitalize">{tpl.layout}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-1.5 py-0.5 rounded font-body">{tpl.article_blocks?.length || 0} slots</span> <span className="text-xs bg-[#FFFFFF] text-[#888] px-1.5 py-0.5 rounded font-body">{tpl.article_blocks?.length || 0} slots</span>
</div> </div>
</div> </div>
</button> </button>

View File

@@ -42,7 +42,7 @@ const STYLE_OPTIONS = [
]; ];
const ACCENT_COLORS = [ const ACCENT_COLORS = [
'#3b82f6', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#f97316', '#1D4ED8', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#f97316',
]; ];
const LAYOUT_ICONS: Record<string, React.ReactNode> = { const LAYOUT_ICONS: Record<string, React.ReactNode> = {
@@ -83,7 +83,7 @@ const emptyForm = (): Omit<NewsletterTemplate, 'id' | 'is_preset' | 'created_at'
subject_prefix: '', subject_prefix: '',
header_text: '', header_text: '',
footer_text: '', footer_text: '',
accent_color: '#3b82f6', accent_color: '#1D4ED8',
article_blocks: [{ title: '', excerpt: '', url: '', category: '' }], article_blocks: [{ title: '', excerpt: '', url: '', category: '' }],
}); });
@@ -226,7 +226,7 @@ export default function NewsletterTemplatesPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -242,7 +242,7 @@ export default function NewsletterTemplatesPage() {
)} )}
{/* Header */} {/* Header */}
<div className="bg-[#111] border-b border-[#252525] px-6 py-4"> <div className="bg-[#111] border-b border-[#DCE6F2] px-6 py-4">
<div className="max-w-6xl mx-auto flex items-center justify-between"> <div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin/newsletter" className="text-[#555] hover:text-[#888] transition-colors"> <Link href="/admin/newsletter" className="text-[#555] hover:text-[#888] transition-colors">
@@ -272,11 +272,11 @@ export default function NewsletterTemplatesPage() {
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<h2 className="font-display font-bold text-white text-base">Preset Templates</h2> <h2 className="font-display font-bold text-white text-base">Preset Templates</h2>
<span className="text-xs bg-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{presets.length}</span> <span className="text-xs bg-[#FFFFFF] text-[#1D4ED8] px-2 py-0.5 rounded-full font-body">{presets.length}</span>
</div> </div>
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@@ -298,15 +298,15 @@ export default function NewsletterTemplatesPage() {
<div> <div>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<h2 className="font-display font-bold text-white text-base">Custom Templates</h2> <h2 className="font-display font-bold text-white text-base">Custom Templates</h2>
<span className="text-xs bg-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{custom.length}</span> <span className="text-xs bg-[#FFFFFF] text-[#1D4ED8] px-2 py-0.5 rounded-full font-body">{custom.length}</span>
</div> </div>
{!loadingData && custom.length === 0 ? ( {!loadingData && custom.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-10 text-center"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-10 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"> <svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg> </svg>
<p className="text-[#555] font-body text-sm mb-3">No custom templates yet</p> <p className="text-[#555] font-body text-sm mb-3">No custom templates yet</p>
<button onClick={openNew} className="text-[#3b82f6] text-sm font-body font-semibold hover:text-blue-400 transition-colors"> <button onClick={openNew} className="text-[#1D4ED8] text-sm font-body font-semibold hover:text-blue-400 transition-colors">
Create your first template Create your first template
</button> </button>
</div> </div>
@@ -330,8 +330,8 @@ export default function NewsletterTemplatesPage() {
{/* Template Form Modal */} {/* Template Form Modal */}
{showForm && ( {showForm && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-start justify-center overflow-y-auto py-8 px-4"> <div className="fixed inset-0 z-50 bg-black/70 flex items-start justify-center overflow-y-auto py-8 px-4">
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-2xl"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl w-full max-w-2xl">
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525]"> <div className="flex items-center justify-between px-6 py-4 border-b border-[#DCE6F2]">
<h2 className="font-display font-bold text-white text-base"> <h2 className="font-display font-bold text-white text-base">
{editingId ? 'Edit Template' : 'New Template'} {editingId ? 'Edit Template' : 'New Template'}
</h2> </h2>
@@ -376,8 +376,8 @@ export default function NewsletterTemplatesPage() {
key={opt.value} key={opt.value}
type="button" type="button"
onClick={() => setForm((f) => ({ ...f, layout: opt.value as NewsletterTemplate['layout'] }))} onClick={() => setForm((f) => ({ ...f, layout: opt.value as NewsletterTemplate['layout'] }))}
className={`flex items-start gap-3 p-3 rounded-lg border text-left transition-colors ${form.layout === opt.value ? 'border-[#3b82f6] bg-[#1a2535]' : 'border-[#252525] bg-[#0d0d0d] hover:border-[#333]'}`}> className={`flex items-start gap-3 p-3 rounded-lg border text-left transition-colors ${form.layout === opt.value ? 'border-[#1D4ED8] bg-[#FFFFFF]' : 'border-[#DCE6F2] bg-[#F8FAFC] hover:border-[#333]'}`}>
<span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#3b82f6]' : 'text-[#555]'}`}> <span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#1D4ED8]' : 'text-[#555]'}`}>
{LAYOUT_ICONS[opt.value]} {LAYOUT_ICONS[opt.value]}
</span> </span>
<div> <div>
@@ -398,7 +398,7 @@ export default function NewsletterTemplatesPage() {
key={opt.value} key={opt.value}
type="button" type="button"
onClick={() => setForm((f) => ({ ...f, style: opt.value as NewsletterTemplate['style'] }))} onClick={() => setForm((f) => ({ ...f, style: opt.value as NewsletterTemplate['style'] }))}
className={`flex-1 py-2.5 px-3 rounded-lg border text-sm font-body font-semibold transition-colors ${form.style === opt.value ? 'border-[#3b82f6] bg-[#1a2535] text-white' : 'border-[#252525] bg-[#0d0d0d] text-[#888] hover:border-[#333]'}`}> className={`flex-1 py-2.5 px-3 rounded-lg border text-sm font-body font-semibold transition-colors ${form.style === opt.value ? 'border-[#1D4ED8] bg-[#FFFFFF] text-white' : 'border-[#DCE6F2] bg-[#F8FAFC] text-[#888] hover:border-[#333]'}`}>
{opt.label} {opt.label}
</button> </button>
))} ))}
@@ -469,14 +469,14 @@ export default function NewsletterTemplatesPage() {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<label className="text-xs font-body font-bold text-[#888] uppercase tracking-wider">Article Block Slots</label> <label className="text-xs font-body font-bold text-[#888] uppercase tracking-wider">Article Block Slots</label>
<button type="button" onClick={addBlock} className="text-xs text-[#3b82f6] hover:text-blue-400 font-body font-semibold transition-colors"> <button type="button" onClick={addBlock} className="text-xs text-[#1D4ED8] hover:text-blue-400 font-body font-semibold transition-colors">
+ Add Slot + Add Slot
</button> </button>
</div> </div>
<p className="text-xs text-[#555] font-body mb-3">Define placeholder slots for articles. These will be pre-filled when using the template in Send Digest.</p> <p className="text-xs text-[#555] font-body mb-3">Define placeholder slots for articles. These will be pre-filled when using the template in Send Digest.</p>
<div className="space-y-2"> <div className="space-y-2">
{form.article_blocks.map((block, i) => ( {form.article_blocks.map((block, i) => (
<div key={i} className="bg-[#0d0d0d] border border-[#252525] rounded-lg p-3"> <div key={i} className="bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg p-3">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs font-body text-[#555]">Slot {i + 1}</span> <span className="text-xs font-body text-[#555]">Slot {i + 1}</span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -486,7 +486,7 @@ export default function NewsletterTemplatesPage() {
type="checkbox" type="checkbox"
checked={!!block.featured} checked={!!block.featured}
onChange={(e) => updateBlock(i, 'featured', e.target.checked)} onChange={(e) => updateBlock(i, 'featured', e.target.checked)}
className="w-3 h-3 accent-[#3b82f6]" className="w-3 h-3 accent-[#1D4ED8]"
/> />
<span className="text-xs font-body text-[#888]">Featured</span> <span className="text-xs font-body text-[#888]">Featured</span>
</label> </label>
@@ -527,7 +527,7 @@ export default function NewsletterTemplatesPage() {
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-[#252525]"> <div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-[#DCE6F2]">
<button onClick={() => setShowForm(false)} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white transition-colors"> <button onClick={() => setShowForm(false)} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white transition-colors">
Cancel Cancel
</button> </button>
@@ -545,8 +545,8 @@ export default function NewsletterTemplatesPage() {
{/* Preview Modal */} {/* Preview Modal */}
{previewTemplate && ( {previewTemplate && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4" onClick={() => setPreviewTemplate(null)}> <div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4" onClick={() => setPreviewTemplate(null)}>
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-lg" onClick={(e) => e.stopPropagation()}> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl w-full max-w-lg" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525]"> <div className="flex items-center justify-between px-6 py-4 border-b border-[#DCE6F2]">
<h2 className="font-display font-bold text-white text-base">Template Preview</h2> <h2 className="font-display font-bold text-white text-base">Template Preview</h2>
<button onClick={() => setPreviewTemplate(null)} className="text-[#555] hover:text-white transition-colors"> <button onClick={() => setPreviewTemplate(null)} className="text-[#555] hover:text-white transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -565,27 +565,27 @@ export default function NewsletterTemplatesPage() {
</div> </div>
</div> </div>
<div className="grid grid-cols-3 gap-3 text-center"> <div className="grid grid-cols-3 gap-3 text-center">
<div className="bg-[#0d0d0d] rounded-lg p-3"> <div className="bg-[#F8FAFC] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Layout</p> <p className="text-xs text-[#555] font-body mb-1">Layout</p>
<p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.layout}</p> <p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.layout}</p>
</div> </div>
<div className="bg-[#0d0d0d] rounded-lg p-3"> <div className="bg-[#F8FAFC] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Style</p> <p className="text-xs text-[#555] font-body mb-1">Style</p>
<p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.style}</p> <p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.style}</p>
</div> </div>
<div className="bg-[#0d0d0d] rounded-lg p-3"> <div className="bg-[#F8FAFC] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Slots</p> <p className="text-xs text-[#555] font-body mb-1">Slots</p>
<p className="text-sm font-body font-semibold text-white">{previewTemplate.article_blocks?.length || 0}</p> <p className="text-sm font-body font-semibold text-white">{previewTemplate.article_blocks?.length || 0}</p>
</div> </div>
</div> </div>
{previewTemplate.subject_prefix && ( {previewTemplate.subject_prefix && (
<div className="bg-[#0d0d0d] rounded-lg p-3"> <div className="bg-[#F8FAFC] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Subject Prefix</p> <p className="text-xs text-[#555] font-body mb-1">Subject Prefix</p>
<p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.subject_prefix}</p> <p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.subject_prefix}</p>
</div> </div>
)} )}
{previewTemplate.header_text && ( {previewTemplate.header_text && (
<div className="bg-[#0d0d0d] rounded-lg p-3"> <div className="bg-[#F8FAFC] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Header Text</p> <p className="text-xs text-[#555] font-body mb-1">Header Text</p>
<p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.header_text}</p> <p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.header_text}</p>
</div> </div>
@@ -595,9 +595,9 @@ export default function NewsletterTemplatesPage() {
<p className="text-xs text-[#555] font-body mb-2">Article Slots ({previewTemplate.article_blocks.length})</p> <p className="text-xs text-[#555] font-body mb-2">Article Slots ({previewTemplate.article_blocks.length})</p>
<div className="space-y-1.5"> <div className="space-y-1.5">
{previewTemplate.article_blocks.map((b, i) => ( {previewTemplate.article_blocks.map((b, i) => (
<div key={i} className="flex items-center gap-2 bg-[#0d0d0d] rounded px-3 py-2"> <div key={i} className="flex items-center gap-2 bg-[#F8FAFC] rounded px-3 py-2">
<span className="text-xs text-[#555] font-body w-12 flex-shrink-0">Slot {i + 1}</span> <span className="text-xs text-[#555] font-body w-12 flex-shrink-0">Slot {i + 1}</span>
{b.featured && <span className="text-xs text-[#3b82f6] font-body bg-[#1a2535] px-1.5 py-0.5 rounded">Featured</span>} {b.featured && <span className="text-xs text-[#1D4ED8] font-body bg-[#FFFFFF] px-1.5 py-0.5 rounded">Featured</span>}
{b.category && <span className="text-xs text-[#888] font-body">{b.category}</span>} {b.category && <span className="text-xs text-[#888] font-body">{b.category}</span>}
{b.title && <span className="text-xs text-[#e0e0e0] font-body truncate">{b.title}</span>} {b.title && <span className="text-xs text-[#e0e0e0] font-body truncate">{b.title}</span>}
</div> </div>
@@ -614,7 +614,7 @@ export default function NewsletterTemplatesPage() {
{!previewTemplate.is_preset && ( {!previewTemplate.is_preset && (
<button <button
onClick={() => { setPreviewTemplate(null); openEdit(previewTemplate); }} onClick={() => { setPreviewTemplate(null); openEdit(previewTemplate); }}
className="px-4 py-2 text-sm font-body text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors"> className="px-4 py-2 text-sm font-body text-[#888] hover:text-white border border-[#DCE6F2] rounded-lg transition-colors">
Edit Edit
</button> </button>
)} )}
@@ -637,7 +637,7 @@ interface TemplateCardProps {
function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: TemplateCardProps) { function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: TemplateCardProps) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-xl overflow-hidden hover:border-[#333] transition-colors group"> <div className="bg-[#111] border border-[#DCE6F2] rounded-xl overflow-hidden hover:border-[#333] transition-colors group">
{/* Color bar */} {/* Color bar */}
<div className="h-1" style={{ backgroundColor: template.accent_color }} /> <div className="h-1" style={{ backgroundColor: template.accent_color }} />
<div className="p-4"> <div className="p-4">
@@ -660,27 +660,27 @@ function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: Tem
)} )}
<div className="flex flex-wrap gap-1.5 mb-4"> <div className="flex flex-wrap gap-1.5 mb-4">
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.layout}</span> <span className="text-xs bg-[#FFFFFF] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.layout}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.style}</span> <span className="text-xs bg-[#FFFFFF] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.style}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body">{template.article_blocks?.length || 0} slots</span> <span className="text-xs bg-[#FFFFFF] text-[#888] px-2 py-0.5 rounded-full font-body">{template.article_blocks?.length || 0} slots</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => onPreview(template)} onClick={() => onPreview(template)}
className="flex-1 py-1.5 text-xs font-body font-semibold text-[#3b82f6] border border-[#1e3a5f] rounded-lg hover:bg-[#1a2535] transition-colors"> className="flex-1 py-1.5 text-xs font-body font-semibold text-[#1D4ED8] border border-[#DCE6F2] rounded-lg hover:bg-[#FFFFFF] transition-colors">
Preview Preview
</button> </button>
<Link <Link
href={`/admin/newsletter?template=${template.id}`} href={`/admin/newsletter?template=${template.id}`}
className="flex-1 py-1.5 text-xs font-body font-semibold text-center text-white bg-[#1a2535] border border-[#1e3a5f] rounded-lg hover:bg-[#1e3a5f] transition-colors"> className="flex-1 py-1.5 text-xs font-body font-semibold text-center text-white bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg hover:bg-[#DCE6F2] transition-colors">
Use Use
</Link> </Link>
{!template.is_preset && ( {!template.is_preset && (
<> <>
<button <button
onClick={() => onEdit(template)} onClick={() => onEdit(template)}
className="p-1.5 text-[#555] hover:text-white border border-[#252525] rounded-lg transition-colors"> className="p-1.5 text-[#555] hover:text-white border border-[#DCE6F2] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125" /> <path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125" />
</svg> </svg>
@@ -688,7 +688,7 @@ function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: Tem
<button <button
onClick={() => onDelete(template.id)} onClick={() => onDelete(template.id)}
disabled={deletingId === template.id} disabled={deletingId === template.id}
className="p-1.5 text-[#555] hover:text-red-400 border border-[#252525] rounded-lg transition-colors disabled:opacity-50"> className="p-1.5 text-[#555] hover:text-red-400 border border-[#DCE6F2] rounded-lg transition-colors disabled:opacity-50">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /> <path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg> </svg>

View File

@@ -166,7 +166,7 @@ function StatCard({ label, value, icon, color, active, onClick }: StatCardProps)
className={`w-full text-left p-4 rounded-lg border transition-all ${ className={`w-full text-left p-4 rounded-lg border transition-all ${
active active
? `${color} border-current/30` ? `${color} border-current/30`
: 'bg-[#111] border-[#252525] hover:border-[#333]' : 'bg-[#111] border-[#DCE6F2] hover:border-[#333]'
}`} }`}
> >
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
@@ -191,7 +191,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
return ( return (
<div <div
className={`relative p-4 rounded-lg border transition-all ${ className={`relative p-4 rounded-lg border transition-all ${
n.read ? 'bg-[#0d0d0d] border-[#1e1e1e]' : `${cfg.bg} ${cfg.border}` n.read ? 'bg-[#F8FAFC] border-[#1e1e1e]' : `${cfg.bg} ${cfg.border}`
}`} }`}
> >
{/* Unread dot */} {/* Unread dot */}
@@ -202,7 +202,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
{/* Icon */} {/* Icon */}
<div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ <div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
n.read ? 'bg-[#1a1a1a]' : cfg.badge n.read ? 'bg-[#FFFFFF]' : cfg.badge
}`}> }`}>
<TypeIcon type={n.type} /> <TypeIcon type={n.type} />
</div> </div>
@@ -244,7 +244,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
{n.actionUrl && ( {n.actionUrl && (
<Link <Link
href={n.actionUrl} href={n.actionUrl}
className="text-xs font-body font-bold text-[#3b82f6] hover:text-blue-300 transition-colors" className="text-xs font-body font-bold text-[#1D4ED8] hover:text-blue-300 transition-colors"
> >
{n.actionLabel || 'View →'} {n.actionLabel || 'View →'}
</Link> </Link>
@@ -474,7 +474,7 @@ export default function NotificationsPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin mx-auto mb-3" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin mx-auto mb-3" />
<p className="text-[#555] text-sm font-body">Loading notifications</p> <p className="text-[#555] text-sm font-body">Loading notifications</p>
</div> </div>
</div> </div>
@@ -512,7 +512,7 @@ export default function NotificationsPage() {
{stats.unread > 0 && ( {stats.unread > 0 && (
<button <button
onClick={handleMarkAllRead} onClick={handleMarkAllRead}
className="px-3 py-1.5 text-xs font-body font-bold text-[#3b82f6] border border-[#3b82f6]/30 rounded hover:bg-[#3b82f6]/10 transition-colors" className="px-3 py-1.5 text-xs font-body font-bold text-[#1D4ED8] border border-[#1D4ED8]/30 rounded hover:bg-[#1D4ED8]/10 transition-colors"
> >
Mark all read Mark all read
</button> </button>
@@ -520,7 +520,7 @@ export default function NotificationsPage() {
<button <button
onClick={handleRefresh} onClick={handleRefresh}
disabled={refreshing} disabled={refreshing}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body text-[#888] border border-[#252525] rounded hover:border-[#333] hover:text-white transition-colors disabled:opacity-50" className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body text-[#888] border border-[#DCE6F2] rounded hover:border-[#333] hover:text-white transition-colors disabled:opacity-50"
> >
<RefreshIcon className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin' : ''}`} /> <RefreshIcon className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin' : ''}`} />
Refresh Refresh
@@ -558,7 +558,7 @@ export default function NotificationsPage() {
label="Unread" label="Unread"
value={stats.unread} value={stats.unread}
icon={<BellIcon className="w-4 h-4" />} icon={<BellIcon className="w-4 h-4" />}
color="text-[#3b82f6] bg-[#3b82f6]/5 border-[#3b82f6]/30" color="text-[#1D4ED8] bg-[#1D4ED8]/5 border-[#1D4ED8]/30"
active={filter === 'unread'} active={filter === 'unread'}
onClick={() => setFilter(filter === 'unread' ? 'all' : 'unread')} onClick={() => setFilter(filter === 'unread' ? 'all' : 'unread')}
/> />
@@ -580,7 +580,7 @@ export default function NotificationsPage() {
onClick={() => setFilter(f)} onClick={() => setFilter(f)}
className={`px-3 py-1.5 text-xs font-body font-bold rounded transition-colors ${ className={`px-3 py-1.5 text-xs font-body font-bold rounded transition-colors ${
filter === f filter === f
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'text-[#555] hover:text-[#888] hover:bg-[#111]' : 'text-[#555] hover:text-[#888] hover:bg-[#111]'
}`} }`}
> >

View File

@@ -4,6 +4,7 @@ import React, { useState, useEffect, useCallback } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import AdminShell from '@/components/admin/AdminShell';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
@@ -44,8 +45,8 @@ function timeAgo(dateStr: string): string {
const STATUS_COLORS: Record<string, string> = { const STATUS_COLORS: Record<string, string> = {
published: 'text-green-400 bg-green-400/10', published: 'text-green-400 bg-green-400/10',
pending: 'text-yellow-400 bg-yellow-400/10', pending: 'text-yellow-400 bg-yellow-400/10',
draft: 'text-[#888] bg-[#1a1a1a]', draft: 'text-[#888] bg-[#FFFFFF]',
archived: 'text-[#555] bg-[#1a1a1a]', archived: 'text-[#555] bg-[#FFFFFF]',
imported: 'text-blue-400 bg-blue-400/10', imported: 'text-blue-400 bg-blue-400/10',
}; };
@@ -91,7 +92,7 @@ interface MetricCardProps {
function MetricCard({ label, value, sub, accent, icon, href, badge }: MetricCardProps) { function MetricCard({ label, value, sub, accent, icon, href, badge }: MetricCardProps) {
const content = ( const content = (
<div className={`bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors ${href ? 'cursor-pointer' : ''}`}> <div className={`bg-[#111] border border-[#DCE6F2] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors ${href ? 'cursor-pointer' : ''}`}>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${accent}`}> <div className={`w-10 h-10 rounded-lg flex items-center justify-center ${accent}`}>
{icon} {icon}
@@ -125,15 +126,15 @@ interface QuickActionProps {
function QuickAction({ label, description, href, icon, accent }: QuickActionProps) { function QuickAction({ label, description, href, icon, accent }: QuickActionProps) {
return ( return (
<Link href={href} className="flex items-center gap-3 p-3 bg-[#111] border border-[#252525] rounded-lg hover:border-[#3b82f6] hover:bg-[#0d1a2d] transition-all group"> <Link href={href} className="flex items-center gap-3 p-3 bg-[#111] border border-[#DCE6F2] rounded-lg hover:border-[#1D4ED8] hover:bg-[#0d1a2d] transition-all group">
<div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${accent} group-hover:scale-105 transition-transform`}> <div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${accent} group-hover:scale-105 transition-transform`}>
{icon} {icon}
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-body font-semibold text-white group-hover:text-[#3b82f6] transition-colors">{label}</p> <p className="text-sm font-body font-semibold text-white group-hover:text-[#1D4ED8] transition-colors">{label}</p>
<p className="text-xs text-[#555] font-body truncate">{description}</p> <p className="text-xs text-[#555] font-body truncate">{description}</p>
</div> </div>
<svg className="w-4 h-4 text-[#444] group-hover:text-[#3b82f6] ml-auto flex-shrink-0 transition-colors" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4 text-[#444] group-hover:text-[#1D4ED8] ml-auto flex-shrink-0 transition-colors" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg> </svg>
</Link> </Link>
@@ -239,7 +240,7 @@ export default function AdminDashboardPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading dashboard</p> <p className="text-[#555] text-sm font-body">Loading dashboard</p>
</div> </div>
</div> </div>
@@ -249,6 +250,7 @@ export default function AdminDashboardPage() {
if (!user) return null; if (!user) return null;
return ( return (
<AdminShell title="Dashboard">
<div className="min-h-screen bg-[#0a0a0a]"> <div className="min-h-screen bg-[#0a0a0a]">
<div className="max-w-7xl mx-auto px-4 py-8"> <div className="max-w-7xl mx-auto px-4 py-8">
@@ -256,7 +258,7 @@ export default function AdminDashboardPage() {
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-display font-bold text-white">Admin Dashboard</h1> <h1 className="text-2xl font-display font-bold text-white">Admin Dashboard</h1>
<p className="text-[#555] text-sm font-body mt-1">Overview of your broadcast platform</p> <p className="text-[#555] text-sm font-body mt-1">Overview of your pro AV platform</p>
</div> </div>
<div className="flex items-center gap-2 text-xs text-[#555] font-body"> <div className="flex items-center gap-2 text-xs text-[#555] font-body">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -345,7 +347,7 @@ export default function AdminDashboardPage() {
<MetricCard <MetricCard
label="Drafts" label="Drafts"
value={metrics.draftArticles} value={metrics.draftArticles}
accent="bg-[#1a1a1a]" accent="bg-[#FFFFFF]"
href="/admin/articles" href="/admin/articles"
icon={ icon={
<svg className="w-5 h-5 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -356,7 +358,7 @@ export default function AdminDashboardPage() {
<MetricCard <MetricCard
label="Archived" label="Archived"
value={metrics.archivedArticles} value={metrics.archivedArticles}
accent="bg-[#1a1a1a]" accent="bg-[#FFFFFF]"
href="/admin/articles" href="/admin/articles"
icon={ icon={
<svg className="w-5 h-5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -372,20 +374,20 @@ export default function AdminDashboardPage() {
{/* Recent Activity Feed */} {/* Recent Activity Feed */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-5 py-4 border-b border-[#FFFFFF] flex items-center justify-between">
<h2 className="text-sm font-display font-bold text-white">Recent Activity</h2> <h2 className="text-sm font-display font-bold text-white">Recent Activity</h2>
<Link href="/admin/articles" className="text-xs text-[#3b82f6] hover:text-blue-300 font-body transition-colors">View all </Link> <Link href="/admin/articles" className="text-xs text-[#1D4ED8] hover:text-blue-300 font-body transition-colors">View all </Link>
</div> </div>
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#FFFFFF]">
{recentActivity.length === 0 ? ( {recentActivity.length === 0 ? (
<div className="px-5 py-10 text-center"> <div className="px-5 py-10 text-center">
<p className="text-[#555] text-sm font-body">No recent activity found</p> <p className="text-[#555] text-sm font-body">No recent activity found</p>
</div> </div>
) : ( ) : (
recentActivity.map((item) => ( recentActivity.map((item) => (
<div key={`${item.source}-${item.id}`} className="px-5 py-3 flex items-start gap-3 hover:bg-[#0d0d0d] transition-colors"> <div key={`${item.source}-${item.id}`} className="px-5 py-3 flex items-start gap-3 hover:bg-[#F8FAFC] transition-colors">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 mt-0.5"> <div className="w-6 h-6 rounded-full bg-[#FFFFFF] flex items-center justify-center flex-shrink-0 mt-0.5">
{ACTIVITY_ICONS[item.type] || ACTIVITY_ICONS['draft']} {ACTIVITY_ICONS[item.type] || ACTIVITY_ICONS['draft']}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -412,8 +414,8 @@ export default function AdminDashboardPage() {
{/* Quick Actions */} {/* Quick Actions */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a]"> <div className="px-5 py-4 border-b border-[#FFFFFF]">
<h2 className="text-sm font-display font-bold text-white">Quick Actions</h2> <h2 className="text-sm font-display font-bold text-white">Quick Actions</h2>
</div> </div>
<div className="p-4 flex flex-col gap-2"> <div className="p-4 flex flex-col gap-2">
@@ -587,21 +589,21 @@ export default function AdminDashboardPage() {
</div> </div>
{/* Status Summary Mini-Card */} {/* Status Summary Mini-Card */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4">
<h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Content Status</h3> <h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Content Status</h3>
<div className="space-y-2"> <div className="space-y-2">
{[ {[
{ label: 'Published', value: metrics.publishedArticles, color: 'bg-green-400', total: metrics.totalArticles }, { label: 'Published', value: metrics.publishedArticles, color: 'bg-green-400', total: metrics.totalArticles },
{ label: 'Pending', value: metrics.pendingApprovals, color: 'bg-yellow-400', total: metrics.totalArticles }, { label: 'Pending', value: metrics.pendingApprovals, color: 'bg-yellow-400', total: metrics.totalArticles },
{ label: 'Draft', value: metrics.draftArticles, color: 'bg-[#444]', total: metrics.totalArticles }, { label: 'Draft', value: metrics.draftArticles, color: 'bg-[#444]', total: metrics.totalArticles },
{ label: 'Archived', value: metrics.archivedArticles, color: 'bg-[#2a2a2a]', total: metrics.totalArticles }, { label: 'Archived', value: metrics.archivedArticles, color: 'bg-[#DCE6F2]', total: metrics.totalArticles },
].map(({ label, value, color, total }) => ( ].map(({ label, value, color, total }) => (
<div key={label}> <div key={label}>
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className="text-xs text-[#888] font-body">{label}</span> <span className="text-xs text-[#888] font-body">{label}</span>
<span className="text-xs text-white font-body font-semibold">{value}</span> <span className="text-xs text-white font-body font-semibold">{value}</span>
</div> </div>
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden"> <div className="h-1.5 bg-[#FFFFFF] rounded-full overflow-hidden">
<div <div
className={`h-full rounded-full ${color} transition-all duration-500`} className={`h-full rounded-full ${color} transition-all duration-500`}
style={{ width: total > 0 ? `${Math.round((value / total) * 100)}%` : '0%' }} style={{ width: total > 0 ? `${Math.round((value / total) * 100)}%` : '0%' }}
@@ -615,5 +617,6 @@ export default function AdminDashboardPage() {
</div> </div>
</div> </div>
</div> </div>
</AdminShell>
); );
} }

View File

@@ -0,0 +1,269 @@
"use client";
/**
* /admin/promote/[slug]
*
* Per-article promote sheet. Opens a single screen with one button per
* social platform. Each button uses that platform's standard share URL
* (LinkedIn Sharing, X Web Intent, Facebook Sharer) — no OAuth, no API
* tokens. On iPhone the button opens the platform's native app via the
* universal link.
*
* Instagram has no URL share API, so we render a "Copy caption + open IG"
* affordance: caption goes to clipboard, IG opens via instagram:// (falls
* back to web), and you paste in the composer.
*
* This page is the MVP. The follow-up commits will:
* • Add a distribute.social_posts table to track what was promoted
* • Replace each share URL with a server-side API POST once OAuth is wired
* • Add caption auto-generation (LLM summary of the article)
*
* Until then this works tonight from your phone.
*/
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { useAuth } from "@/contexts/AuthContext";
import AdminShell from "@/components/admin/AdminShell";
interface Article {
slug: string;
title: string;
excerpt: string | null;
url: string;
featured_image: string | null;
}
const SITE = "https://avbeat.com";
function strip(html: string | null | undefined, max = 260): string {
if (!html) return "";
const text = html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
return text.length > max ? text.slice(0, max - 1).trimEnd() + "…" : text;
}
function shareUrls(a: Article) {
const url = a.url;
const text = `${a.title}`;
const hashtags = "AVBeat,proAV,InfoComm";
return {
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
x: `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(url)}&hashtags=${encodeURIComponent(hashtags)}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
email: `mailto:?subject=${encodeURIComponent(text)}&body=${encodeURIComponent(strip(a.excerpt) + "\n\n" + url)}`,
};
}
export default function PromotePage() {
const params = useParams<{ slug: string }>();
const router = useRouter();
const { user, loading } = useAuth();
const [article, setArticle] = useState<Article | null>(null);
const [err, setErr] = useState<string | null>(null);
const [copied, setCopied] = useState<string | null>(null);
const [caption, setCaption] = useState<string>("");
useEffect(() => {
if (!loading && !user) {
router.push("/login?next=" + encodeURIComponent(window.location.pathname));
}
}, [user, loading, router]);
useEffect(() => {
if (!params?.slug) return;
// Hit the public posts API to get title/excerpt/image. Falls back to a
// stub if the article isn't surfaced there (e.g., draft, archived).
fetch(`/api/public/posts?slug=${encodeURIComponent(params.slug)}`)
.then((r) => r.ok ? r.json() : null)
.then((d) => {
const item = (d?.items || []).find((x: any) => x.slug === params.slug) || d?.item;
if (item) {
const url = `${SITE}/news/${item.slug}`;
const a: Article = {
slug: item.slug,
title: item.title || item.slug,
excerpt: item.excerpt || null,
url,
featured_image: item.image || item.featured_image || null,
};
setArticle(a);
setCaption(`${a.title}\n\n${strip(a.excerpt)}\n\n${a.url}`);
} else {
// Stub — minimum required to share by URL alone
const url = `${SITE}/news/${params.slug}`;
setArticle({ slug: params.slug, title: params.slug, excerpt: null, url, featured_image: null });
setCaption(`Check this out — ${url}`);
}
})
.catch((e) => setErr(e.message || "Failed to load article"));
}, [params?.slug]);
if (!user || !article) {
return (
<AdminShell title="Promote">
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center px-4">
{err ? (
<p className="text-red-400 text-sm">{err}</p>
) : (
<div className="flex flex-col items-center gap-3">
<div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading article</p>
</div>
)}
</div>
</AdminShell>
);
}
const urls = shareUrls(article);
function copyToClipboard(text: string, label: string) {
navigator.clipboard?.writeText(text)
.then(() => {
setCopied(label);
setTimeout(() => setCopied(null), 1500);
})
.catch(() => setErr("Couldn't copy. Long-press the caption box to copy manually."));
}
return (
<AdminShell title="Promote">
<main className="min-h-screen bg-[#0a0a0a]">
<div className="max-w-2xl mx-auto px-4 py-6">
{/* Article preview */}
<section className="bg-[#FFFFFF] border border-[#1a1f2a] rounded-lg overflow-hidden mb-5">
{article.featured_image && (
<img
src={article.featured_image}
alt=""
className="w-full h-40 object-cover"
/>
)}
<div className="p-4">
<p className="text-[10px] font-mono uppercase tracking-widest text-[#1D4ED8] mb-1">
Promoting
</p>
<h2 className="font-display text-lg font-bold text-white leading-tight">
{article.title}
</h2>
{article.excerpt && (
<p className="text-[#888] text-sm mt-2 line-clamp-3 font-body">
{strip(article.excerpt)}
</p>
)}
<a
href={article.url}
target="_blank"
rel="noopener noreferrer"
className="text-[#1D4ED8] text-xs font-mono mt-3 inline-block hover:underline break-all"
>
{article.url}
</a>
</div>
</section>
{/* Caption editor — copy-pastable for any platform */}
<section className="mb-6">
<label className="block text-[10px] font-mono uppercase tracking-widest text-[#1D4ED8] mb-2">
Caption
</label>
<textarea
value={caption}
onChange={(e) => setCaption(e.target.value)}
rows={4}
className="w-full bg-[#FFFFFF] border border-[#1a1f2a] rounded-lg px-3 py-2 text-white text-sm font-body focus:outline-none focus:border-[#1D4ED8]"
placeholder="Caption shown on platforms that accept one…"
/>
<button
type="button"
onClick={() => copyToClipboard(caption, "caption")}
className="mt-2 text-xs text-[#1D4ED8] hover:underline font-body"
>
{copied === "caption" ? "✓ Copied" : "Copy caption to clipboard"}
</button>
</section>
{/* Platform buttons */}
<section className="space-y-3 mb-6">
<a
href={urls.linkedin}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between w-full bg-[#0A66C2] hover:bg-[#004182] text-white font-body font-semibold text-sm rounded-lg px-4 py-3.5 transition-colors"
>
<span className="flex items-center gap-3">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.063 2.063 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
LinkedIn
</span>
<span className="text-xs opacity-80">Share</span>
</a>
<a
href={urls.x}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between w-full bg-black hover:bg-[#1a1a1a] text-white font-body font-semibold text-sm rounded-lg px-4 py-3.5 transition-colors border border-[#333]"
>
<span className="flex items-center gap-3">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
X (Twitter)
</span>
<span className="text-xs opacity-80">Post</span>
</a>
<a
href={urls.facebook}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between w-full bg-[#1877F2] hover:bg-[#0d65d9] text-white font-body font-semibold text-sm rounded-lg px-4 py-3.5 transition-colors"
>
<span className="flex items-center gap-3">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
Facebook
</span>
<span className="text-xs opacity-80">Share</span>
</a>
<button
type="button"
onClick={() => {
copyToClipboard(caption, "instagram");
window.location.href = "instagram://camera";
setTimeout(() => {
if (document.visibilityState === "visible") {
window.open("https://www.instagram.com/", "_blank");
}
}, 600);
}}
className="flex items-center justify-between w-full bg-gradient-to-r from-[#833ab4] via-[#fd1d1d] to-[#fcb045] hover:opacity-90 text-white font-body font-semibold text-sm rounded-lg px-4 py-3.5 transition-opacity"
>
<span className="flex items-center gap-3">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>
Instagram
</span>
<span className="text-xs opacity-80">
{copied === "instagram" ? "Caption copied · opening IG…" : "Copy + open"}
</span>
</button>
<a
href={urls.email}
className="flex items-center justify-between w-full bg-[#FFFFFF] hover:bg-[#1f2937] text-white font-body font-semibold text-sm rounded-lg px-4 py-3.5 transition-colors border border-[#222]"
>
<span className="flex items-center gap-3">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
Email
</span>
<span className="text-xs opacity-80">Compose</span>
</a>
</section>
<p className="text-xs text-[#555] font-body leading-relaxed">
Each button opens the platform's share sheet pre-filled with the
article URL and caption. Instagram has no URL-share API its
button copies the caption to your clipboard and opens the IG app
so you can paste it into a new post or story. One-tap server-side
posting via LinkedIn / X / Meta APIs is coming next; the buttons
above will stop opening external sheets and post silently
instead.
</p>
</div>
</main>
</AdminShell>
);
}

View File

@@ -41,7 +41,7 @@ export default function ReviewActions({ id, currentStatus }: Props) {
value={note} value={note}
onChange={(e) => setNote(e.target.value)} onChange={(e) => setNote(e.target.value)}
placeholder="Reviewer note (optional)" placeholder="Reviewer note (optional)"
className="w-full rounded border border-[#1f2937] bg-[#070a10] p-2 text-sm text-[#e5e7eb] focus:border-[#3b82f6] focus:outline-none" className="w-full rounded border border-[#1f2937] bg-[#070a10] p-2 text-sm text-[#e5e7eb] focus:border-[#1D4ED8] focus:outline-none"
rows={2} rows={2}
/> />
<div className="mt-3 flex flex-wrap gap-3 text-sm"> <div className="mt-3 flex flex-wrap gap-3 text-sm">

View File

@@ -41,12 +41,12 @@ export default async function ReviewDetailPage({ params }: { params: Promise<{ i
return ( return (
<main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]"> <main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]">
<Link href="/admin/review-queue" className="text-sm text-[#60a5fa] hover:underline"> <Link href="/admin/review-queue" className="text-sm text-[#1D4ED8] hover:underline">
Back to queue Back to queue
</Link> </Link>
<header className="mt-4 mb-6 border-b border-[#222] pb-4"> <header className="mt-4 mb-6 border-b border-[#222] pb-4">
<div className="flex items-center gap-3 text-xs uppercase tracking-wider text-[#60a5fa]"> <div className="flex items-center gap-3 text-xs uppercase tracking-wider text-[#1D4ED8]">
<span>{persona?.name || "—"}</span> <span>{persona?.name || "—"}</span>
<span className="text-[#4b5563]">·</span> <span className="text-[#4b5563]">·</span>
<span>{art.category}</span> <span>{art.category}</span>

View File

@@ -70,7 +70,7 @@ export default async function ReviewQueuePage() {
return ( return (
<li <li
key={r.id} key={r.id}
className="rounded border border-[#1f2937] bg-[#0b0f17] p-5 hover:border-[#3b82f6] transition-colors" className="rounded border border-[#1f2937] bg-[#0b0f17] p-5 hover:border-[#1D4ED8] transition-colors"
> >
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
{persona?.avatar_url ? ( {persona?.avatar_url ? (
@@ -85,7 +85,7 @@ export default async function ReviewQueuePage() {
<div className="h-14 w-14 rounded-full bg-[#1f2937]" /> <div className="h-14 w-14 rounded-full bg-[#1f2937]" />
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-xs uppercase tracking-wider text-[#60a5fa]"> <div className="text-xs uppercase tracking-wider text-[#1D4ED8]">
{persona?.name || "—"} · {r.category} {persona?.name || "—"} · {r.category}
</div> </div>
<h2 className="text-xl font-semibold mt-1">{r.title}</h2> <h2 className="text-xl font-semibold mt-1">{r.title}</h2>
@@ -100,7 +100,7 @@ export default async function ReviewQueuePage() {
<div className="mt-4 flex flex-wrap items-center gap-3 text-xs"> <div className="mt-4 flex flex-wrap items-center gap-3 text-xs">
<Link <Link
href={`/admin/review-queue/${r.id}`} href={`/admin/review-queue/${r.id}`}
className="rounded bg-[#1d4ed8] px-3 py-1.5 font-medium text-white hover:bg-[#2563eb]" className="rounded bg-[#1d4ed8] px-3 py-1.5 font-medium text-white hover:bg-[#1E3A8A]"
> >
Review Review
</Link> </Link>

View File

@@ -12,7 +12,7 @@ export default function RmpHubPage() {
useEffect(() => { if (!loading && !user) router?.push('/login'); }, [user, loading, router]); useEffect(() => { if (!loading && !user) router?.push('/login'); }, [user, loading, router]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -68,7 +68,7 @@ export default function RmpHubPage() {
], ],
}, },
]?.map(section => ( ]?.map(section => (
<div key={section?.title} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <div key={section?.title} className="bg-[#111] border border-[#DCE6F2] rounded-lg p-4 space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-2xl">{section?.icon}</span> <span className="text-2xl">{section?.icon}</span>
<div> <div>
@@ -78,19 +78,19 @@ export default function RmpHubPage() {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
{section?.links?.map(l => ( {section?.links?.map(l => (
<Link key={l?.href} href={l?.href} className="block px-2 py-1 rounded text-xs text-[#888] hover:text-white hover:bg-[#1a1a1a] transition-colors"> <Link key={l?.href} href={l?.href} className="block px-2 py-1 rounded text-xs text-[#888] hover:text-white hover:bg-[#FFFFFF] transition-colors">
{l?.label} {l?.label}
</Link> </Link>
))} ))}
</div> </div>
<Link href={section?.href} className="block w-full text-center px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors"> <Link href={section?.href} className="block w-full text-center px-3 py-1.5 bg-[#FFFFFF] border border-[#2a3a50] rounded text-xs text-[#1D4ED8] hover:bg-[#0d1a2d] transition-colors">
Open {section?.title} Open {section?.title}
</Link> </Link>
</div> </div>
))} ))}
</div> </div>
<div className="bg-[#0d1117] border border-[#1a2535] rounded-lg p-4"> <div className="bg-[#0d1117] border border-[#FFFFFF] rounded-lg p-4">
<p className="text-xs text-[#555] mb-3">Quick Navigation</p> <p className="text-xs text-[#555] mb-3">Quick Navigation</p>
<RmpAdminNav /> <RmpAdminNav />
</div> </div>

View File

@@ -61,7 +61,7 @@ const ROLE_COLORS: Record<string, string> = {
editor: 'text-purple-400 bg-purple-400/10 border-purple-400/20', editor: 'text-purple-400 bg-purple-400/10 border-purple-400/20',
moderator: 'text-orange-400 bg-orange-400/10 border-orange-400/20', moderator: 'text-orange-400 bg-orange-400/10 border-orange-400/20',
contributor: 'text-blue-400 bg-blue-400/10 border-blue-400/20', contributor: 'text-blue-400 bg-blue-400/10 border-blue-400/20',
reader: 'text-[#888] bg-[#1a1a1a] border-[#333]', reader: 'text-[#888] bg-[#FFFFFF] border-[#333]',
}; };
const ROLE_ICONS: Record<string, React.ReactNode> = { const ROLE_ICONS: Record<string, React.ReactNode> = {
@@ -324,7 +324,7 @@ export default function AdminRolesPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading roles panel</p> <p className="text-[#555] text-sm font-body">Loading roles panel</p>
</div> </div>
</div> </div>
@@ -376,7 +376,7 @@ export default function AdminRolesPage() {
key={role} key={role}
onClick={() => { setSelectedRole(role === selectedRole ? 'all' : role); setActiveTab('assign'); }} onClick={() => { setSelectedRole(role === selectedRole ? 'all' : role); setActiveTab('assign'); }}
className={`bg-[#111] border rounded-lg p-4 text-left hover:border-[#333] transition-all ${ className={`bg-[#111] border rounded-lg p-4 text-left hover:border-[#333] transition-all ${
selectedRole === role ? 'border-[#3b82f6] bg-[#0d1a2d]' : 'border-[#252525]' selectedRole === role ? 'border-[#1D4ED8] bg-[#0d1a2d]' : 'border-[#DCE6F2]'
}`} }`}
> >
<div className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-body font-semibold border mb-2 ${color}`}> <div className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-body font-semibold border mb-2 ${color}`}>
@@ -391,7 +391,7 @@ export default function AdminRolesPage() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit"> <div className="flex gap-1 mb-6 bg-[#111] border border-[#DCE6F2] rounded-lg p-1 w-fit">
{([ {([
{ id: 'assign', label: 'Assign Roles', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z' }, { id: 'assign', label: 'Assign Roles', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z' },
{ id: 'permissions', label: 'Permissions', icon: 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z' }, { id: 'permissions', label: 'Permissions', icon: 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z' },
@@ -402,7 +402,7 @@ export default function AdminRolesPage() {
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-body font-medium transition-all ${ className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-body font-medium transition-all ${
activeTab === tab.id activeTab === tab.id
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'text-[#888] hover:text-white' : 'text-[#888] hover:text-white'
}`} }`}
> >
@@ -416,9 +416,9 @@ export default function AdminRolesPage() {
{/* ── Tab: Assign Roles ── */} {/* ── Tab: Assign Roles ── */}
{activeTab === 'assign' && ( {activeTab === 'assign' && (
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
{/* Toolbar */} {/* Toolbar */}
<div className="px-5 py-4 border-b border-[#1a1a1a] flex flex-col sm:flex-row gap-3"> <div className="px-5 py-4 border-b border-[#FFFFFF] flex flex-col sm:flex-row gap-3">
<div className="relative flex-1"> <div className="relative flex-1">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
@@ -428,13 +428,13 @@ export default function AdminRolesPage() {
placeholder="Search users…" placeholder="Search users…"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#252525] rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder-[#555] font-body focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder-[#555] font-body focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<select <select
value={selectedRole} value={selectedRole}
onChange={(e) => setSelectedRole(e.target.value)} onChange={(e) => setSelectedRole(e.target.value)}
className="bg-[#0d0d0d] border border-[#252525] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#3b82f6]" className="bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#1D4ED8]"
> >
<option value="all">All Roles</option> <option value="all">All Roles</option>
{ROLES.map((r) => ( {ROLES.map((r) => (
@@ -447,7 +447,7 @@ export default function AdminRolesPage() {
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#FFFFFF]">
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">User</th> <th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">User</th>
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">Current Role</th> <th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">Current Role</th>
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider hidden sm:table-cell">Status</th> <th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider hidden sm:table-cell">Status</th>
@@ -455,7 +455,7 @@ export default function AdminRolesPage() {
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">Assign Role</th> <th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">Assign Role</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#1a1a1a]"> <tbody className="divide-y divide-[#FFFFFF]">
{filteredUsers.length === 0 ? ( {filteredUsers.length === 0 ? (
<tr> <tr>
<td colSpan={5} className="px-5 py-10 text-center text-[#555] text-sm font-body"> <td colSpan={5} className="px-5 py-10 text-center text-[#555] text-sm font-body">
@@ -464,10 +464,10 @@ export default function AdminRolesPage() {
</tr> </tr>
) : ( ) : (
filteredUsers.map((u) => ( filteredUsers.map((u) => (
<tr key={u.id} className="hover:bg-[#0d0d0d] transition-colors"> <tr key={u.id} className="hover:bg-[#F8FAFC] transition-colors">
<td className="px-5 py-3"> <td className="px-5 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 text-xs font-body font-bold text-[#888]"> <div className="w-8 h-8 rounded-full bg-[#FFFFFF] flex items-center justify-center flex-shrink-0 text-xs font-body font-bold text-[#888]">
{u.avatar_url {u.avatar_url
? <img src={u.avatar_url} alt={u.full_name} className="w-8 h-8 rounded-full object-cover" /> ? <img src={u.avatar_url} alt={u.full_name} className="w-8 h-8 rounded-full object-cover" />
: getInitials(u.full_name || u.email) : getInitials(u.full_name || u.email)
@@ -484,7 +484,7 @@ export default function AdminRolesPage() {
</td> </td>
<td className="px-5 py-3 hidden sm:table-cell"> <td className="px-5 py-3 hidden sm:table-cell">
<span className={`text-xs font-body font-semibold px-2 py-0.5 rounded-full ${ <span className={`text-xs font-body font-semibold px-2 py-0.5 rounded-full ${
u.is_active ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#1a1a1a]' u.is_active ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#FFFFFF]'
}`}> }`}>
{u.is_active ? 'Active' : 'Inactive'} {u.is_active ? 'Active' : 'Inactive'}
</span> </span>
@@ -500,14 +500,14 @@ export default function AdminRolesPage() {
defaultValue={u.role} defaultValue={u.role}
onChange={(e) => handleAssignRole(u.id, e.target.value)} onChange={(e) => handleAssignRole(u.id, e.target.value)}
disabled={processingUserId === u.id} disabled={processingUserId === u.id}
className="bg-[#0d0d0d] border border-[#252525] rounded-lg px-2 py-1.5 text-xs text-white font-body focus:outline-none focus:border-[#3b82f6] disabled:opacity-50" className="bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-2 py-1.5 text-xs text-white font-body focus:outline-none focus:border-[#1D4ED8] disabled:opacity-50"
> >
{ROLES.map((r) => ( {ROLES.map((r) => (
<option key={r} value={r} className="capitalize">{r.charAt(0).toUpperCase() + r.slice(1)}</option> <option key={r} value={r} className="capitalize">{r.charAt(0).toUpperCase() + r.slice(1)}</option>
))} ))}
</select> </select>
{processingUserId === u.id && ( {processingUserId === u.id && (
<div className="w-4 h-4 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
)} )}
</div> </div>
</td> </td>
@@ -519,7 +519,7 @@ export default function AdminRolesPage() {
</div> </div>
{filteredUsers.length > 0 && ( {filteredUsers.length > 0 && (
<div className="px-5 py-3 border-t border-[#1a1a1a]"> <div className="px-5 py-3 border-t border-[#FFFFFF]">
<p className="text-xs text-[#555] font-body">{filteredUsers.length} user{filteredUsers.length !== 1 ? 's' : ''} shown</p> <p className="text-xs text-[#555] font-body">{filteredUsers.length} user{filteredUsers.length !== 1 ? 's' : ''} shown</p>
</div> </div>
)} )}
@@ -535,9 +535,9 @@ export default function AdminRolesPage() {
const color = ROLE_COLORS[perm.role] || ROLE_COLORS['reader']; const color = ROLE_COLORS[perm.role] || ROLE_COLORS['reader'];
return ( return (
<div key={perm.role} className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div key={perm.role} className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
{/* Role Header */} {/* Role Header */}
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-5 py-4 border-b border-[#FFFFFF] flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-body font-semibold border ${color}`}> <div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-body font-semibold border ${color}`}>
{ROLE_ICONS[perm.role]} {ROLE_ICONS[perm.role]}
@@ -557,7 +557,7 @@ export default function AdminRolesPage() {
<button <button
onClick={handleSavePermissions} onClick={handleSavePermissions}
disabled={savingPermissions} disabled={savingPermissions}
className="px-3 py-1.5 text-xs font-body font-semibold bg-[#3b82f6] hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-50 flex items-center gap-1.5" className="px-3 py-1.5 text-xs font-body font-semibold bg-[#1D4ED8] hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-50 flex items-center gap-1.5"
> >
{savingPermissions && <div className="w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />} {savingPermissions && <div className="w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />}
Save Changes Save Changes
@@ -593,12 +593,12 @@ export default function AdminRolesPage() {
{isEditing ? ( {isEditing ? (
<button <button
onClick={() => setEditedPermissions((prev) => ({ ...prev, [key]: !val }))} onClick={() => setEditedPermissions((prev) => ({ ...prev, [key]: !val }))}
className={`relative w-9 h-5 rounded-full transition-colors ${val ? 'bg-[#3b82f6]' : 'bg-[#333]'}`} className={`relative w-9 h-5 rounded-full transition-colors ${val ? 'bg-[#1D4ED8]' : 'bg-[#333]'}`}
> >
<span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${val ? 'translate-x-4' : 'translate-x-0.5'}`} /> <span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${val ? 'translate-x-4' : 'translate-x-0.5'}`} />
</button> </button>
) : ( ) : (
<span className={`w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0 ${val ? 'bg-green-400/10' : 'bg-[#1a1a1a]'}`}> <span className={`w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0 ${val ? 'bg-green-400/10' : 'bg-[#FFFFFF]'}`}>
{val ? ( {val ? (
<svg className="w-3 h-3 text-green-400" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24"> <svg className="w-3 h-3 text-green-400" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
@@ -631,9 +631,9 @@ export default function AdminRolesPage() {
{/* ── Tab: Activity Logs ── */} {/* ── Tab: Activity Logs ── */}
{activeTab === 'activity' && ( {activeTab === 'activity' && (
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#DCE6F2] rounded-lg overflow-hidden">
{/* Toolbar */} {/* Toolbar */}
<div className="px-5 py-4 border-b border-[#1a1a1a] flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between"> <div className="px-5 py-4 border-b border-[#FFFFFF] flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between">
<h2 className="text-sm font-display font-bold text-white">Role-Based Activity</h2> <h2 className="text-sm font-display font-bold text-white">Role-Based Activity</h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-[#555] font-body">Filter by role:</span> <span className="text-xs text-[#555] font-body">Filter by role:</span>
@@ -644,8 +644,8 @@ export default function AdminRolesPage() {
onClick={() => setActivityRoleFilter(r)} onClick={() => setActivityRoleFilter(r)}
className={`px-2.5 py-1 rounded-md text-xs font-body font-medium transition-colors capitalize ${ className={`px-2.5 py-1 rounded-md text-xs font-body font-medium transition-colors capitalize ${
activityRoleFilter === r activityRoleFilter === r
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:text-white' : 'bg-[#FFFFFF] text-[#888] hover:text-white'
}`} }`}
> >
{r} {r}
@@ -656,7 +656,7 @@ export default function AdminRolesPage() {
</div> </div>
{/* Log List */} {/* Log List */}
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#FFFFFF]">
{activityLogs.length === 0 ? ( {activityLogs.length === 0 ? (
<div className="px-5 py-12 text-center"> <div className="px-5 py-12 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"> <svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
@@ -666,13 +666,13 @@ export default function AdminRolesPage() {
</div> </div>
) : ( ) : (
activityLogs.map((log) => { activityLogs.map((log) => {
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#1a1a1a]'; const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#FFFFFF]';
const actionLabel = ACTION_LABELS[log.action] || log.action.replace(/_/g, ' '); const actionLabel = ACTION_LABELS[log.action] || log.action.replace(/_/g, ' ');
const performer = log.user_profiles?.full_name || log.performed_by_email || 'System'; const performer = log.user_profiles?.full_name || log.performed_by_email || 'System';
const performerRole = log.user_profiles?.role; const performerRole = log.user_profiles?.role;
return ( return (
<div key={log.id} className="px-5 py-3.5 flex items-start gap-4 hover:bg-[#0d0d0d] transition-colors"> <div key={log.id} className="px-5 py-3.5 flex items-start gap-4 hover:bg-[#F8FAFC] transition-colors">
{/* Action badge */} {/* Action badge */}
<div className={`mt-0.5 px-2 py-0.5 rounded text-xs font-body font-semibold flex-shrink-0 ${actionColor}`}> <div className={`mt-0.5 px-2 py-0.5 rounded text-xs font-body font-semibold flex-shrink-0 ${actionColor}`}>
{actionLabel} {actionLabel}
@@ -720,7 +720,7 @@ export default function AdminRolesPage() {
</div> </div>
{activityLogs.length > 0 && ( {activityLogs.length > 0 && (
<div className="px-5 py-3 border-t border-[#1a1a1a]"> <div className="px-5 py-3 border-t border-[#FFFFFF]">
<p className="text-xs text-[#555] font-body">{activityLogs.length} log entries shown</p> <p className="text-xs text-[#555] font-body">{activityLogs.length} log entries shown</p>
</div> </div>
)} )}

View File

@@ -223,7 +223,7 @@ export default function AdminSuspensionsPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center"> <div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -288,7 +288,7 @@ export default function AdminSuspensionsPage() {
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${ className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${
activeTab === tab activeTab === tab
? 'text-[#3b82f6] border-[#3b82f6]' ? 'text-[#1D4ED8] border-[#1D4ED8]'
: 'text-[#555] border-transparent hover:text-[#888]' : 'text-[#555] border-transparent hover:text-[#888]'
}`} }`}
> >
@@ -304,7 +304,7 @@ export default function AdminSuspensionsPage() {
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : ( ) : (
<> <>
@@ -336,7 +336,7 @@ export default function AdminSuspensionsPage() {
<tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors"> <tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] font-bold text-sm flex-shrink-0 overflow-hidden"> <div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#1D4ED8] font-bold text-sm flex-shrink-0 overflow-hidden">
{s.user?.avatar_url ? ( {s.user?.avatar_url ? (
<img src={s.user.avatar_url} alt={s.user.full_name || s.user.email} className="w-full h-full object-cover" /> <img src={s.user.avatar_url} alt={s.user.full_name || s.user.email} className="w-full h-full object-cover" />
) : ( ) : (
@@ -415,7 +415,7 @@ export default function AdminSuspensionsPage() {
<tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors"> <tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] text-xs font-bold flex-shrink-0"> <div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#1D4ED8] text-xs font-bold flex-shrink-0">
{(s.user?.full_name?.[0] || s.user?.email?.[0] || '?').toUpperCase()} {(s.user?.full_name?.[0] || s.user?.email?.[0] || '?').toUpperCase()}
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
@@ -465,7 +465,7 @@ export default function AdminSuspensionsPage() {
placeholder="Search users..." placeholder="Search users..."
value={userSearch} value={userSearch}
onChange={(e) => setUserSearch(e.target.value)} onChange={(e) => setUserSearch(e.target.value)}
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
</div> </div>
@@ -488,7 +488,7 @@ export default function AdminSuspensionsPage() {
<tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors"> <tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] font-bold text-sm flex-shrink-0 overflow-hidden"> <div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#1D4ED8] font-bold text-sm flex-shrink-0 overflow-hidden">
{u.avatar_url ? ( {u.avatar_url ? (
<img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" /> <img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" />
) : ( ) : (
@@ -572,7 +572,7 @@ export default function AdminSuspensionsPage() {
<form onSubmit={handleSuspend} className="px-6 py-5 space-y-4"> <form onSubmit={handleSuspend} className="px-6 py-5 space-y-4">
{/* User info */} {/* User info */}
<div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3"> <div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] font-bold text-sm flex-shrink-0 overflow-hidden"> <div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#1D4ED8] font-bold text-sm flex-shrink-0 overflow-hidden">
{selectedUser.avatar_url ? ( {selectedUser.avatar_url ? (
<img src={selectedUser.avatar_url} alt={selectedUser.full_name || selectedUser.email} className="w-full h-full object-cover" /> <img src={selectedUser.avatar_url} alt={selectedUser.full_name || selectedUser.email} className="w-full h-full object-cover" />
) : ( ) : (
@@ -637,7 +637,7 @@ export default function AdminSuspensionsPage() {
<select <select
value={suspendDuration ?? ''} value={suspendDuration ?? ''}
onChange={(e) => setSuspendDuration(e.target.value ? parseInt(e.target.value) : null)} onChange={(e) => setSuspendDuration(e.target.value ? parseInt(e.target.value) : null)}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8]"
> >
{DURATION_OPTIONS.filter(d => d.value !== null).map((d) => ( {DURATION_OPTIONS.filter(d => d.value !== null).map((d) => (
<option key={d.value} value={d.value!}>{d.label}</option> <option key={d.value} value={d.value!}>{d.label}</option>
@@ -657,7 +657,7 @@ export default function AdminSuspensionsPage() {
onChange={(e) => setSuspendReason(e.target.value)} onChange={(e) => setSuspendReason(e.target.value)}
placeholder="Explain the reason for this action..." placeholder="Explain the reason for this action..."
rows={3} rows={3}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#3b82f6] resize-none" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#1D4ED8] resize-none"
/> />
</div> </div>
@@ -729,7 +729,7 @@ export default function AdminSuspensionsPage() {
onChange={(e) => setLiftReason(e.target.value)} onChange={(e) => setLiftReason(e.target.value)}
placeholder="Reason for lifting this restriction..." placeholder="Reason for lifting this restriction..."
rows={2} rows={2}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#3b82f6] resize-none" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#1D4ED8] resize-none"
/> />
</div> </div>
<div className="flex items-center gap-3 pt-1"> <div className="flex items-center gap-3 pt-1">

View File

@@ -31,14 +31,14 @@ interface Invitation {
type Tab = 'users' | 'invitations'; type Tab = 'users' | 'invitations';
const ROLE_OPTIONS = [ const ROLE_OPTIONS = [
{ value: 'reader', label: 'Reader', color: 'text-[#888] bg-[#1a1a1a]' }, { value: 'reader', label: 'Reader', color: 'text-[#888] bg-[#FFFFFF]' },
{ value: 'contributor', label: 'Contributor', color: 'text-blue-400 bg-blue-400/10' }, { value: 'contributor', label: 'Contributor', color: 'text-blue-400 bg-blue-400/10' },
{ value: 'editor', label: 'Editor', color: 'text-purple-400 bg-purple-400/10' }, { value: 'editor', label: 'Editor', color: 'text-purple-400 bg-purple-400/10' },
{ value: 'admin', label: 'Admin', color: 'text-red-400 bg-red-400/10' }, { value: 'admin', label: 'Admin', color: 'text-red-400 bg-red-400/10' },
]; ];
const ROLE_COLORS: Record<string, string> = { const ROLE_COLORS: Record<string, string> = {
reader: 'text-[#888] bg-[#1a1a1a]', reader: 'text-[#888] bg-[#FFFFFF]',
contributor: 'text-blue-400 bg-blue-400/10', contributor: 'text-blue-400 bg-blue-400/10',
editor: 'text-purple-400 bg-purple-400/10', editor: 'text-purple-400 bg-purple-400/10',
admin: 'text-red-400 bg-red-400/10', admin: 'text-red-400 bg-red-400/10',
@@ -47,7 +47,7 @@ const ROLE_COLORS: Record<string, string> = {
const STATUS_COLORS: Record<string, string> = { const STATUS_COLORS: Record<string, string> = {
pending: 'text-yellow-400 bg-yellow-400/10', pending: 'text-yellow-400 bg-yellow-400/10',
accepted: 'text-green-400 bg-green-400/10', accepted: 'text-green-400 bg-green-400/10',
revoked: 'text-[#555] bg-[#1a1a1a]', revoked: 'text-[#555] bg-[#FFFFFF]',
expired: 'text-orange-400 bg-orange-400/10', expired: 'text-orange-400 bg-orange-400/10',
email_failed: 'text-red-400 bg-red-400/10', email_failed: 'text-red-400 bg-red-400/10',
}; };
@@ -246,7 +246,7 @@ export default function AdminUsersPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center"> <div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -276,7 +276,7 @@ export default function AdminUsersPage() {
</div> </div>
<button <button
onClick={() => setShowInviteModal(true)} onClick={() => setShowInviteModal(true)}
className="flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors" className="flex items-center gap-2 bg-[#1D4ED8] hover:bg-[#1E3A8A] text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors"
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.41 2 2 0 0 1 3.6 1.22h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.96a16 16 0 0 0 6 6l.96-.96a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21.5 16.5z" /> <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.41 2 2 0 0 1 3.6 1.22h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.96a16 16 0 0 0 6 6l.96-.96a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21.5 16.5z" />
@@ -306,7 +306,7 @@ export default function AdminUsersPage() {
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${ className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${
activeTab === tab activeTab === tab
? 'text-[#3b82f6] border-[#3b82f6]' ? 'text-[#1D4ED8] border-[#1D4ED8]'
: 'text-[#555] border-transparent hover:text-[#888]' : 'text-[#555] border-transparent hover:text-[#888]'
}`} }`}
> >
@@ -331,14 +331,14 @@ export default function AdminUsersPage() {
placeholder={activeTab === 'users' ? 'Search users...' : 'Search by email...'} placeholder={activeTab === 'users' ? 'Search users...' : 'Search by email...'}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
{activeTab === 'users' && ( {activeTab === 'users' && (
<select <select
value={roleFilter} value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)} onChange={(e) => setRoleFilter(e.target.value)}
className="bg-[#111827] border border-[#1f2937] text-[#ccc] text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#3b82f6]" className="bg-[#111827] border border-[#1f2937] text-[#ccc] text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#1D4ED8]"
> >
<option value="all">All Roles</option> <option value="all">All Roles</option>
{ROLE_OPTIONS.map((r) => ( {ROLE_OPTIONS.map((r) => (
@@ -353,7 +353,7 @@ export default function AdminUsersPage() {
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden"> <div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : filteredUsers.length === 0 ? ( ) : filteredUsers.length === 0 ? (
<div className="text-center py-16 text-[#555]"> <div className="text-center py-16 text-[#555]">
@@ -379,7 +379,7 @@ export default function AdminUsersPage() {
<tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors"> <tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] font-bold text-sm flex-shrink-0 overflow-hidden"> <div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#1D4ED8] font-bold text-sm flex-shrink-0 overflow-hidden">
{u.avatar_url ? ( {u.avatar_url ? (
<img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" /> <img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" />
) : ( ) : (
@@ -393,7 +393,7 @@ export default function AdminUsersPage() {
</div> </div>
</td> </td>
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[u.role] || 'text-[#888] bg-[#1a1a1a]'}`}> <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[u.role] || 'text-[#888] bg-[#FFFFFF]'}`}>
{u.role} {u.role}
</span> </span>
</td> </td>
@@ -418,7 +418,7 @@ export default function AdminUsersPage() {
<button <button
onClick={() => openEditRole(u)} onClick={() => openEditRole(u)}
disabled={processingIds.has(u.id)} disabled={processingIds.has(u.id)}
className="text-[#555] hover:text-[#3b82f6] transition-colors text-xs font-medium px-2 py-1 rounded hover:bg-[#1f2937] disabled:opacity-50" className="text-[#555] hover:text-[#1D4ED8] transition-colors text-xs font-medium px-2 py-1 rounded hover:bg-[#1f2937] disabled:opacity-50"
> >
Edit Role Edit Role
</button> </button>
@@ -455,7 +455,7 @@ export default function AdminUsersPage() {
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden"> <div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : filteredInvitations.length === 0 ? ( ) : filteredInvitations.length === 0 ? (
<div className="text-center py-16 text-[#555]"> <div className="text-center py-16 text-[#555]">
@@ -465,7 +465,7 @@ export default function AdminUsersPage() {
<p className="text-sm">No invitations sent yet</p> <p className="text-sm">No invitations sent yet</p>
<button <button
onClick={() => setShowInviteModal(true)} onClick={() => setShowInviteModal(true)}
className="mt-3 text-[#3b82f6] hover:text-[#60a5fa] text-sm transition-colors" className="mt-3 text-[#1D4ED8] hover:text-[#1D4ED8] text-sm transition-colors"
> >
Send your first invitation Send your first invitation
</button> </button>
@@ -491,19 +491,19 @@ export default function AdminUsersPage() {
<tr key={inv.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors"> <tr key={inv.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] text-xs font-bold flex-shrink-0"> <div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#1D4ED8] text-xs font-bold flex-shrink-0">
{inv.email[0].toUpperCase()} {inv.email[0].toUpperCase()}
</div> </div>
<span className="text-white text-sm">{inv.email}</span> <span className="text-white text-sm">{inv.email}</span>
</div> </div>
</td> </td>
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[inv.role] || 'text-[#888] bg-[#1a1a1a]'}`}> <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[inv.role] || 'text-[#888] bg-[#FFFFFF]'}`}>
{inv.role} {inv.role}
</span> </span>
</td> </td>
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize ${STATUS_COLORS[displayStatus] || 'text-[#888] bg-[#1a1a1a]'}`}> <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize ${STATUS_COLORS[displayStatus] || 'text-[#888] bg-[#FFFFFF]'}`}>
{displayStatus.replace('_', ' ')} {displayStatus.replace('_', ' ')}
</span> </span>
</td> </td>
@@ -564,7 +564,7 @@ export default function AdminUsersPage() {
value={inviteEmail} value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)} onChange={(e) => setInviteEmail(e.target.value)}
placeholder="contributor@example.com" placeholder="contributor@example.com"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<div> <div>
@@ -574,7 +574,7 @@ export default function AdminUsersPage() {
<select <select
value={inviteRole} value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)} onChange={(e) => setInviteRole(e.target.value)}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8]"
> >
<option value="contributor">Contributor can submit articles for review</option> <option value="contributor">Contributor can submit articles for review</option>
<option value="editor">Editor can publish and manage articles</option> <option value="editor">Editor can publish and manage articles</option>
@@ -590,7 +590,7 @@ export default function AdminUsersPage() {
onChange={(e) => setInviteMessage(e.target.value)} onChange={(e) => setInviteMessage(e.target.value)}
placeholder="Add a personal note to the invitation email..." placeholder="Add a personal note to the invitation email..."
rows={3} rows={3}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#3b82f6] resize-none" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#1D4ED8] resize-none"
/> />
</div> </div>
<div className="bg-[#0d1117] border border-[#1f2937] rounded-lg p-3"> <div className="bg-[#0d1117] border border-[#1f2937] rounded-lg p-3">
@@ -609,7 +609,7 @@ export default function AdminUsersPage() {
<button <button
type="submit" type="submit"
disabled={inviteSending || !inviteEmail.trim()} disabled={inviteSending || !inviteEmail.trim()}
className="flex-1 bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2" className="flex-1 bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
> >
{inviteSending ? ( {inviteSending ? (
<> <>
@@ -644,7 +644,7 @@ export default function AdminUsersPage() {
</div> </div>
<div className="px-6 py-5 space-y-4"> <div className="px-6 py-5 space-y-4">
<div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3"> <div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] font-bold text-sm flex-shrink-0 overflow-hidden"> <div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#1D4ED8] font-bold text-sm flex-shrink-0 overflow-hidden">
{editingUser.avatar_url ? ( {editingUser.avatar_url ? (
<img src={editingUser.avatar_url} alt={editingUser.full_name || editingUser.email} className="w-full h-full object-cover" /> <img src={editingUser.avatar_url} alt={editingUser.full_name || editingUser.email} className="w-full h-full object-cover" />
) : ( ) : (
@@ -666,7 +666,7 @@ export default function AdminUsersPage() {
key={r.value} key={r.value}
className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${ className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
editRole === r.value editRole === r.value
? 'border-[#3b82f6] bg-[#3b82f6]/10' ? 'border-[#1D4ED8] bg-[#1D4ED8]/10'
: 'border-[#1f2937] hover:border-[#2a3a50]' : 'border-[#1f2937] hover:border-[#2a3a50]'
}`} }`}
> >
@@ -679,9 +679,9 @@ export default function AdminUsersPage() {
className="sr-only" className="sr-only"
/> />
<span className={`w-3.5 h-3.5 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${ <span className={`w-3.5 h-3.5 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${
editRole === r.value ? 'border-[#3b82f6]' : 'border-[#444]' editRole === r.value ? 'border-[#1D4ED8]' : 'border-[#444]'
}`}> }`}>
{editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#3b82f6]" />} {editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#1D4ED8]" />}
</span> </span>
<span className={`text-xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded ${r.color}`}> <span className={`text-xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded ${r.color}`}>
{r.label} {r.label}
@@ -700,7 +700,7 @@ export default function AdminUsersPage() {
<button <button
onClick={handleEditSave} onClick={handleEditSave}
disabled={editSaving || editRole === editingUser.role} disabled={editSaving || editRole === editingUser.role}
className="flex-1 bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2" className="flex-1 bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
> >
{editSaving ? ( {editSaving ? (
<> <>

View File

@@ -223,7 +223,7 @@ export default function WPUsersAdminPage() {
</thead> </thead>
<tbody> <tbody>
{filteredUsers.map(u => ( {filteredUsers.map(u => (
<tr key={u.id} className="border-b border-[#1a1a1a] hover:bg-[#151515] transition-colors"> <tr key={u.id} className="border-b border-[#FFFFFF] hover:bg-[#151515] transition-colors">
<td className="px-4 py-3 text-[#555] font-mono text-xs">{u.wp_id}</td> <td className="px-4 py-3 text-[#555] font-mono text-xs">{u.wp_id}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className="text-white font-medium">{u.wp_username}</span> <span className="text-white font-medium">{u.wp_username}</span>

View File

@@ -1,12 +1,12 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Advertise With AV Beat — Reach Broadcast Engineering Professionals', title: 'Advertise With AV Beat — Reach Pro AV & Live Production Professionals',
description: 'Advertise with AV Beat to reach broadcast engineers, production professionals, and media technology decision-makers. Display ads, sponsored content, and newsletter placements.', description: 'Advertise with AV Beat to reach pro AV integrators, live production engineers, display tech specialists, and media decision-makers. Display ads, sponsored content, and newsletter placements.',
alternates: { canonical: '/advertise' }, alternates: { canonical: '/advertise' },
openGraph: { openGraph: {
title: 'Advertise With AV Beat', title: 'Advertise With AV Beat',
description: 'Reach broadcast engineers, production professionals, and media technology decision-makers.', description: 'Reach pro AV integrators, live production engineers, and display tech decision-makers.',
url: '/advertise', url: '/advertise',
type: 'website', type: 'website',
}, },

View File

@@ -44,15 +44,17 @@ export default function AdvertisePage() {
{/* Hero */} {/* Hero */}
{/* DO NOT OVERRIDE — Advertise page hero section */} {/* DO NOT OVERRIDE — Advertise page hero section */}
<div className="bg-[#0d1520] border-b border-[#1e3a5f] py-12"> <div className="bg-[#0d1520] border-b border-[#DCE6F2] py-12">
<div className="max-w-container mx-auto px-4 text-center"> <div className="max-w-container mx-auto px-4 text-center">
<span className="section-label mb-3 inline-block">Advertise</span> <span className="section-label mb-3 inline-block">Advertise</span>
<h1 className="font-heading text-white text-4xl font-bold mb-4">Reach Broadcast Engineering Professionals</h1> <h1 className="font-heading text-white text-4xl font-bold mb-4">Reach Pro AV &amp; Live Production Professionals</h1>
<p className="text-[#aaa] font-body text-lg max-w-2xl mx-auto mb-6"> <p className="text-[#aaa] font-body text-lg max-w-2xl mx-auto mb-6">
AV Beat is the leading digital platform for broadcast engineering professionals. Connect your brand with decision-makers, engineers, and executives across the broadcast industry. AV Beat is the leading digital platform for pro AV, live production, and display tech professionals. Connect your brand with decision-makers, integrators, and executives across the pro AV industry.
</p> </p>
<a <a
href="#contact" href="https://relevantmediaproperties.com/av-beat---media-kit"
target="_blank"
rel="noopener"
className="btn-subscribe py-3 px-8 text-base inline-block"> className="btn-subscribe py-3 px-8 text-base inline-block">
Request Media Kit Request Media Kit
</a> </a>
@@ -64,12 +66,12 @@ export default function AdvertisePage() {
<section className="mb-12"> <section className="mb-12">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<span className="section-label">Our Audience</span> <span className="section-label">Our Audience</span>
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#DCE6F2]" />
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat) => ( {stats.map((stat) => (
<div key={stat.label} className="bg-[#111] border border-[#222] p-5 text-center"> <div key={stat.label} className="bg-[#111] border border-[#222] p-5 text-center">
<div className="font-heading text-[#3b82f6] text-2xl font-bold mb-1">{stat.value}</div> <div className="font-heading text-[#1D4ED8] text-2xl font-bold mb-1">{stat.value}</div>
<div className="font-body text-[#777] text-xs uppercase tracking-wide">{stat.label}</div> <div className="font-body text-[#777] text-xs uppercase tracking-wide">{stat.label}</div>
</div> </div>
))} ))}
@@ -80,25 +82,25 @@ export default function AdvertisePage() {
<section className="mb-12"> <section className="mb-12">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<span className="section-label">Ad Placements</span> <span className="section-label">Ad Placements</span>
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#DCE6F2]" />
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm font-body"> <table className="w-full text-sm font-body">
<thead> <thead>
<tr className="border-b border-[#222]"> <tr className="border-b border-[#222]">
<th className="text-left py-3 px-4 text-[#3b82f6] font-bold text-xs uppercase tracking-wider">Placement</th> <th className="text-left py-3 px-4 text-[#1D4ED8] font-bold text-xs uppercase tracking-wider">Placement</th>
<th className="text-left py-3 px-4 text-[#3b82f6] font-bold text-xs uppercase tracking-wider">Dimensions</th> <th className="text-left py-3 px-4 text-[#1D4ED8] font-bold text-xs uppercase tracking-wider">Dimensions</th>
<th className="text-left py-3 px-4 text-[#3b82f6] font-bold text-xs uppercase tracking-wider">Location</th> <th className="text-left py-3 px-4 text-[#1D4ED8] font-bold text-xs uppercase tracking-wider">Location</th>
<th className="text-left py-3 px-4 text-[#3b82f6] font-bold text-xs uppercase tracking-wider">Rate</th> <th className="text-left py-3 px-4 text-[#1D4ED8] font-bold text-xs uppercase tracking-wider">Rate</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{adZones.map((zone, i) => ( {adZones.map((zone, i) => (
<tr key={zone.name} className={`border-b border-[#1a1a1a] ${i % 2 === 0 ? "bg-[#0d0d0d]" : "bg-[#111]"}`}> <tr key={zone.name} className={`border-b border-[#FFFFFF] ${i % 2 === 0 ? "bg-[#F8FAFC]" : "bg-[#111]"}`}>
<td className="py-3 px-4 text-[#e0e0e0] font-medium">{zone.name}</td> <td className="py-3 px-4 text-[#e0e0e0] font-medium">{zone.name}</td>
<td className="py-3 px-4 text-[#888] font-mono text-xs">{zone.dimensions}</td> <td className="py-3 px-4 text-[#888] font-mono text-xs">{zone.dimensions}</td>
<td className="py-3 px-4 text-[#777]">{zone.placement}</td> <td className="py-3 px-4 text-[#777]">{zone.placement}</td>
<td className="py-3 px-4 text-[#3b82f6] font-medium">{zone.cpm}</td> <td className="py-3 px-4 text-[#1D4ED8] font-medium">{zone.cpm}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -114,7 +116,7 @@ export default function AdvertisePage() {
<section id="contact" className="mb-12"> <section id="contact" className="mb-12">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<span className="section-label">Contact Sales</span> <span className="section-label">Contact Sales</span>
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#DCE6F2]" />
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Contact info */} {/* Contact info */}
@@ -123,23 +125,23 @@ export default function AdvertisePage() {
<h3 className="font-heading text-[#e0e0e0] text-lg font-bold mb-4">Sales Contact</h3> <h3 className="font-heading text-[#e0e0e0] text-lg font-bold mb-4">Sales Contact</h3>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<p className="font-body text-[#3b82f6] text-xs font-bold uppercase tracking-wider mb-1">Advertising Sales</p> <p className="font-body text-[#1D4ED8] text-xs font-bold uppercase tracking-wider mb-1">Advertising Sales</p>
<p className="font-body text-[#e0e0e0] font-medium">Jeff Victor</p> <p className="font-body text-[#e0e0e0] font-medium">Jeff Victor</p>
<a href="mailto:jeff@avbeat.com" className="font-body text-[#3b82f6] text-sm hover:underline"> <a href="mailto:advertising@relevantmediaproperties.com" className="font-body text-[#1D4ED8] text-sm hover:underline">
jeff@avbeat.com advertising@relevantmediaproperties.com
</a> </a>
</div> </div>
<div className="border-t border-[#222] pt-4"> <div className="border-t border-[#222] pt-4">
<p className="font-body text-[#3b82f6] text-xs font-bold uppercase tracking-wider mb-1">Publication</p> <p className="font-body text-[#1D4ED8] text-xs font-bold uppercase tracking-wider mb-1">Publication</p>
<p className="font-body text-[#777] text-sm">AV Beat</p> <p className="font-body text-[#777] text-sm">AV Beat</p>
<p className="font-body text-[#777] text-sm">Relevant Media Properties</p> <p className="font-body text-[#777] text-sm">Relevant Media Properties</p>
</div> </div>
<div className="border-t border-[#222] pt-4"> <div className="border-t border-[#222] pt-4">
<p className="font-body text-[#3b82f6] text-xs font-bold uppercase tracking-wider mb-2">What We Offer</p> <p className="font-body text-[#1D4ED8] text-xs font-bold uppercase tracking-wider mb-2">What We Offer</p>
<ul className="space-y-1.5"> <ul className="space-y-1.5">
{["Display advertising (web)", "Newsletter sponsorships", "Sponsored content", "Event coverage partnerships", "Podcast sponsorships"].map((item) => ( {["Display advertising (web)", "Newsletter sponsorships", "Sponsored content", "Event coverage partnerships", "Podcast sponsorships"].map((item) => (
<li key={item} className="flex items-start gap-2 text-[#777] text-sm font-body"> <li key={item} className="flex items-start gap-2 text-[#777] text-sm font-body">
<span className="text-[#3b82f6] mt-0.5"></span> <span className="text-[#1D4ED8] mt-0.5"></span>
{item} {item}
</li> </li>
))} ))}
@@ -152,16 +154,16 @@ export default function AdvertisePage() {
{/* Form */} {/* Form */}
<div className="lg:col-span-8"> <div className="lg:col-span-8">
{submitted ? ( {submitted ? (
<div className="bg-[#111] border border-[#3b82f6] p-8 text-center"> <div className="bg-[#111] border border-[#1D4ED8] p-8 text-center">
<div className="w-12 h-12 rounded-full bg-[#3b82f6]/20 flex items-center justify-center mx-auto mb-4"> <div className="w-12 h-12 rounded-full bg-[#1D4ED8]/20 flex items-center justify-center mx-auto mb-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 12l4 4 10-10" stroke="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path d="M5 12l4 4 10-10" stroke="#1D4ED8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</div> </div>
<h3 className="font-heading text-[#e0e0e0] text-xl font-bold mb-2">Message Sent!</h3> <h3 className="font-heading text-[#e0e0e0] text-xl font-bold mb-2">Message Sent!</h3>
<p className="font-body text-[#777] text-sm"> <p className="font-body text-[#777] text-sm">
Thanks for reaching out. Jeff will be in touch within one business day at{" "} Thanks for reaching out. Jeff will be in touch within one business day at{" "}
<a href="mailto:jeff@avbeat.com" className="text-[#3b82f6] hover:underline">jeff@avbeat.com</a>. <a href="mailto:advertising@relevantmediaproperties.com" className="text-[#1D4ED8] hover:underline">advertising@relevantmediaproperties.com</a>.
</p> </p>
</div> </div>
) : ( ) : (
@@ -187,7 +189,7 @@ export default function AdvertisePage() {
value={formData.company} value={formData.company}
onChange={(e) => setFormData({ ...formData, company: e.target.value })} onChange={(e) => setFormData({ ...formData, company: e.target.value })}
className="search-input w-full py-2.5 px-3 text-sm" className="search-input w-full py-2.5 px-3 text-sm"
placeholder="Acme Broadcast Co." placeholder="Acme AV Systems"
/> />
</div> </div>
</div> </div>
@@ -219,7 +221,7 @@ export default function AdvertisePage() {
<select <select
value={formData.budget} value={formData.budget}
onChange={(e) => setFormData({ ...formData, budget: e.target.value })} onChange={(e) => setFormData({ ...formData, budget: e.target.value })}
className="search-input w-full py-2.5 px-3 text-sm bg-[#1a1a1a]"> className="search-input w-full py-2.5 px-3 text-sm bg-[#FFFFFF]">
<option value="">Select a range...</option> <option value="">Select a range...</option>
<option value="under-5k">Under $5,000</option> <option value="under-5k">Under $5,000</option>
<option value="5k-15k">$5,000 $15,000</option> <option value="5k-15k">$5,000 $15,000</option>
@@ -245,7 +247,7 @@ export default function AdvertisePage() {
{loading ? "Sending..." : "Send Inquiry"} {loading ? "Sending..." : "Send Inquiry"}
</button> </button>
<p className="text-[#555] text-xs font-body"> <p className="text-[#555] text-xs font-body">
Or email directly: <a href="mailto:jeff@avbeat.com" className="text-[#3b82f6] hover:underline">jeff@avbeat.com</a> Or email directly: <a href="mailto:advertising@relevantmediaproperties.com" className="text-[#1D4ED8] hover:underline">advertising@relevantmediaproperties.com</a>
</p> </p>
</form> </form>
)} )}

View File

@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
html: ` html: `
<div style="font-family:Arial,sans-serif;max-width:600px;background:#0a0a0a;color:#e5e5e5;padding:32px;border-radius:8px;"> <div style="font-family:Arial,sans-serif;max-width:600px;background:#0a0a0a;color:#e5e5e5;padding:32px;border-radius:8px;">
<div style="margin-bottom:24px;"> <div style="margin-bottom:24px;">
<h1 style="color:#3b82f6;font-size:20px;margin:0 0 4px;">Relevant Media Properties</h1> <h1 style="color:#1D4ED8;font-size:20px;margin:0 0 4px;">Relevant Media Properties</h1>
<p style="color:#555;font-size:12px;margin:0;">AV Beat · AV Beat — Company Story Engine</p> <p style="color:#555;font-size:12px;margin:0;">AV Beat · AV Beat — Company Story Engine</p>
</div> </div>
<h2 style="font-size:18px;color:#fff;margin:0 0 12px;">New Company Stories Queued for Review</h2> <h2 style="font-size:18px;color:#fff;margin:0 0 12px;">New Company Stories Queued for Review</h2>
@@ -32,11 +32,11 @@ export async function POST(request: NextRequest) {
${moreText} ${moreText}
</div> </div>
<a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com'}/admin/company-tracker" <a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com'}/admin/company-tracker"
style="display:inline-block;background:#3b82f6;color:#fff;text-decoration:none;padding:10px 20px;border-radius:6px;font-size:14px;font-weight:bold;"> style="display:inline-block;background:#1D4ED8;color:#fff;text-decoration:none;padding:10px 20px;border-radius:6px;font-size:14px;font-weight:bold;">
Review Story Queue → Review Story Queue →
</a> </a>
<p style="color:#444;font-size:12px;margin-top:24px;"> <p style="color:#444;font-size:12px;margin-top:24px;">
Manage settings at <a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com'}/admin/company-tracker" style="color:#3b82f6;">Company Tracker</a> Manage settings at <a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com'}/admin/company-tracker" style="color:#1D4ED8;">Company Tracker</a>
</p> </p>
</div> </div>
`, `,

View File

@@ -22,12 +22,12 @@ export async function POST(request: NextRequest) {
date: a.date, date: a.date,
})); }));
const systemPrompt = `You are a personalized content recommendation engine for AV Beat, a broadcast engineering news platform. const systemPrompt = `You are a personalized content recommendation engine for AV Beat, a pro AV / live production / display tech news platform.
Given a reader's topic interests and reading history, select the 4 most relevant articles from the provided article list. Given a reader's topic interests and reading history, select the 4 most relevant articles from the provided article list.
Return ONLY a valid JSON array of slugs in order of relevance. Example: ["slug-1","slug-2","slug-3","slug-4"] Return ONLY a valid JSON array of slugs in order of relevance. Example: ["slug-1","slug-2","slug-3","slug-4"]
Do not include any explanation or markdown — only the raw JSON array.`; Do not include any explanation or markdown — only the raw JSON array.`;
const userPrompt = `Reader interests: ${interests?.length ? interests.join(', ') : 'general broadcast engineering'} const userPrompt = `Reader interests: ${interests?.length ? interests.join(', ') : 'general pro AV and live production'}
Reading history (recently read slugs): ${readingHistory?.length ? readingHistory.join(', ') : 'none'} Reading history (recently read slugs): ${readingHistory?.length ? readingHistory.join(', ') : 'none'}
Available articles: Available articles:

View File

@@ -0,0 +1,130 @@
/**
* Article star ratings — no-auth, one-vote-per-browser-session.
*
* GET /api/articles/rating?slug=<slug>
* → { avg: number|null, count: number, my_rating: number|null }
*
* POST /api/articles/rating
* body { slug: string, rating: 1..5 }
* → { ok: true, avg, count, my_rating }
*
* Storage: distribute.article_ratings (centralized RMP cross-property table).
* Each property writes with its own `property` value so the same table
* powers BB / AV Beat / SNS / etc. — see the top-level constant below.
*
* Session: a uuid stored in the `av_rate_sid` cookie (1y) — the (property,
* slug, session_id) tuple is UNIQUE so a duplicate POST from the same
* browser session for the same article is a no-op. Clearing cookies /
* opening incognito creates a new session, which is acceptable per the
* "one per computer within a session" spec.
*/
import { NextRequest, NextResponse } from "next/server";
import { createAdminClient } from "@/lib/supabase/admin";
import { randomUUID, createHash } from "crypto";
export const dynamic = "force-dynamic";
const PROPERTY = "avbeat";
const SESSION_COOKIE = "av_rate_sid";
const SESSION_TTL_SECONDS = 60 * 60 * 24 * 365; // 1 year
function getOrMintSession(req: NextRequest): { sid: string; mint: boolean } {
const existing = req.cookies.get(SESSION_COOKIE)?.value;
if (existing && /^[0-9a-f-]{30,40}$/i.test(existing)) {
return { sid: existing, mint: false };
}
return { sid: randomUUID(), mint: true };
}
function hashIp(req: NextRequest): string | null {
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
|| req.headers.get("x-real-ip")
|| null;
if (!ip) return null;
// SHA-256 truncated — enough for soft analytics, not reversible
return createHash("sha256").update(ip).digest("hex").slice(0, 32);
}
function attachSessionCookie(res: NextResponse, sid: string) {
res.cookies.set(SESSION_COOKIE, sid, {
maxAge: SESSION_TTL_SECONDS,
sameSite: "lax",
path: "/",
httpOnly: false, // client JS may read it for optimistic UI
});
}
async function loadSummary(slug: string, sid: string) {
const sb = createAdminClient("distribute");
const [agg, mine] = await Promise.all([
sb.from("article_rating_aggregates")
.select("real_count, real_avg")
.eq("property", PROPERTY)
.eq("article_slug", slug)
.maybeSingle(),
sb.from("article_ratings")
.select("rating")
.eq("property", PROPERTY)
.eq("article_slug", slug)
.eq("session_id", sid)
.maybeSingle(),
]);
return {
avg: agg.data?.real_avg ?? null,
count: agg.data?.real_count ?? 0,
my_rating: mine.data?.rating ?? null,
};
}
export async function GET(req: NextRequest) {
const slug = new URL(req.url).searchParams.get("slug");
if (!slug) {
return NextResponse.json({ error: "slug required" }, { status: 400 });
}
const { sid, mint } = getOrMintSession(req);
const summary = await loadSummary(slug, sid);
const res = NextResponse.json(summary);
if (mint) attachSessionCookie(res, sid);
return res;
}
export async function POST(req: NextRequest) {
let body: { slug?: unknown; rating?: unknown };
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "invalid json" }, { status: 400 });
}
const slug = typeof body.slug === "string" ? body.slug : null;
const rating = typeof body.rating === "number" ? Math.round(body.rating) : null;
if (!slug || !rating || rating < 1 || rating > 5) {
return NextResponse.json({ error: "slug + rating (1..5) required" }, { status: 400 });
}
const { sid, mint } = getOrMintSession(req);
const sb = createAdminClient("distribute");
// Insert; on conflict (property, slug, session_id) we silently no-op
// because the spec says one rating per session. We then return the
// CURRENT summary so the client renders what's actually stored.
const { error } = await sb
.from("article_ratings")
.insert({
property: PROPERTY,
article_slug: slug,
rating,
session_id: sid,
ip_hash: hashIp(req),
user_agent: req.headers.get("user-agent")?.slice(0, 200) ?? null,
});
// 23505 = unique_violation — duplicate vote from same session, expected
if (error && (error as { code?: string }).code !== "23505") {
return NextResponse.json({ error: error.message }, { status: 500 });
}
const summary = await loadSummary(slug, sid);
const res = NextResponse.json({ ok: true, ...summary });
if (mint) attachSessionCookie(res, sid);
return res;
}

View File

@@ -28,7 +28,7 @@ export async function GET(req: NextRequest) {
// Layer 1 — the rich subscriber registry (95k+ rows) // Layer 1 — the rich subscriber registry (95k+ rows)
const { data: sub } = await svc const { data: sub } = await svc
.schema("bb") .schema("av")
.from("email_subscribers") .from("email_subscribers")
.select("id, status") .select("id, status")
.eq("email_domain", email.split("@")[1]) .eq("email_domain", email.split("@")[1])
@@ -39,7 +39,7 @@ export async function GET(req: NextRequest) {
// Layer 2 — the lean property-specific list // Layer 2 — the lean property-specific list
const { data: nl } = await svc const { data: nl } = await svc
.schema("bb") .schema("av")
.from("newsletter_subscribers") .from("newsletter_subscribers")
.select("id") .select("id")
.ilike("email", email) .ilike("email", email)

View File

@@ -1,66 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { hybridAI } from '@/lib/ai/hybridRouter';
export async function POST(req: NextRequest) { export async function POST(_req: NextRequest) {
try { return NextResponse.json({ error: 'Feature unavailable' }, { status: 410 });
const body = await req.json(); }
const { thread_id, question, thread_title, thread_body, replies } = body;
if (!thread_id || !question) {
return NextResponse.json({ error: 'thread_id and question are required' }, { status: 400 });
}
const existingContext = replies?.length
? replies.map((r: any) => `${r.author_name}: ${r.body}`).join('\n\n')
: '';
const result = await hybridAI(
[
{
role: 'system',
content: `You are the AV Beat AI Assistant — a knowledgeable, helpful assistant for broadcast engineering professionals. You have deep expertise in broadcast technology including live production, IP workflows, SMPTE ST 2110, audio engineering, cameras, streaming, post-production, MAM systems, and broadcast industry trends.
You are responding in a community forum. Be helpful, technically accurate, and conversational. Acknowledge when a question is complex or when professional consultation is advisable. Keep responses focused and practical — broadcast engineers value concise, actionable information.
Important: You are an AI assistant. Be transparent about this if asked. Do not pretend to be a human.`,
},
{
role: 'user',
content: `Forum thread: "${thread_title}"
Original post: ${thread_body}
${existingContext ? `Existing discussion:\n${existingContext}\n\n` : ''}New question: ${question}
Please provide a helpful, technically accurate response for this broadcast engineering forum thread.`,
},
],
{ maxTokens: 600, isBackground: false, priority: 2 }
);
const aiResponseText = result.text;
if (!aiResponseText) {
return NextResponse.json({ error: 'No response from AI' }, { status: 500 });
}
const supabase = await createClient();
const { data: savedReply, error: saveError } = await supabase
.from('forum_replies')
.insert({
thread_id,
body: aiResponseText,
author_name: 'AV Beat AI',
is_ai_response: true,
})
.select()
.single();
if (saveError) throw saveError;
return NextResponse.json({ reply: savedReply });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}

View File

@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server'; import { createClient } from '@/lib/supabase/server';
import { attachAuthorUsernames, buildAuthorUsernameMap } from '@/lib/forum/authorUsernames';
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try { try {
@@ -30,7 +31,13 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
if (repliesError) throw repliesError; if (repliesError) throw repliesError;
return NextResponse.json({ thread, replies: replies || [] }); const usernameMap = await buildAuthorUsernameMap(supabase);
const [threadWithUsername] = attachAuthorUsernames([thread], usernameMap);
return NextResponse.json({
thread: threadWithUsername,
replies: attachAuthorUsernames(replies, usernameMap),
});
} catch (err: any) { } catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 }); return NextResponse.json({ error: err.message }, { status: 500 });
} }

View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server'; import { createClient } from '@/lib/supabase/server';
import { ensureForumProfile } from '@/lib/forum/profile'; import { ensureForumProfile } from '@/lib/forum/profile';
import { attachAuthorUsernames, buildAuthorUsernameMap } from '@/lib/forum/authorUsernames';
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
try { try {
@@ -50,7 +51,13 @@ export async function GET(req: NextRequest) {
const { data, error, count } = await query; const { data, error, count } = await query;
if (error) throw error; if (error) throw error;
return NextResponse.json({ threads: data, total: count, page, limit }); const usernameMap = await buildAuthorUsernameMap(supabase);
return NextResponse.json({
threads: attachAuthorUsernames(data, usernameMap),
total: count,
page,
limit,
});
} catch (err: any) { } catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 }); return NextResponse.json({ error: err.message }, { status: 500 });
} }

View File

@@ -6,7 +6,7 @@ export const revalidate = 120;
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!; const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb"; const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "av";
function client() { function client() {
return createClient(SUPABASE_URL, SUPABASE_ANON, { return createClient(SUPABASE_URL, SUPABASE_ANON, {
@@ -16,24 +16,19 @@ function client() {
} }
export async function GET( export async function GET(
req: Request, _req: Request,
{ params }: { params: Promise<{ username: string }> }, { params }: { params: Promise<{ username: string }> },
) { ) {
const { username } = await params; const { username } = await params;
const sb = client(); const sb = client();
// 1) Profile — accept EITHER username OR display_name (URL-encoded).
// forum_threads/forum_replies store author_name = display_name,
// so the link in the UI uses the display name; we resolve it here.
// Multiple profiles can share a display_name (Michael Carter, Alex Rivera,
// etc.) — pick the earliest-created profile deterministically rather than
// erroring out with maybeSingle().
const decoded = decodeURIComponent(username); const decoded = decodeURIComponent(username);
let { data: profile } = await sb let { data: profile } = await sb
.from("forum_user_profiles") .from("forum_user_profiles")
.select("*") .select("*")
.eq("username", decoded) .eq("username", decoded)
.maybeSingle(); .maybeSingle();
if (!profile) { if (!profile) {
const r2 = await sb const r2 = await sb
.from("forum_user_profiles") .from("forum_user_profiles")
@@ -48,51 +43,79 @@ export async function GET(
return NextResponse.json({ error: "not found" }, { status: 404 }); return NextResponse.json({ error: "not found" }, { status: 404 });
} }
// 2) Their threads (use display_name match since virtual users live const displayName = profile.display_name as string;
// outside the auth.users / bb.user_profiles graph and the
// forum_threads.author_id is NULL for them).
const displayName = (profile as any).display_name as string;
const { data: threads } = await sb
.from("forum_threads")
.select("id,title,reply_count,view_count,vote_score,created_at,category_id,forum_categories(name,slug)")
.eq("author_name", displayName)
.order("created_at", { ascending: false })
.limit(50);
// 3) Recent replies const [
const { data: replies } = await sb { count: threadCount },
.from("forum_replies") { count: replyCount },
.select("id,thread_id,body,vote_score,created_at,forum_threads(title)") { data: threads },
.eq("author_name", displayName) { data: replies },
.order("created_at", { ascending: false }) ] = await Promise.all([
.limit(20); sb
.from("forum_threads")
.select("*", { count: "exact", head: true })
.eq("author_name", displayName),
sb
.from("forum_replies")
.select("*", { count: "exact", head: true })
.eq("author_name", displayName),
sb
.from("forum_threads")
.select(
"id,title,reply_count,view_count,vote_score,created_at,category_id,forum_categories(name,slug)",
)
.eq("author_name", displayName)
.order("created_at", { ascending: false })
.limit(50),
sb
.from("forum_replies")
.select("id,thread_id,body,vote_score,created_at")
.eq("author_name", displayName)
.order("created_at", { ascending: false })
.limit(20),
]);
// 4) Computed counts const threadIds = [...new Set((replies || []).map((r) => r.thread_id).filter(Boolean))];
const threadCount = (threads || []).length; let titleByThread: Record<string, string> = {};
const replyCount = (replies || []).length; if (threadIds.length > 0) {
const { data: threadTitles } = await sb
.from("forum_threads")
.select("id,title")
.in("id", threadIds);
titleByThread = Object.fromEntries(
(threadTitles || []).map((t) => [t.id, t.title]),
);
}
const repliesWithTitles = (replies || []).map((r) => ({
...r,
forum_threads: { title: titleByThread[r.thread_id] || "(thread)" },
}));
const totalThreads = threadCount ?? 0;
const totalReplies = replyCount ?? 0;
return NextResponse.json({ return NextResponse.json({
profile: { profile: {
username: (profile as any).username, username: profile.username,
display_name: (profile as any).display_name, display_name: profile.display_name,
bio: (profile as any).bio || null, bio: profile.bio || null,
avatar_url: (profile as any).avatar_url || null, avatar_url: profile.avatar_url || null,
role_title: (profile as any).role_title || null, role_title: profile.role_title || null,
company: (profile as any).company || null, company: profile.company || null,
location_city: (profile as any).location_city || null, location_city: profile.location_city || null,
location_country: (profile as any).location_country || null, location_country: profile.location_country || null,
years_experience: (profile as any).years_experience || null, years_experience: profile.years_experience || null,
ai_persona_archetype: (profile as any).ai_persona_archetype || null, expertise_level: profile.expertise_level || null,
expertise_level: (profile as any).expertise_level || null, expertise_tags: profile.expertise_tags || [],
expertise_tags: (profile as any).expertise_tags || [], signature: profile.signature || null,
writing_voice: (profile as any).writing_voice || null, join_date: profile.join_date || null,
signature: (profile as any).signature || null, last_seen_at: profile.last_seen_at || null,
join_date: (profile as any).join_date || null, thread_count: totalThreads,
last_seen_at: (profile as any).last_seen_at || null, reply_count: totalReplies,
thread_count: threadCount, post_count: totalThreads + totalReplies,
reply_count: replyCount,
}, },
threads: threads || [], threads: threads || [],
replies: replies || [], replies: repliesWithTitles,
}); });
} }

View File

@@ -25,7 +25,7 @@ export async function POST(req: NextRequest) {
}); });
const noteHtml = note const noteHtml = note
? `<div style="background:#1a1a1a;border-left:3px solid #3b82f6;padding:12px 16px;margin-bottom:20px;border-radius:2px;"> ? `<div style="background:#1a1a1a;border-left:3px solid #1D4ED8;padding:12px 16px;margin-bottom:20px;border-radius:2px;">
<p style="margin:0;color:#aaa;font-size:14px;font-style:italic;">"${note}"</p> <p style="margin:0;color:#aaa;font-size:14px;font-style:italic;">"${note}"</p>
</div>` </div>`
: ""; : "";
@@ -45,7 +45,7 @@ export async function POST(req: NextRequest) {
<!-- Header --> <!-- Header -->
<tr> <tr>
<td style="background:#0a0a0a;border-bottom:1px solid #1a1a1a;padding:20px 28px;"> <td style="background:#0a0a0a;border-bottom:1px solid #1a1a1a;padding:20px 28px;">
<p style="margin:0;color:#3b82f6;font-size:11px;font-weight:700;letter-spacing:3px;text-transform:uppercase;">AV Beat</p> <p style="margin:0;color:#1D4ED8;font-size:11px;font-weight:700;letter-spacing:3px;text-transform:uppercase;">AV Beat</p>
</td> </td>
</tr> </tr>
<!-- Body --> <!-- Body -->
@@ -53,10 +53,10 @@ export async function POST(req: NextRequest) {
<td style="padding:28px;"> <td style="padding:28px;">
<p style="margin:0 0 8px;color:#555;font-size:11px;text-transform:uppercase;letter-spacing:2px;">Someone shared an article with you</p> <p style="margin:0 0 8px;color:#555;font-size:11px;text-transform:uppercase;letter-spacing:2px;">Someone shared an article with you</p>
${noteHtml} ${noteHtml}
<p style="margin:0 0 6px;color:#3b82f6;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:2px;">NEWS</p> <p style="margin:0 0 6px;color:#1D4ED8;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:2px;">NEWS</p>
<h1 style="margin:0 0 12px;color:#f0f0f0;font-size:22px;font-weight:700;line-height:1.3;">${articleTitle}</h1> <h1 style="margin:0 0 12px;color:#f0f0f0;font-size:22px;font-weight:700;line-height:1.3;">${articleTitle}</h1>
<p style="margin:0 0 24px;color:#999;font-size:14px;line-height:1.6;">${articleExcerpt}</p> <p style="margin:0 0 24px;color:#999;font-size:14px;line-height:1.6;">${articleExcerpt}</p>
<a href="${articleUrl}" style="display:inline-block;background:#3b82f6;color:#fff;text-decoration:none;font-size:13px;font-weight:700;padding:12px 24px;border-radius:2px;letter-spacing:1px;text-transform:uppercase;">Read Article</a> <a href="${articleUrl}" style="display:inline-block;background:#1D4ED8;color:#fff;text-decoration:none;font-size:13px;font-weight:700;padding:12px 24px;border-radius:2px;letter-spacing:1px;text-transform:uppercase;">Read Article</a>
</td> </td>
</tr> </tr>
<!-- Footer --> <!-- Footer -->

View File

@@ -86,7 +86,7 @@ async function sendWelcome(email: string, topics?: string[]) {
const topicsBlock = const topicsBlock =
topics && topics.length > 0 topics && topics.length > 0
? `<p style="color:#aaa;font-size:14px;line-height:1.5;margin:14px 0 0;">You'll receive coverage on: <strong style="color:#3b82f6;">${topics.join(', ')}</strong></p>` ? `<p style="color:#aaa;font-size:14px;line-height:1.5;margin:14px 0 0;">You'll receive coverage on: <strong style="color:#1D4ED8;">${topics.join(', ')}</strong></p>`
: ''; : '';
await transporter.sendMail({ await transporter.sendMail({
@@ -96,24 +96,24 @@ async function sendWelcome(email: string, topics?: string[]) {
html: ` html: `
<!DOCTYPE html> <!DOCTYPE html>
<html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>Welcome to AV Beat</title></head> <html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>Welcome to AV Beat</title></head>
<body style="margin:0;padding:0;background:#0a0e15;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;"> <body style="margin:0;padding:0;background:#F8FAFC;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#0a0e15;padding:40px 16px;"> <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#F8FAFC;padding:40px 16px;">
<tr><td align="center"> <tr><td align="center">
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="max-width:600px;background:#11192a;border:1px solid #1e3a5f;border-radius:10px;overflow:hidden;"> <table role="presentation" cellpadding="0" cellspacing="0" width="600" style="max-width:600px;background:#FFFFFF;border:1px solid #DCE6F2;border-radius:10px;overflow:hidden;">
<tr><td style="padding:32px 36px 8px;"> <tr><td style="padding:32px 36px 8px;">
<div style="font-family:'Courier New',monospace;font-size:10px;font-weight:700;letter-spacing:0.22em;text-transform:uppercase;color:#3b82f6;">AV Beat Newsletter</div> <div style="font-family:'Courier New',monospace;font-size:10px;font-weight:700;letter-spacing:0.22em;text-transform:uppercase;color:#1D4ED8;">AV Beat Newsletter</div>
<h1 style="margin:14px 0 8px;font-size:24px;line-height:1.2;color:#ffffff;font-weight:800;">You're in</h1> <h1 style="margin:14px 0 8px;font-size:24px;line-height:1.2;color:#ffffff;font-weight:800;">You're in</h1>
<p style="margin:0;color:#cfd6e1;font-size:15px;line-height:1.55;">Thanks for subscribing to <strong style="color:#ffffff;">AV Beat</strong> — the digital platform for broadcast engineering professionals.</p> <p style="margin:0;color:#0F172A;font-size:15px;line-height:1.55;">Thanks for subscribing to <strong style="color:#ffffff;">AV Beat</strong> — the digital platform for pro AV, live production, and display tech professionals.</p>
${topicsBlock} ${topicsBlock}
</td></tr> </td></tr>
<tr><td style="padding:18px 36px 8px;"> <tr><td style="padding:18px 36px 8px;">
<p style="margin:0;color:#98a3b3;font-size:14px;line-height:1.6;">You'll get one editorial dispatch a week — industry news, product launches, NAB/IBC coverage, and deep-dives. No spam, no sales calls.</p> <p style="margin:0;color:#475569;font-size:14px;line-height:1.6;">You'll get one editorial dispatch a week — industry news, product launches, InfoComm/ISE coverage, and deep-dives. No spam, no sales calls.</p>
</td></tr> </td></tr>
<tr><td style="padding:18px 36px 26px;" align="center"> <tr><td style="padding:18px 36px 26px;" align="center">
<a href="${siteUrl}/news" style="display:inline-block;background:#3b82f6;color:#ffffff;text-decoration:none;padding:11px 22px;border-radius:6px;font-weight:700;font-size:13px;letter-spacing:0.05em;text-transform:uppercase;">Browse Industry News</a> <a href="${siteUrl}/news" style="display:inline-block;background:#1D4ED8;color:#ffffff;text-decoration:none;padding:11px 22px;border-radius:6px;font-weight:700;font-size:13px;letter-spacing:0.05em;text-transform:uppercase;">Browse AV Industry News</a>
</td></tr> </td></tr>
<tr><td style="padding:18px 36px 28px;border-top:1px solid #1e3a5f;"> <tr><td style="padding:18px 36px 28px;border-top:1px solid #DCE6F2;">
<p style="margin:0;color:#5a6470;font-size:11px;line-height:1.5;">You're receiving this because you signed up at <a href="${siteUrl}" style="color:#3b82f6;text-decoration:none;">avbeat.com</a>. <a href="${siteUrl}/unsubscribe?email=${encodeURIComponent(email)}" style="color:#98a3b3;">Unsubscribe anytime</a>.</p> <p style="margin:0;color:#8a7e6e;font-size:11px;line-height:1.5;">You're receiving this because you signed up at <a href="${siteUrl}" style="color:#1D4ED8;text-decoration:none;">avbeat.com</a>. <a href="${siteUrl}/unsubscribe?email=${encodeURIComponent(email)}" style="color:#475569;">Unsubscribe anytime</a>.</p>
</td></tr> </td></tr>
</table> </table>
</td></tr> </td></tr>

View File

@@ -124,37 +124,37 @@ export async function POST(req: NextRequest) {
const articlesHtml = articles const articlesHtml = articles
.map( .map(
(a: { title: string; excerpt?: string; url?: string; image?: string; category?: string }) => ` (a: { title: string; excerpt?: string; url?: string; image?: string; category?: string }) => `
<div style="border-bottom:1px solid #1e3a5f;padding:20px 0;"> <div style="border-bottom:1px solid #DCE6F2;padding:20px 0;">
${a.category ? `<span style="color:#3b82f6;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:1px;">${a.category}</span>` : ''} ${a.category ? `<span style="color:#1D4ED8;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:1px;">${a.category}</span>` : ''}
<h2 style="color:#e0e0e0;font-size:18px;margin:8px 0 6px;"> <h2 style="color:#e0e0e0;font-size:18px;margin:8px 0 6px;">
${a.url ? `<a href="${siteUrl}${a.url}" style="color:#e0e0e0;text-decoration:none;">${a.title}</a>` : a.title} ${a.url ? `<a href="${siteUrl}${a.url}" style="color:#e0e0e0;text-decoration:none;">${a.title}</a>` : a.title}
</h2> </h2>
${a.excerpt ? `<p style="color:#888;font-size:14px;margin:0 0 10px;">${a.excerpt}</p>` : ''} ${a.excerpt ? `<p style="color:#888;font-size:14px;margin:0 0 10px;">${a.excerpt}</p>` : ''}
${a.url ? `<a href="${siteUrl}${a.url}" style="color:#3b82f6;font-size:13px;text-decoration:none;">Read more →</a>` : ''} ${a.url ? `<a href="${siteUrl}${a.url}" style="color:#1D4ED8;font-size:13px;text-decoration:none;">Read more →</a>` : ''}
</div> </div>
` `
) )
.join(''); .join('');
const emailHtml = ` const emailHtml = `
<div style="background:#0d1520;padding:0;font-family:sans-serif;max-width:600px;margin:0 auto;"> <div style="background:#F8FAFC;padding:0;font-family:sans-serif;max-width:600px;margin:0 auto;">
<!-- Header --> <!-- Header -->
<div style="background:#111;border-bottom:2px solid #3b82f6;padding:24px 32px;"> <div style="background:#111;border-bottom:2px solid #1D4ED8;padding:24px 32px;">
<h1 style="color:#3b82f6;font-size:22px;margin:0;font-weight:bold;">AV Beat</h1> <h1 style="color:#1D4ED8;font-size:22px;margin:0;font-weight:bold;">AV Beat</h1>
<p style="color:#555;font-size:12px;margin:4px 0 0;">News & Intelligence for Broadcast, Post-Production & Streaming Technology</p> <p style="color:#555;font-size:12px;margin:4px 0 0;">Pro AV, Display and Live Production Tech</p>
</div> </div>
<!-- Body --> <!-- Body -->
<div style="padding:32px;"> <div style="padding:32px;">
${previewText ? `<p style="color:#aaa;font-size:15px;margin:0 0 24px;">${previewText}</p>` : ''} ${previewText ? `<p style="color:#aaa;font-size:15px;margin:0 0 24px;">${previewText}</p>` : ''}
${customMessage ? `<div style="background:#1a2535;border-left:3px solid #3b82f6;padding:16px;margin-bottom:24px;"><p style="color:#e0e0e0;font-size:14px;margin:0;">${customMessage}</p></div>` : ''} ${customMessage ? `<div style="background:#FFFFFF;border-left:3px solid #1D4ED8;padding:16px;margin-bottom:24px;"><p style="color:#e0e0e0;font-size:14px;margin:0;">${customMessage}</p></div>` : ''}
${articlesHtml} ${articlesHtml}
</div> </div>
<!-- Footer --> <!-- Footer -->
<div style="background:#0a0f1a;padding:24px 32px;border-top:1px solid #1e3a5f;"> <div style="background:#F8FAFC;padding:24px 32px;border-top:1px solid #DCE6F2;">
<p style="color:#555;font-size:12px;margin:0;">You're receiving this because you subscribed at avbeat.com.</p> <p style="color:#555;font-size:12px;margin:0;">You're receiving this because you subscribed at avbeat.com.</p>
<p style="color:#555;font-size:12px;margin:8px 0 0;"> <p style="color:#555;font-size:12px;margin:8px 0 0;">
<a href="${siteUrl}" style="color:#3b82f6;">Visit AV Beat</a> · <a href="${siteUrl}" style="color:#1D4ED8;">Visit AV Beat</a> ·
<a href="${siteUrl}/unsubscribe?email={{email}}" style="color:#3b82f6;">Unsubscribe</a> <a href="${siteUrl}/unsubscribe?email={{email}}" style="color:#1D4ED8;">Unsubscribe</a>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -38,7 +38,7 @@ export async function POST(req: NextRequest) {
subject_prefix: subject_prefix || '', subject_prefix: subject_prefix || '',
header_text: header_text || '', header_text: header_text || '',
footer_text: footer_text || '', footer_text: footer_text || '',
accent_color: accent_color || '#3b82f6', accent_color: accent_color || '#1D4ED8',
article_blocks: article_blocks || [], article_blocks: article_blocks || [],
is_preset: false, is_preset: false,
}) })

View File

@@ -33,8 +33,7 @@ export async function GET(request: Request) {
source: "imported" as const, source: "imported" as const,
})); }));
// Decorate + reorder so premium and current-advertiser stories surface // Decorate with promotion badges; preserve wp_published_at order.
// at the top of the feed.
const decorated = await decorateHomepageFeed("avbeat", base); const decorated = await decorateHomepageFeed("avbeat", base);
const payload = decorated.map((d) => ({ const payload = decorated.map((d) => ({
...d.article, ...d.article,

View File

@@ -110,8 +110,52 @@ export async function POST(request: NextRequest) {
.trim() .trim()
.slice(0, 240); .slice(0, 240);
// Passthrough mode (skip_rewrite=true): publish the original PR body
// VERBATIM with byline = firm + noindex=true on the wp_imported_posts
// row. No AI rewrite, no Google News indexing, no sitemap. For clients
// flagged in distribute.users.skip_rewrite (e.g. Mike Nann / Writeturn).
const skipRewrite = !!body?.skip_rewrite;
if (skipRewrite) {
const wpId = Math.floor(900_000 + Math.random() * 99_000_000);
const wpSlug = `${baseSlug}-pr-${randSuffix()}`;
const byline = contributor?.contact_name || contributor?.firm_name || 'PR Submission';
const { error: wpErr } = await supabase
.schema('av')
.from('wp_imported_posts')
.insert({
wp_id: wpId,
wp_slug: wpSlug,
title,
excerpt,
content,
author_name: byline,
category: 'Industry News',
status: 'published',
wp_published_at: new Date().toISOString(),
noindex: true,
});
if (wpErr) {
return NextResponse.json({ error: wpErr.message, where: 'passthrough wp_imported_posts insert' }, { status: 500 });
}
// Keep the audit trail in original_submissions even for passthrough.
await supabase.schema('av').from('original_submissions').insert({
submission_id: submissionId,
property,
title,
body: content,
featured_image_url: featuredImage,
attachments,
contributor,
contributor_email: contributor?.email || null,
raw_payload: body,
native_article_id: null,
});
return NextResponse.json({ ok: true, slug: wpSlug, status: 'published_passthrough', noindex: true }, { status: 201 });
}
const { data: article, error: artErr } = await supabase const { data: article, error: artErr } = await supabase
.schema('bb') .schema('av')
.from('native_articles') .from('native_articles')
.insert({ .insert({
slug, slug,
@@ -129,7 +173,7 @@ export async function POST(request: NextRequest) {
if (artErr) return NextResponse.json({ error: artErr.message, where: 'native_articles insert' }, { status: 500 }); if (artErr) return NextResponse.json({ error: artErr.message, where: 'native_articles insert' }, { status: 500 });
const { error: subErr } = await supabase const { error: subErr } = await supabase
.schema('bb') .schema('av')
.from('original_submissions') .from('original_submissions')
.insert({ .insert({
submission_id: submissionId, submission_id: submissionId,

View File

@@ -227,11 +227,11 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
{/* Breadcrumb navigation */} {/* Breadcrumb navigation */}
{/* DO NOT OVERRIDE — article breadcrumb and back navigation */} {/* DO NOT OVERRIDE — article breadcrumb and back navigation */}
<div className="bg-[#0d0d0d] border-b border-[#1a1a1a]"> <div className="bg-[#F8FAFC] border-b border-[#FFFFFF]">
<div className="max-w-container mx-auto px-4 py-2.5 flex items-center gap-2 text-xs font-body text-[#555]"> <div className="max-w-container mx-auto px-4 py-2.5 flex items-center gap-2 text-xs font-body text-[#555]">
<Link href="/home-page" className="hover:text-[#3b82f6] transition-colors">Home</Link> <Link href="/home-page" className="hover:text-[#1D4ED8] transition-colors">Home</Link>
<span>/</span> <span>/</span>
<Link href={sectionHref} className="hover:text-[#3b82f6] transition-colors">{sectionLabel}</Link> <Link href={sectionHref} className="hover:text-[#1D4ED8] transition-colors">{sectionLabel}</Link>
<span>/</span> <span>/</span>
<span className="text-[#888] truncate max-w-[300px]">{article.title}</span> <span className="text-[#888] truncate max-w-[300px]">{article.title}</span>
</div> </div>
@@ -245,14 +245,14 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
{/* Article Header */} {/* Article Header */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<Link href={sectionHref} className="text-xs font-bold uppercase tracking-widest text-[#3b82f6] hover:underline"> <Link href={sectionHref} className="text-xs font-bold uppercase tracking-widest text-[#1D4ED8] hover:underline">
{article.category} {article.category}
</Link> </Link>
<span className="text-xs text-[#777]">{article.date}</span> <span className="text-xs text-[#777]">{article.date}</span>
<span className="text-xs text-[#555]">·</span> <span className="text-xs text-[#555]">·</span>
<span className="text-xs text-[#777]">{article.readTime}</span> <span className="text-xs text-[#777]">{article.readTime}</span>
</div> </div>
<h1 className="font-heading text-3xl md:text-4xl font-bold mb-4 leading-tight text-[#f0f0f0]"> <h1 className="font-serif text-3xl md:text-4xl font-bold mb-4 leading-tight text-[#1e293b]">
{article.title} {article.title}
</h1> </h1>
<div className="mb-4"> <div className="mb-4">
@@ -271,7 +271,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
<div> <div>
<Link <Link
href={`/authors/${article.authorSlug}`} href={`/authors/${article.authorSlug}`}
className="font-heading font-bold text-sm hover:text-[#3b82f6] transition-colors text-[#e0e0e0]"> className="font-serif font-bold text-sm hover:text-[#1D4ED8] transition-colors text-[#1e293b]">
{article.author} {article.author}
</Link> </Link>
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p> <p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
@@ -286,7 +286,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
<> <>
<a href={`mailto:?subject=${encodeURIComponent(article.title)}&body=${encodeURIComponent(articleUrl)}`} <a href={`mailto:?subject=${encodeURIComponent(article.title)}&body=${encodeURIComponent(articleUrl)}`}
aria-label="Share via email" title="Email" aria-label="Share via email" title="Email"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#3b82f6] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#1D4ED8] hover:bg-[#FFFFFF] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#1D4ED8]">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<rect x="2" y="4" width="20" height="16" rx="2" /> <rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /> <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
@@ -294,39 +294,39 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
</a> </a>
<a href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(articleUrl)}`} <a href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(articleUrl)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on LinkedIn" title="LinkedIn" target="_blank" rel="noopener noreferrer" aria-label="Share on LinkedIn" title="LinkedIn"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#0a66c2] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#0a66c2] hover:bg-[#FFFFFF] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/></svg>
</a> </a>
<a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(articleUrl)}&text=${encodeURIComponent(article.title)}&via=BroadcastBeat`} <a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(articleUrl)}&text=${encodeURIComponent(article.title)}&via=BroadcastBeat`}
target="_blank" rel="noopener noreferrer" aria-label="Share on X" title="X (Twitter)" target="_blank" rel="noopener noreferrer" aria-label="Share on X" title="X (Twitter)"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-white hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-white hover:bg-[#FFFFFF] transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a> </a>
<a href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(articleUrl)}`} <a href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(articleUrl)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook" title="Facebook" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook" title="Facebook"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#1877f2] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#1877f2] hover:bg-[#FFFFFF] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.073c0-6.627-5.373-12-12-12S0 5.446 0 12.073C0 18.062 4.388 23.027 10.125 23.927v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.073c0-6.627-5.373-12-12-12S0 5.446 0 12.073C0 18.062 4.388 23.027 10.125 23.927v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
</a> </a>
<a href={`https://www.reddit.com/submit?url=${encodeURIComponent(articleUrl)}&title=${encodeURIComponent(article.title)}`} <a href={`https://www.reddit.com/submit?url=${encodeURIComponent(articleUrl)}&title=${encodeURIComponent(article.title)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on Reddit" title="Reddit" target="_blank" rel="noopener noreferrer" aria-label="Share on Reddit" title="Reddit"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#ff4500] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#ff4500] hover:bg-[#FFFFFF] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/></svg>
</a> </a>
<a href={`https://api.whatsapp.com/send?text=${encodeURIComponent(article.title + " " + articleUrl)}`} <a href={`https://api.whatsapp.com/send?text=${encodeURIComponent(article.title + " " + articleUrl)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on WhatsApp" title="WhatsApp" target="_blank" rel="noopener noreferrer" aria-label="Share on WhatsApp" title="WhatsApp"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#25d366] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#25d366] hover:bg-[#FFFFFF] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M.057 24l1.687-6.163a11.867 11.867 0 0 1-1.587-5.945C.16 5.335 5.495 0 12.05 0a11.817 11.817 0 0 1 8.413 3.488 11.824 11.824 0 0 1 3.48 8.414c-.003 6.557-5.338 11.892-11.893 11.892a11.9 11.9 0 0 1-5.688-1.448L.057 24zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.711.306 1.265.489 1.697.626.713.226 1.362.194 1.875.118.572-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M.057 24l1.687-6.163a11.867 11.867 0 0 1-1.587-5.945C.16 5.335 5.495 0 12.05 0a11.817 11.817 0 0 1 8.413 3.488 11.824 11.824 0 0 1 3.48 8.414c-.003 6.557-5.338 11.892-11.893 11.892a11.9 11.9 0 0 1-5.688-1.448L.057 24zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.711.306 1.265.489 1.697.626.713.226 1.362.194 1.875.118.572-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413z"/></svg>
</a> </a>
<button type="button" <button type="button"
onClick={async () => { try { await navigator.clipboard.writeText(articleUrl); } catch {} }} onClick={async () => { try { await navigator.clipboard.writeText(articleUrl); } catch {} }}
aria-label="Copy article link" title="Copy link" aria-label="Copy article link" title="Copy link"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#3b82f6] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#1D4ED8] hover:bg-[#FFFFFF] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/> <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg> </svg>
</button> </button>
<span className="w-px h-5 bg-[#252525] mx-1" aria-hidden="true" /> <span className="w-px h-5 bg-[#DCE6F2] mx-1" aria-hidden="true" />
</> </>
); );
})()} })()}
@@ -334,29 +334,29 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
onClick={handleSaveToReadingList} onClick={handleSaveToReadingList}
disabled={savingState !== "idle"} disabled={savingState !== "idle"}
aria-label={isSaved ? "Remove from reading list" : "Save to reading list"} aria-label={isSaved ? "Remove from reading list" : "Save to reading list"}
className="text-xs font-bold uppercase tracking-widest text-[#3b82f6] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]"> className="text-xs font-bold uppercase tracking-widest text-[#1D4ED8] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#1D4ED8]">
{savingState === "saving" ? "Saving..." : savingState === "removing" ? "Removing..." : isSaved ? "✓ Saved" : "Save"} {savingState === "saving" ? "Saving..." : savingState === "removing" ? "Removing..." : isSaved ? "✓ Saved" : "Save"}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{/* Featured Image */} {/* Featured Image — editorial standard: cap at 480px high. */}
<div className="mb-8 overflow-hidden rounded-sm"> <figure className="mb-8 overflow-hidden rounded-sm bg-[#F1F5F9] flex items-center justify-center" style={{maxHeight: 480}}>
<AppImage <AppImage
src={article.image} src={article.image}
alt={article.alt} alt={article.alt}
width={800} width={800}
height={450} height={450}
className="w-full h-auto object-cover" className="max-h-[480px] w-auto max-w-full object-contain"
priority priority
/> />
</div> </figure>
{/* Article Body */} {/* Article Body */}
{/* DO NOT OVERRIDE — article body prose styles */} {/* DO NOT OVERRIDE — article body prose styles */}
<div <div
className="bb-article-prose prose prose-invert max-w-none mb-8" className="bb-article-prose prose max-w-none mb-8"
dangerouslySetInnerHTML={{ __html: article.content }} dangerouslySetInnerHTML={{ __html: article.content }}
/> />
@@ -368,7 +368,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
{article.tags.map((tag) => ( {article.tags.map((tag) => (
<span <span
key={tag} key={tag}
className="px-3 py-1 text-xs font-body bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6] transition-colors rounded-sm cursor-default"> className="px-3 py-1 text-xs font-body bg-[#FFFFFF] border border-[#DCE6F2] text-[#888] hover:border-[#1D4ED8] hover:text-[#1D4ED8] transition-colors rounded-sm cursor-default">
{tag} {tag}
</span> </span>
))} ))}
@@ -380,7 +380,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
<div className="pt-6 border-t border-[#222]"> <div className="pt-6 border-t border-[#222]">
<Link <Link
href={sectionHref} href={sectionHref}
className="inline-flex items-center gap-2 text-[#3b82f6] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]"> className="inline-flex items-center gap-2 text-[#1D4ED8] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#1D4ED8]">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<polyline points="15 18 9 12 15 6" /> <polyline points="15 18 9 12 15 6" />
</svg> </svg>
@@ -389,16 +389,19 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
</div> </div>
{/* Article comments — anonymous read, signed-in members can {/* Article comments — anonymous read, signed-in members can
post + vote + reply. AI personas participate too (tagged). */} post + vote + reply. */}
<ArticleComments articleSlug={article.slug} /> <ArticleComments articleSlug={article.slug} />
</div> </div>
{/* Sidebar */} {/* Sidebar — every right-rail block (ads, Related Articles, Related
<aside className="lg:col-span-4 space-y-6"> Forum Posts) shares the same 300px column width on lg+ so left +
right edges align cleanly with the ad inventory below. */}
<aside className="lg:col-span-4">
<div className="space-y-6 w-full lg:w-[300px] lg:ml-auto">
{/* Related Articles */} {/* Related Articles */}
{/* DO NOT OVERRIDE — related articles sidebar */} {/* DO NOT OVERRIDE — related articles sidebar */}
<div className="bg-[#111] border border-[#222] p-5"> <div className="bg-[#111] border border-[#222] p-5">
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]"> <h3 className="font-body font-bold text-xs text-[#1D4ED8] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
Related Articles Related Articles
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
@@ -417,10 +420,10 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
/> />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-[#3b82f6] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5"> <p className="text-[#1D4ED8] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
{related.category} {related.category}
</p> </p>
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug"> <p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#1D4ED8] transition-colors line-clamp-2 leading-snug">
{related.title} {related.title}
</p> </p>
</div> </div>
@@ -435,19 +438,19 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
recently-active threads so the box is never empty. */} recently-active threads so the box is never empty. */}
{relatedForumThreads.length > 0 && ( {relatedForumThreads.length > 0 && (
<div className="bg-[#111] border border-[#222] p-5"> <div className="bg-[#111] border border-[#222] p-5">
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]"> <h3 className="font-body font-bold text-xs text-[#1D4ED8] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
Related Forum Posts Related Forum Posts
</h3> </h3>
<ul className="space-y-3.5"> <ul className="space-y-3.5">
{relatedForumThreads.map((t) => ( {relatedForumThreads.map((t) => (
<li key={t.id}> <li key={t.id}>
<Link href={`/forum/thread/${t.id}`} className="group block"> <Link href={`/forum/thread/${t.id}`} className="group block">
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug"> <p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#1D4ED8] transition-colors line-clamp-2 leading-snug">
{t.title} {t.title}
</p> </p>
<p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]"> <p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]">
{t.category_name && ( {t.category_name && (
<span className="text-[#3b82f6] font-body font-bold uppercase tracking-wider"> <span className="text-[#1D4ED8] font-body font-bold uppercase tracking-wider">
{t.category_name} {t.category_name}
</span> </span>
)} )}
@@ -461,7 +464,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
</ul> </ul>
<Link <Link
href="/forum" href="/forum"
className="block mt-4 pt-3 border-t border-[#222] text-[11px] text-[#3b82f6] hover:underline font-body font-semibold uppercase tracking-wider" className="block mt-4 pt-3 border-t border-[#222] text-[11px] text-[#1D4ED8] hover:underline font-body font-semibold uppercase tracking-wider"
> >
Browse all forum threads Browse all forum threads
</Link> </Link>
@@ -470,6 +473,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
{/* Sidebar ad stack — 300x600 + 300x250 rotating */} {/* Sidebar ad stack — 300x600 + 300x250 rotating */}
<SidebarAdStack /> <SidebarAdStack />
</div>
</aside> </aside>
</div> </div>
</article> </article>
@@ -479,7 +483,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
<div <div
role="status" role="status"
aria-live="polite" aria-live="polite"
className="fixed bottom-4 right-4 bg-[#1a1a1a] border border-[#3b82f6] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50"> className="fixed bottom-4 right-4 bg-[#FFFFFF] border border-[#1D4ED8] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50">
{saveToast} {saveToast}
</div> </div>
)} )}

View File

@@ -118,7 +118,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
}; };
return ( return (
<section className="mt-10 pt-8 border-t border-[#2a2a2a]"> <section className="mt-10 pt-8 border-t border-[#DCE6F2]">
{/* Section Header */} {/* Section Header */}
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<span className="section-label">Comments</span> <span className="section-label">Comments</span>
@@ -127,15 +127,15 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
{comments.length} {comments.length === 1 ? "comment" : "comments"} {comments.length} {comments.length === 1 ? "comment" : "comments"}
</span> </span>
)} )}
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#DCE6F2]" />
</div> </div>
{/* Comment Form */} {/* Comment Form */}
{user ? ( {user ? (
<form onSubmit={handleSubmit} className="mb-8"> <form onSubmit={handleSubmit} className="mb-8">
<div className="flex gap-3"> <div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-[#1e3a5f] border border-[#2a3a50] flex items-center justify-center flex-shrink-0 mt-0.5"> <div className="w-8 h-8 rounded-full bg-[#DCE6F2] border border-[#2a3a50] flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="font-body text-[10px] font-bold text-[#3b82f6]"> <span className="font-body text-[10px] font-bold text-[#1D4ED8]">
{getInitials(user.user_metadata?.full_name || user.email || "U")} {getInitials(user.user_metadata?.full_name || user.email || "U")}
</span> </span>
</div> </div>
@@ -146,10 +146,10 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
placeholder="Share your thoughts on this article..." placeholder="Share your thoughts on this article..."
rows={3} rows={3}
maxLength={1000} maxLength={1000}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444] resize-none" className="w-full bg-[#FFFFFF] border border-[#DCE6F2] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#1D4ED8] focus:ring-1 focus:ring-[#1D4ED8] transition-colors placeholder-[#444] resize-none"
/> />
{error && ( {error && (
<p className="font-body text-xs text-[#cc0000] mt-1.5">{error}</p> <p className="font-body text-xs text-[#1D4ED8] mt-1.5">{error}</p>
)} )}
<div className="flex items-center justify-between mt-2"> <div className="flex items-center justify-between mt-2">
<span className="font-body text-[11px] text-[#555]"> <span className="font-body text-[11px] text-[#555]">
@@ -158,7 +158,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
<button <button
type="submit" type="submit"
disabled={submitting || !newComment.trim()} disabled={submitting || !newComment.trim()}
className="bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body text-xs font-bold uppercase tracking-wide px-5 py-2 rounded-sm transition-colors disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]"> className="bg-[#1D4ED8] hover:bg-[#1E3A8A] text-white font-body text-xs font-bold uppercase tracking-wide px-5 py-2 rounded-sm transition-colors disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#1D4ED8]">
{submitting ? "Posting..." : "Post Comment"} {submitting ? "Posting..." : "Post Comment"}
</button> </button>
</div> </div>
@@ -166,14 +166,14 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
</div> </div>
</form> </form>
) : ( ) : (
<div className="mb-8 bg-[#0d1520] border border-[#1e3a5f] rounded-sm p-5 flex items-center justify-between gap-4"> <div className="mb-8 bg-[#0d1520] border border-[#DCE6F2] rounded-sm p-5 flex items-center justify-between gap-4">
<div> <div>
<p className="font-body text-sm font-bold text-[#e0e0e0] mb-0.5">Join the conversation</p> <p className="font-body text-sm font-bold text-[#e0e0e0] mb-0.5">Join the conversation</p>
<p className="font-body text-xs text-[#666]">Sign in to post a comment on this article.</p> <p className="font-body text-xs text-[#666]">Sign in to post a comment on this article.</p>
</div> </div>
<Link <Link
href="/login" href="/login"
className="flex-shrink-0 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body text-xs font-bold uppercase tracking-wide px-5 py-2 rounded-sm transition-colors"> className="flex-shrink-0 bg-[#1D4ED8] hover:bg-[#1E3A8A] text-white font-body text-xs font-bold uppercase tracking-wide px-5 py-2 rounded-sm transition-colors">
Sign In Sign In
</Link> </Link>
</div> </div>
@@ -208,7 +208,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
const isOwn = user?.id === comment.user_id; const isOwn = user?.id === comment.user_id;
return ( return (
<div key={comment.id} className="flex gap-3 group"> <div key={comment.id} className="flex gap-3 group">
<div className="w-8 h-8 rounded-full bg-[#1a2535] border border-[#2a3a50] flex items-center justify-center flex-shrink-0 mt-0.5 overflow-hidden"> <div className="w-8 h-8 rounded-full bg-[#FFFFFF] border border-[#2a3a50] flex items-center justify-center flex-shrink-0 mt-0.5 overflow-hidden">
{comment.user_profiles?.avatar_url ? ( {comment.user_profiles?.avatar_url ? (
<img <img
src={comment.user_profiles.avatar_url} src={comment.user_profiles.avatar_url}
@@ -216,7 +216,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
<span className="font-body text-[10px] font-bold text-[#3b82f6]"> <span className="font-body text-[10px] font-bold text-[#1D4ED8]">
{getInitials(name)} {getInitials(name)}
</span> </span>
)} )}
@@ -230,7 +230,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
onClick={() => handleDelete(comment.id)} onClick={() => handleDelete(comment.id)}
disabled={deletingId === comment.id} disabled={deletingId === comment.id}
aria-label="Delete comment" aria-label="Delete comment"
className="ml-auto opacity-0 group-hover:opacity-100 font-body text-[11px] text-[#555] hover:text-[#cc0000] transition-all disabled:opacity-30"> className="ml-auto opacity-0 group-hover:opacity-100 font-body text-[11px] text-[#555] hover:text-[#1D4ED8] transition-all disabled:opacity-30">
{deletingId === comment.id ? "Deleting..." : "Delete"} {deletingId === comment.id ? "Deleting..." : "Delete"}
</button> </button>
)} )}

View File

@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { redirect } from "next/navigation"; import { cleanExcerpt } from "@/lib/articles/excerpt";
import { notFound } from "next/navigation";
import ArticleDetailPage from "./ArticleDetailClient"; import ArticleDetailPage from "./ArticleDetailClient";
import type { Article } from "@/lib/articles/types"; import type { Article } from "@/lib/articles/types";
import { import {
@@ -9,7 +10,10 @@ import {
getRelatedForumThreads, type RelatedForumThread, getRelatedForumThreads, type RelatedForumThread,
} from "@/lib/articles/legacy-source"; } from "@/lib/articles/legacy-source";
export const revalidate = 3600; // Deleted or unpublished posts must 404 immediately — never serve a stale
// cached article shell for up to an hour after DB removal.
export const dynamic = "force-dynamic";
export const revalidate = 0;
export const dynamicParams = true; export const dynamicParams = true;
interface PageProps { interface PageProps {
@@ -51,14 +55,19 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
if (!article) { if (!article) {
return { title: "Article Not Found", description: "The article you are looking for does not exist." }; return { title: "Article Not Found", description: "The article you are looking for does not exist." };
} }
const noindex = !!article.noindex;
return { return {
title: article.title, title: article.title,
description: article.excerpt, description: cleanExcerpt(article.excerpt),
alternates: { canonical: `/articles/${article.slug}` }, alternates: { canonical: `/articles/${article.slug}` },
robots: noindex
? { index: false, follow: false, nocache: true, googleBot: { index: false, follow: false, noimageindex: true } }
: undefined,
other: noindex ? { "googlebot-news": "noindex" } : undefined,
openGraph: { openGraph: {
type: "article", type: "article",
title: article.title, title: article.title,
description: article.excerpt, description: cleanExcerpt(article.excerpt),
url: `/articles/${article.slug}`, url: `/articles/${article.slug}`,
images: [{ url: article.image, width: 1200, height: 630, alt: article.alt }], images: [{ url: article.image, width: 1200, height: 630, alt: article.alt }],
authors: [article.author], authors: [article.author],
@@ -68,7 +77,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
title: article.title, title: article.title,
description: article.excerpt, description: cleanExcerpt(article.excerpt),
images: [article.image], images: [article.image],
creator: "@AV Beat", creator: "@AV Beat",
}, },
@@ -84,16 +93,14 @@ export default async function ArticlePage({ params }: PageProps) {
const { slug } = await params; const { slug } = await params;
const { article, related, relatedForumThreads } = await loadArticle(slug); const { article, related, relatedForumThreads } = await loadArticle(slug);
if (!article) { if (!article) {
// Slug isn't in any of our article tables — send the reader to the news notFound();
// index so a click from the homepage/ticker doesn't dead-end on 404.
redirect("/news");
} }
const articleSchema = { const articleSchema = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Article", "@type": "Article",
headline: article.title, headline: article.title,
description: article.excerpt, description: cleanExcerpt(article.excerpt),
image: [absoluteImage(article.image)], image: [absoluteImage(article.image)],
author: { "@type": "Person", name: article.author }, author: { "@type": "Person", name: article.author },
publisher: { publisher: {

View File

@@ -55,7 +55,7 @@ export default function ClientReadout() {
}, []); }, []);
return ( return (
<pre style={{ padding: 20, fontFamily: "monospace", whiteSpace: "pre-wrap", background: "#1a1a1a", color: "#bbb", borderTop: "1px solid #333" }}> <pre style={{ padding: 20, fontFamily: "monospace", whiteSpace: "pre-wrap", background: "#FFFFFF", color: "#bbb", borderTop: "1px solid #333" }}>
{JSON.stringify(out, null, 2)} {JSON.stringify(out, null, 2)}
</pre> </pre>
); );

View File

@@ -89,7 +89,7 @@ export default function AuthorProfilePage() {
excerpt: a.excerpt || "", excerpt: a.excerpt || "",
category: a.category || "News", category: a.category || "News",
date: a.date || "", date: a.date || "",
image: a.image || "/assets/images/article-placeholder.svg", image: a.image || "/assets/images/article-placeholder-v2.svg",
alt: a.alt || a.title, alt: a.alt || a.title,
readTime: a.readTime || "1 min read", readTime: a.readTime || "1 min read",
})); }));
@@ -113,7 +113,7 @@ export default function AuthorProfilePage() {
return ( return (
<> <>
<Header /> <Header />
<main className="bg-[#111111] min-h-screen"> <main className="bg-[#F8FAFC] min-h-screen">
<aside className="float-right ml-6 mb-6 hidden lg:block max-w-[300px]"><SidebarAdStack /></aside> <aside className="float-right ml-6 mb-6 hidden lg:block max-w-[300px]"><SidebarAdStack /></aside>
{/* Cover Image */} {/* Cover Image */}
<div className="relative h-40 md:h-52 overflow-hidden"> <div className="relative h-40 md:h-52 overflow-hidden">
@@ -126,14 +126,14 @@ export default function AuthorProfilePage() {
className="w-full h-full object-cover opacity-40" className="w-full h-full object-cover opacity-40"
/> />
)} )}
<div className="absolute inset-0 bg-gradient-to-t from-[#111111] via-[#111111]/60 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-[#F8FAFC] via-[#F8FAFC]/60 to-transparent" />
</div> </div>
<div className="max-w-container mx-auto px-4"> <div className="max-w-container mx-auto px-4">
{/* Author Header */} {/* Author Header */}
<div className="relative -mt-16 md:-mt-20 mb-6"> <div className="relative -mt-16 md:-mt-20 mb-6">
<div className="flex flex-col sm:flex-row sm:items-end gap-4"> <div className="flex flex-col sm:flex-row sm:items-end gap-4">
<div className="w-24 h-24 md:w-28 md:h-28 rounded-full overflow-hidden border-4 border-[#111111] flex-shrink-0 bg-[#1a1a1a]"> <div className="w-24 h-24 md:w-28 md:h-28 rounded-full overflow-hidden border-4 border-[#F8FAFC] flex-shrink-0 bg-[#FFFFFF]">
{profile && ( {profile && (
<AppImage <AppImage
src={profile.avatar_url} src={profile.avatar_url}
@@ -145,10 +145,10 @@ export default function AuthorProfilePage() {
)} )}
</div> </div>
<div className="pb-1 flex-1"> <div className="pb-1 flex-1">
<h1 className="font-heading text-2xl md:text-3xl font-bold text-[#e8e8e8] leading-tight"> <h1 className="font-heading text-2xl md:text-3xl font-bold text-[#0F172A] leading-tight">
{loading ? "Loading…" : profile?.display_name || "Author"} {loading ? "Loading…" : profile?.display_name || "Author"}
</h1> </h1>
<p className="font-body text-sm text-[#3b82f6] mt-0.5"> <p className="font-body text-sm text-[#1D4ED8] mt-0.5">
{profile?.role || "Contributor"} {profile?.role || "Contributor"}
</p> </p>
<div className="flex flex-wrap items-center gap-3 mt-2"> <div className="flex flex-wrap items-center gap-3 mt-2">
@@ -180,7 +180,7 @@ export default function AuthorProfilePage() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label={`${profile.display_name} on Twitter`} aria-label={`${profile.display_name} on Twitter`}
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#3b82f6] hover:text-[#3b82f6] text-[#888] transition-colors rounded-sm" className="w-8 h-8 flex items-center justify-center bg-[#FFFFFF] border border-[#DCE6F2] hover:border-[#1D4ED8] hover:text-[#1D4ED8] text-[#888] transition-colors rounded-sm"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /> <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
@@ -193,7 +193,7 @@ export default function AuthorProfilePage() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label={`${profile.display_name} on LinkedIn`} aria-label={`${profile.display_name} on LinkedIn`}
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#3b82f6] hover:text-[#3b82f6] text-[#888] transition-colors rounded-sm" className="w-8 h-8 flex items-center justify-center bg-[#FFFFFF] border border-[#DCE6F2] hover:border-[#1D4ED8] hover:text-[#1D4ED8] text-[#888] transition-colors rounded-sm"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" /> <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
@@ -204,7 +204,7 @@ export default function AuthorProfilePage() {
<a <a
href={`mailto:${profile.email}`} href={`mailto:${profile.email}`}
aria-label={`Email ${profile.display_name}`} aria-label={`Email ${profile.display_name}`}
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#3b82f6] hover:text-[#3b82f6] text-[#888] transition-colors rounded-sm" className="w-8 h-8 flex items-center justify-center bg-[#FFFFFF] border border-[#DCE6F2] hover:border-[#1D4ED8] hover:text-[#1D4ED8] text-[#888] transition-colors rounded-sm"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" /> <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
@@ -223,7 +223,7 @@ export default function AuthorProfilePage() {
{profile.specialties.map((spec) => ( {profile.specialties.map((spec) => (
<span <span
key={spec} key={spec}
className="font-body text-xs text-[#3b82f6] bg-[#1e3a5f]/30 border border-[#1e3a5f] px-2.5 py-1 rounded-sm" className="font-body text-xs text-[#1D4ED8] bg-[#DCE6F2]/30 border border-[#DCE6F2] px-2.5 py-1 rounded-sm"
> >
{spec} {spec}
</span> </span>
@@ -232,12 +232,12 @@ export default function AuthorProfilePage() {
)} )}
{/* Tabs */} {/* Tabs */}
<div className="flex gap-0 border-b border-[#2a2a2a] mb-6"> <div className="flex gap-0 border-b border-[#DCE6F2] mb-6">
<button <button
onClick={() => setActiveTab("articles")} onClick={() => setActiveTab("articles")}
className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors ${ className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors ${
activeTab === "articles" activeTab === "articles"
? "border-[#3b82f6] text-[#3b82f6]" ? "border-[#1D4ED8] text-[#1D4ED8]"
: "border-transparent text-[#666] hover:text-[#aaa]" : "border-transparent text-[#666] hover:text-[#aaa]"
}`} }`}
> >
@@ -247,7 +247,7 @@ export default function AuthorProfilePage() {
onClick={() => setActiveTab("about")} onClick={() => setActiveTab("about")}
className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors ${ className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors ${
activeTab === "about" activeTab === "about"
? "border-[#3b82f6] text-[#3b82f6]" ? "border-[#1D4ED8] text-[#1D4ED8]"
: "border-transparent text-[#666] hover:text-[#aaa]" : "border-transparent text-[#666] hover:text-[#aaa]"
}`} }`}
> >
@@ -271,11 +271,11 @@ export default function AuthorProfilePage() {
<Link <Link
key={article.slug} key={article.slug}
href={`/articles/${article.slug}`} href={`/articles/${article.slug}`}
className="group bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#3b82f6] transition-colors rounded-sm overflow-hidden article-card" className="group bg-[#FFFFFF] border border-[#DCE6F2] hover:border-[#1D4ED8] transition-colors rounded-sm overflow-hidden article-card"
> >
<div className="img-zoom h-44 overflow-hidden"> <div className="img-zoom h-44 overflow-hidden">
<AppImage <AppImage
src={article.image || "/assets/images/article-placeholder.svg"} src={article.image || "/assets/images/article-placeholder-v2.svg"}
alt={article.alt} alt={article.alt}
width={400} width={400}
height={176} height={176}
@@ -284,13 +284,13 @@ export default function AuthorProfilePage() {
</div> </div>
<div className="p-4"> <div className="p-4">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#3b82f6]"> <span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#1D4ED8]">
{article.category} {article.category}
</span> </span>
<span className="font-body text-[10px] text-[#555]">·</span> <span className="font-body text-[10px] text-[#555]">·</span>
<span className="font-body text-[10px] text-[#555]">{article.readTime}</span> <span className="font-body text-[10px] text-[#555]">{article.readTime}</span>
</div> </div>
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#3b82f6] transition-colors leading-snug mb-2 line-clamp-2"> <h3 className="font-heading text-sm font-bold text-[#0F172A] group-hover:text-[#1D4ED8] transition-colors leading-snug mb-2 line-clamp-2">
{article.title} {article.title}
</h3> </h3>
<p className="font-body text-xs text-[#777] leading-relaxed line-clamp-2 mb-3"> <p className="font-body text-xs text-[#777] leading-relaxed line-clamp-2 mb-3">
@@ -308,10 +308,10 @@ export default function AuthorProfilePage() {
{/* About tab */} {/* About tab */}
{activeTab === "about" && profile && ( {activeTab === "about" && profile && (
<div className="max-w-3xl pb-10 space-y-5"> <div className="max-w-3xl pb-10 space-y-5">
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-6">
<p className="section-label mb-4">Biography</p> <p className="section-label mb-4">Biography</p>
{profile.bio ? ( {profile.bio ? (
<p className="font-body text-sm text-[#aaaaaa] leading-relaxed whitespace-pre-line"> <p className="font-body text-sm text-[#1e293b] leading-relaxed whitespace-pre-line">
{profile.bio} {profile.bio}
</p> </p>
) : ( ) : (
@@ -328,7 +328,7 @@ export default function AuthorProfilePage() {
profile.address_line1 || profile.address_line1 ||
profile.city || profile.city ||
profile.country) && ( profile.country) && (
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-6">
<p className="section-label mb-4">Contact</p> <p className="section-label mb-4">Contact</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{addressLines.length > 0 && ( {addressLines.length > 0 && (
@@ -336,7 +336,7 @@ export default function AuthorProfilePage() {
<p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1"> <p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1">
Address Address
</p> </p>
<address className="font-body text-sm text-[#cccccc] not-italic leading-relaxed"> <address className="font-body text-sm text-[#1e293b] not-italic leading-relaxed">
{addressLines.map((line, i) => ( {addressLines.map((line, i) => (
<span key={i}> <span key={i}>
{line} {line}
@@ -353,7 +353,7 @@ export default function AuthorProfilePage() {
</p> </p>
<a <a
href={`tel:${profile.phone.replace(/\s+/g, "")}`} href={`tel:${profile.phone.replace(/\s+/g, "")}`}
className="font-body text-sm text-[#3b82f6] hover:underline" className="font-body text-sm text-[#1D4ED8] hover:underline"
> >
{profile.phone} {profile.phone}
</a> </a>
@@ -366,7 +366,7 @@ export default function AuthorProfilePage() {
</p> </p>
<a <a
href={`mailto:${profile.email}`} href={`mailto:${profile.email}`}
className="font-body text-sm text-[#3b82f6] hover:underline break-all" className="font-body text-sm text-[#1D4ED8] hover:underline break-all"
> >
{profile.email} {profile.email}
</a> </a>
@@ -381,7 +381,7 @@ export default function AuthorProfilePage() {
href={profile.website} href={profile.website}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-body text-sm text-[#3b82f6] hover:underline break-all" className="font-body text-sm text-[#1D4ED8] hover:underline break-all"
> >
{profile.website.replace(/^https?:\/\//, "").replace(/\/$/, "")} {profile.website.replace(/^https?:\/\//, "").replace(/\/$/, "")}
</a> </a>
@@ -392,7 +392,7 @@ export default function AuthorProfilePage() {
)} )}
{/* Profile facts */} {/* Profile facts */}
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-sm p-6">
<p className="section-label mb-4">Profile</p> <p className="section-label mb-4">Profile</p>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{locationLine && ( {locationLine && (
@@ -400,7 +400,7 @@ export default function AuthorProfilePage() {
<p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1"> <p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1">
Location Location
</p> </p>
<p className="font-body text-sm text-[#cccccc]">{locationLine}</p> <p className="font-body text-sm text-[#1e293b]">{locationLine}</p>
</div> </div>
)} )}
{profile.joined_label && ( {profile.joined_label && (
@@ -408,14 +408,14 @@ export default function AuthorProfilePage() {
<p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1"> <p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1">
Member Since Member Since
</p> </p>
<p className="font-body text-sm text-[#cccccc]">{profile.joined_label}</p> <p className="font-body text-sm text-[#1e293b]">{profile.joined_label}</p>
</div> </div>
)} )}
<div> <div>
<p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1"> <p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1">
Articles Published Articles Published
</p> </p>
<p className="font-body text-sm text-[#cccccc]"> <p className="font-body text-sm text-[#1e293b]">
{articleTotal.toLocaleString()} {articleTotal.toLocaleString()}
</p> </p>
</div> </div>
@@ -423,7 +423,7 @@ export default function AuthorProfilePage() {
<p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1"> <p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1">
Role Role
</p> </p>
<p className="font-body text-sm text-[#cccccc]">{profile.role}</p> <p className="font-body text-sm text-[#1e293b]">{profile.role}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -283,7 +283,7 @@ export default function ContributorDashboard() {
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors text-left ${ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors text-left ${
view === item.id view === item.id
? 'bg-[#c9a84c]/10 text-[#c9a84c] border border-[#c9a84c]/20' ? 'bg-[#c9a84c]/10 text-[#c9a84c] border border-[#c9a84c]/20'
: 'text-[#888] hover:text-white hover:bg-[#1a1a1a]' : 'text-[#888] hover:text-white hover:bg-[#FFFFFF]'
}`} }`}
> >
<span className="text-base w-5 text-center">{item.icon}</span> <span className="text-base w-5 text-center">{item.icon}</span>
@@ -339,7 +339,7 @@ export default function ContributorDashboard() {
<button onClick={() => setView('new-post')} className="px-5 py-2.5 bg-[#c9a84c] text-black font-semibold rounded-lg text-sm hover:bg-[#b8973b] transition-colors"> <button onClick={() => setView('new-post')} className="px-5 py-2.5 bg-[#c9a84c] text-black font-semibold rounded-lg text-sm hover:bg-[#b8973b] transition-colors">
+ New Post + New Post
</button> </button>
<button onClick={() => setView('my-posts')} className="px-5 py-2.5 bg-[#1a1a1a] border border-[#333] text-white rounded-lg text-sm hover:bg-[#222] transition-colors"> <button onClick={() => setView('my-posts')} className="px-5 py-2.5 bg-[#FFFFFF] border border-[#333] text-white rounded-lg text-sm hover:bg-[#222] transition-colors">
View My Posts View My Posts
</button> </button>
</div> </div>
@@ -453,7 +453,7 @@ export default function ContributorDashboard() {
{submittingPost ? 'Submitting...' : profile?.role === 'contributor' ? 'Submit for Review' : 'Publish Post'} {submittingPost ? 'Submitting...' : profile?.role === 'contributor' ? 'Submit for Review' : 'Publish Post'}
</button> </button>
<button type="button" onClick={() => setView('overview')} <button type="button" onClick={() => setView('overview')}
className="px-5 py-2.5 bg-[#1a1a1a] border border-[#333] text-[#888] rounded-lg text-sm hover:text-white hover:bg-[#222] transition-colors"> className="px-5 py-2.5 bg-[#FFFFFF] border border-[#333] text-[#888] rounded-lg text-sm hover:text-white hover:bg-[#222] transition-colors">
Cancel Cancel
</button> </button>
</div> </div>

View File

@@ -47,7 +47,7 @@ interface Stats {
} }
const PUBLICATIONS = [ const PUBLICATIONS = [
{ id: 'broadcast-beat', name: 'AV Beat', short: 'BB' }, { id: 'avbeat', name: 'AV Beat', short: 'AVB' },
{ id: 'av-beat', name: 'AV Beat', short: 'AV' }, { id: 'av-beat', name: 'AV Beat', short: 'AV' },
]; ];

View File

@@ -214,7 +214,7 @@ function renderMarkdown(text: string): string {
.replace(/^# (.+)$/gm, '<h1 class="text-xl font-bold text-white mt-5 mb-2">$1</h1>') .replace(/^# (.+)$/gm, '<h1 class="text-xl font-bold text-white mt-5 mb-2">$1</h1>')
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-white font-semibold">$1</strong>') .replace(/\*\*(.+?)\*\*/g, '<strong class="text-white font-semibold">$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>') .replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code class="bg-[#1a2535] text-[#3b82f6] px-1 py-0.5 rounded text-sm font-mono">$1</code>') .replace(/`([^`]+)`/g, '<code class="bg-[#FFFFFF] text-[#1D4ED8] px-1 py-0.5 rounded text-sm font-mono">$1</code>')
.replace(/^- (.+)$/gm, '<li class="ml-4 list-disc text-[#aaa]">$1</li>') .replace(/^- (.+)$/gm, '<li class="ml-4 list-disc text-[#aaa]">$1</li>')
.replace(/^\d+\. (.+)$/gm, '<li class="ml-4 list-decimal text-[#aaa]">$1</li>') .replace(/^\d+\. (.+)$/gm, '<li class="ml-4 list-decimal text-[#aaa]">$1</li>')
.replace(/\n\n/g, '</p><p class="mb-3 text-[#ccc] leading-relaxed">') .replace(/\n\n/g, '</p><p class="mb-3 text-[#ccc] leading-relaxed">')
@@ -294,7 +294,7 @@ function DraftPreviewModal({ content, title, penName, siteId, onClose, onSave }:
</button> </button>
<button <button
onClick={() => onSave(true)} onClick={() => onSave(true)}
className="bg-[#3b82f6] hover:bg-[#2563eb] text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium" className="bg-[#1D4ED8] hover:bg-[#1E3A8A] text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium"
> >
Open in Editor Open in Editor
</button> </button>
@@ -353,7 +353,7 @@ function RegenerateModal({ onClose, onRegenerate }: RegenerateModalProps) {
onClick={() => { setSelectedVariant(v.prompt); setCustomPrompt(''); }} onClick={() => { setSelectedVariant(v.prompt); setCustomPrompt(''); }}
className={`text-left text-xs px-3 py-2 rounded border transition-colors ${ className={`text-left text-xs px-3 py-2 rounded border transition-colors ${
selectedVariant === v.prompt selectedVariant === v.prompt
? 'border-[#3b82f6] bg-[#1e3a5f] text-white' ? 'border-[#1D4ED8] bg-[#DCE6F2] text-white'
: 'border-[#1e2a3a] text-[#aaa] hover:border-[#2a3a50] hover:text-white hover:bg-[#161f2e]' : 'border-[#1e2a3a] text-[#aaa] hover:border-[#2a3a50] hover:text-white hover:bg-[#161f2e]'
}`} }`}
> >
@@ -370,7 +370,7 @@ function RegenerateModal({ onClose, onRegenerate }: RegenerateModalProps) {
onChange={e => { setCustomPrompt(e.target.value); setSelectedVariant(null); }} onChange={e => { setCustomPrompt(e.target.value); setSelectedVariant(null); }}
placeholder="e.g. Rewrite this focusing on the European market impact..." placeholder="e.g. Rewrite this focusing on the European market impact..."
rows={3} rows={3}
className="w-full bg-[#0d1117] border border-[#1e2a3a] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#3b82f6] placeholder-[#333] resize-none" className="w-full bg-[#0d1117] border border-[#1e2a3a] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#1D4ED8] placeholder-[#333] resize-none"
/> />
</div> </div>
@@ -386,7 +386,7 @@ function RegenerateModal({ onClose, onRegenerate }: RegenerateModalProps) {
<button onClick={onClose} className="text-[#555] hover:text-[#aaa] text-sm transition-colors">Cancel</button> <button onClick={onClose} className="text-[#555] hover:text-[#aaa] text-sm transition-colors">Cancel</button>
<button <button
onClick={handleSubmit} onClick={handleSubmit}
className="bg-[#3b82f6] hover:bg-[#2563eb] text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium" className="bg-[#1D4ED8] hover:bg-[#1E3A8A] text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium"
> >
Regenerate Regenerate
</button> </button>
@@ -418,7 +418,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
const found = PEN_NAME_PROFILES.find(p => p.name === e.target.value); const found = PEN_NAME_PROFILES.find(p => p.name === e.target.value);
if (found) side === 'left' ? setLeftPen(found) : setRightPen(found); if (found) side === 'left' ? setLeftPen(found) : setRightPen(found);
}} }}
className="w-full bg-[#0d1117] border border-[#1e2a3a] text-white text-sm rounded px-2 py-1.5 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#1e2a3a] text-white text-sm rounded px-2 py-1.5 focus:outline-none focus:border-[#1D4ED8]"
> >
{PEN_NAME_PROFILES.map(p => ( {PEN_NAME_PROFILES.map(p => (
<option key={p.name} value={p.name}>{p.name} ({p.site})</option> <option key={p.name} value={p.name}>{p.name} ({p.site})</option>
@@ -434,7 +434,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
</span> </span>
<span className="text-white font-semibold text-sm">{profile.name}</span> <span className="text-white font-semibold text-sm">{profile.name}</span>
</div> </div>
<p className="text-[#3b82f6] text-xs">{profile.title}</p> <p className="text-[#1D4ED8] text-xs">{profile.title}</p>
</div> </div>
<div> <div>
@@ -458,7 +458,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
<div> <div>
<p className="text-[#555] text-[10px] uppercase tracking-wider font-bold mb-1">Example Lead</p> <p className="text-[#555] text-[10px] uppercase tracking-wider font-bold mb-1">Example Lead</p>
<p className="text-[#aaa] text-xs italic leading-relaxed border-l-2 border-[#3b82f6]/40 pl-3">"{profile.exampleLead}"</p> <p className="text-[#aaa] text-xs italic leading-relaxed border-l-2 border-[#1D4ED8]/40 pl-3">"{profile.exampleLead}"</p>
</div> </div>
<button <button
@@ -485,7 +485,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
<button <button
key={f} key={f}
onClick={() => setFilterSite(f)} onClick={() => setFilterSite(f)}
className={`text-xs px-2.5 py-1 rounded transition-colors ${filterSite === f ? 'bg-[#3b82f6] text-white' : 'bg-[#1e2a3a] text-[#aaa] hover:text-white'}`} className={`text-xs px-2.5 py-1 rounded transition-colors ${filterSite === f ? 'bg-[#1D4ED8] text-white' : 'bg-[#1e2a3a] text-[#aaa] hover:text-white'}`}
> >
{f === 'ALL' ? 'All Writers' : f === 'BB' ? 'AV Beat' : 'AV Beat'} {f === 'ALL' ? 'All Writers' : f === 'BB' ? 'AV Beat' : 'AV Beat'}
</button> </button>
@@ -514,7 +514,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
> >
<div className="flex items-center gap-2 mb-0.5"> <div className="flex items-center gap-2 mb-0.5">
<span className={`text-[9px] font-bold px-1 rounded ${p.site === 'AV' ? 'bg-orange-900/40 text-orange-400' : 'bg-blue-900/40 text-blue-400'}`}>{p.site}</span> <span className={`text-[9px] font-bold px-1 rounded ${p.site === 'AV' ? 'bg-orange-900/40 text-orange-400' : 'bg-blue-900/40 text-blue-400'}`}>{p.site}</span>
<span className="text-white text-xs font-medium group-hover:text-[#3b82f6] transition-colors">{p.name}</span> <span className="text-white text-xs font-medium group-hover:text-[#1D4ED8] transition-colors">{p.name}</span>
</div> </div>
<p className="text-[#555] text-[10px]">{p.title}</p> <p className="text-[#555] text-[10px]">{p.title}</p>
</button> </button>
@@ -560,14 +560,14 @@ function ResponseHistorySidebar({ messages, onJumpTo, onClose }: ResponseHistory
className="w-full text-left p-3 rounded border border-[#1e2a3a] hover:border-[#2a3a50] hover:bg-[#161f2e] transition-colors group" className="w-full text-left p-3 rounded border border-[#1e2a3a] hover:border-[#2a3a50] hover:bg-[#161f2e] transition-colors group"
> >
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[#3b82f6] font-bold">Response #{idx + 1}</span> <span className="text-[10px] text-[#1D4ED8] font-bold">Response #{idx + 1}</span>
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${msg.provider === 'ollama' ? 'bg-green-500' : 'bg-yellow-500'}`} /> <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${msg.provider === 'ollama' ? 'bg-green-500' : 'bg-yellow-500'}`} />
</div> </div>
<p className="text-[#aaa] text-xs line-clamp-3 leading-relaxed">{preview}</p> <p className="text-[#aaa] text-xs line-clamp-3 leading-relaxed">{preview}</p>
<div className="flex items-center gap-3 mt-2"> <div className="flex items-center gap-3 mt-2">
<span className="text-[10px] text-[#444]">{msg.timestamp.toLocaleTimeString()}</span> <span className="text-[10px] text-[#444]">{msg.timestamp.toLocaleTimeString()}</span>
<span className="text-[10px] text-[#444]">{wordCount} words</span> <span className="text-[10px] text-[#444]">{wordCount} words</span>
<span className="text-[10px] text-[#3b82f6] opacity-0 group-hover:opacity-100 transition-opacity">Jump </span> <span className="text-[10px] text-[#1D4ED8] opacity-0 group-hover:opacity-100 transition-opacity">Jump </span>
</div> </div>
</button> </button>
); );
@@ -927,7 +927,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
{/* Header */} {/* Header */}
<div className="bg-[#111827] border-b border-[#1e2a3a] px-4 py-3 flex items-center justify-between"> <div className="bg-[#111827] border-b border-[#1e2a3a] px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-[#3b82f6] text-sm transition-colors"> Dashboard</Link> <Link href="/admin" className="text-[#555] hover:text-[#1D4ED8] text-sm transition-colors"> Dashboard</Link>
<span className="text-[#333]">/</span> <span className="text-[#333]">/</span>
<h1 className="text-white font-bold text-sm tracking-wide">AI Console</h1> <h1 className="text-white font-bold text-sm tracking-wide">AI Console</h1>
</div> </div>
@@ -943,18 +943,18 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
onClick={() => setShowHistorySidebar(v => !v)} onClick={() => setShowHistorySidebar(v => !v)}
className={`text-xs px-2.5 py-1.5 rounded border transition-colors ${ className={`text-xs px-2.5 py-1.5 rounded border transition-colors ${
showHistorySidebar showHistorySidebar
? 'bg-[#1e3a5f] border-[#3b82f6]/40 text-[#3b82f6]' ? 'bg-[#DCE6F2] border-[#1D4ED8]/40 text-[#1D4ED8]'
: 'bg-[#1e2a3a] border-[#2a3a50] text-[#555] hover:text-[#aaa]' : 'bg-[#1e2a3a] border-[#2a3a50] text-[#555] hover:text-[#aaa]'
}`} }`}
title="Toggle response history sidebar" title="Toggle response history sidebar"
> >
History History
{assistantMessageCount > 0 && ( {assistantMessageCount > 0 && (
<span className="ml-1.5 bg-[#3b82f6] text-white text-[9px] font-bold px-1 py-0.5 rounded-full">{assistantMessageCount}</span> <span className="ml-1.5 bg-[#1D4ED8] text-white text-[9px] font-bold px-1 py-0.5 rounded-full">{assistantMessageCount}</span>
)} )}
</button> </button>
<Link href="/dashboard/ai-console/health" className="text-[#555] hover:text-[#3b82f6] text-xs transition-colors">Health</Link> <Link href="/dashboard/ai-console/health" className="text-[#555] hover:text-[#1D4ED8] text-xs transition-colors">Health</Link>
<Link href="/dashboard/ai-console/settings" className="text-[#555] hover:text-[#3b82f6] text-xs transition-colors">Settings</Link> <Link href="/dashboard/ai-console/settings" className="text-[#555] hover:text-[#1D4ED8] text-xs transition-colors">Settings</Link>
<button onClick={handleNewSession} className="text-xs bg-[#1e2a3a] hover:bg-[#253347] text-[#aaa] px-3 py-1.5 rounded transition-colors"> <button onClick={handleNewSession} className="text-xs bg-[#1e2a3a] hover:bg-[#253347] text-[#aaa] px-3 py-1.5 rounded transition-colors">
+ New Session + New Session
</button> </button>
@@ -1002,7 +1002,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
key={session.id} key={session.id}
className={`group p-2.5 rounded border cursor-pointer transition-colors ${ className={`group p-2.5 rounded border cursor-pointer transition-colors ${
currentSessionId === session.id currentSessionId === session.id
? 'border-[#3b82f6]/40 bg-[#1e2a3a]' ? 'border-[#1D4ED8]/40 bg-[#1e2a3a]'
: 'border-[#1e2a3a] hover:border-[#2a3a50] hover:bg-[#161f2e]' : 'border-[#1e2a3a] hover:border-[#2a3a50] hover:bg-[#161f2e]'
}`} }`}
> >
@@ -1046,11 +1046,11 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
onClick={() => { onClick={() => {
fetch('/api/ai/console?action=ping').then(r => r.json()).then(setAiStatus).catch(() => {}); fetch('/api/ai/console?action=ping').then(r => r.json()).then(setAiStatus).catch(() => {});
}} }}
className="text-[10px] text-[#555] hover:text-[#3b82f6] transition-colors" className="text-[10px] text-[#555] hover:text-[#1D4ED8] transition-colors"
> >
Refresh Refresh
</button> </button>
<Link href="/dashboard/ai-console/health" className="text-[10px] text-[#555] hover:text-[#3b82f6] transition-colors"> <Link href="/dashboard/ai-console/health" className="text-[10px] text-[#555] hover:text-[#1D4ED8] transition-colors">
Health Dashboard Health Dashboard
</Link> </Link>
</div> </div>
@@ -1066,7 +1066,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
<select <select
value={selectedSite} value={selectedSite}
onChange={e => setSelectedSite(Number(e.target.value) as 1 | 2)} onChange={e => setSelectedSite(Number(e.target.value) as 1 | 2)}
className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#3b82f6]" className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#1D4ED8]"
> >
<option value={1}>AV Beat</option> <option value={1}>AV Beat</option>
<option value={2}>AV Beat</option> <option value={2}>AV Beat</option>
@@ -1077,7 +1077,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
<select <select
value={selectedPenName} value={selectedPenName}
onChange={e => setSelectedPenName(e.target.value)} onChange={e => setSelectedPenName(e.target.value)}
className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#3b82f6]" className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#1D4ED8]"
> >
{penNames.map(name => ( {penNames.map(name => (
<option key={name} value={name}>{name}</option> <option key={name} value={name}>{name}</option>
@@ -1086,7 +1086,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
</div> </div>
<button <button
onClick={() => setShowStyleGuides(true)} onClick={() => setShowStyleGuides(true)}
className="text-[10px] text-[#3b82f6] hover:text-[#60a5fa] transition-colors ml-auto" className="text-[10px] text-[#1D4ED8] hover:text-[#1D4ED8] transition-colors ml-auto"
> >
View style guide View style guide
</button> </button>
@@ -1123,7 +1123,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
</div> </div>
)} )}
<div className={`rounded-lg px-4 py-3 ${ <div className={`rounded-lg px-4 py-3 ${
msg.role === 'user' ?'bg-[#1e3a5f] text-white text-sm' :'bg-[#111827] border border-[#1e2a3a] text-[#ccc] text-sm' msg.role === 'user' ?'bg-[#DCE6F2] text-white text-sm' :'bg-[#111827] border border-[#1e2a3a] text-[#ccc] text-sm'
}`}> }`}>
{msg.role === 'user' ? ( {msg.role === 'user' ? (
<p className="whitespace-pre-wrap">{msg.content}</p> <p className="whitespace-pre-wrap">{msg.content}</p>
@@ -1151,7 +1151,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
</button> </button>
<button <button
onClick={() => setShowDraftPreview(true)} onClick={() => setShowDraftPreview(true)}
className="text-[10px] text-[#3b82f6] hover:text-[#60a5fa] bg-[#111827] border border-[#3b82f6]/30 px-2 py-1 rounded transition-colors font-medium" className="text-[10px] text-[#1D4ED8] hover:text-[#1D4ED8] bg-[#111827] border border-[#1D4ED8]/30 px-2 py-1 rounded transition-colors font-medium"
> >
Preview Draft Preview Draft
</button> </button>
@@ -1195,9 +1195,9 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
<div className="flex justify-start"> <div className="flex justify-start">
<div className="bg-[#111827] border border-[#1e2a3a] rounded-lg px-4 py-3"> <div className="bg-[#111827] border border-[#1e2a3a] rounded-lg px-4 py-3">
<div className="flex gap-1"> <div className="flex gap-1">
<div className="w-1.5 h-1.5 bg-[#3b82f6] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} /> <div className="w-1.5 h-1.5 bg-[#1D4ED8] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<div className="w-1.5 h-1.5 bg-[#3b82f6] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} /> <div className="w-1.5 h-1.5 bg-[#1D4ED8] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<div className="w-1.5 h-1.5 bg-[#3b82f6] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} /> <div className="w-1.5 h-1.5 bg-[#1D4ED8] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div> </div>
</div> </div>
</div> </div>
@@ -1225,13 +1225,13 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
onChange={e => setUrlInput(e.target.value)} onChange={e => setUrlInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleFetchUrl()} onKeyDown={e => e.key === 'Enter' && handleFetchUrl()}
placeholder="https://example.com/press-release" placeholder="https://example.com/press-release"
className="flex-1 bg-[#1e2a3a] border border-[#2a3a50] text-white text-sm rounded px-3 py-2 focus:outline-none focus:border-[#3b82f6] placeholder-[#444]" className="flex-1 bg-[#1e2a3a] border border-[#2a3a50] text-white text-sm rounded px-3 py-2 focus:outline-none focus:border-[#1D4ED8] placeholder-[#444]"
autoFocus autoFocus
/> />
<button <button
onClick={handleFetchUrl} onClick={handleFetchUrl}
disabled={fetchingUrl} disabled={fetchingUrl}
className="bg-[#3b82f6] hover:bg-[#2563eb] text-white text-xs px-3 py-2 rounded transition-colors disabled:opacity-50" className="bg-[#1D4ED8] hover:bg-[#1E3A8A] text-white text-xs px-3 py-2 rounded transition-colors disabled:opacity-50"
> >
{fetchingUrl ? 'Fetching...' : 'Add'} {fetchingUrl ? 'Fetching...' : 'Add'}
</button> </button>
@@ -1244,19 +1244,19 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
<div className="flex gap-2 mb-2"> <div className="flex gap-2 mb-2">
<button <button
onClick={() => setShowUrlInput(!showUrlInput)} onClick={() => setShowUrlInput(!showUrlInput)}
className="text-xs text-[#555] hover:text-[#3b82f6] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors" className="text-xs text-[#555] hover:text-[#1D4ED8] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors"
> >
+ Add URL + Add URL
</button> </button>
<button <button
onClick={() => setInput(prev => prev + '\n\n[Paste story content here for reference]')} onClick={() => setInput(prev => prev + '\n\n[Paste story content here for reference]')}
className="text-xs text-[#555] hover:text-[#3b82f6] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors" className="text-xs text-[#555] hover:text-[#1D4ED8] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors"
> >
+ Add Story + Add Story
</button> </button>
<button <button
onClick={() => setInput(prev => prev + `\n\nWrite in the style of ${selectedPenName}.`)} onClick={() => setInput(prev => prev + `\n\nWrite in the style of ${selectedPenName}.`)}
className="text-xs text-[#555] hover:text-[#3b82f6] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors" className="text-xs text-[#555] hover:text-[#1D4ED8] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors"
> >
+ Add Style + Add Style
</button> </button>
@@ -1274,13 +1274,13 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
}} }}
placeholder="Ask the AI to write a story, generate headlines, translate content..." placeholder="Ask the AI to write a story, generate headlines, translate content..."
rows={3} rows={3}
className="flex-1 bg-[#111827] border border-[#1e2a3a] text-white text-sm rounded-lg px-4 py-3 focus:outline-none focus:border-[#3b82f6] placeholder-[#333] resize-none" className="flex-1 bg-[#111827] border border-[#1e2a3a] text-white text-sm rounded-lg px-4 py-3 focus:outline-none focus:border-[#1D4ED8] placeholder-[#333] resize-none"
/> />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<button <button
onClick={() => handleSend()} onClick={() => handleSend()}
disabled={isStreaming || !input.trim()} disabled={isStreaming || !input.trim()}
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium flex-1" className="bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium flex-1"
> >
{isStreaming ? ( {isStreaming ? (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">

View File

@@ -117,7 +117,7 @@ export default function AIConsoleSettingsPage() {
if (loading || loadingSettings) { if (loading || loadingSettings) {
return ( return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center"> <div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#3b82f6]/30 border-t-[#3b82f6] rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8]/30 border-t-[#1D4ED8] rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -127,14 +127,14 @@ export default function AIConsoleSettingsPage() {
{/* Header */} {/* Header */}
<div className="bg-[#111827] border-b border-[#1e2a3a] px-6 py-4 flex items-center justify-between"> <div className="bg-[#111827] border-b border-[#1e2a3a] px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/ai-console" className="text-[#555] hover:text-[#3b82f6] text-sm transition-colors"> AI Console</Link> <Link href="/dashboard/ai-console" className="text-[#555] hover:text-[#1D4ED8] text-sm transition-colors"> AI Console</Link>
<span className="text-[#333]">/</span> <span className="text-[#333]">/</span>
<h1 className="text-white font-bold text-sm">AI Console Settings</h1> <h1 className="text-white font-bold text-sm">AI Console Settings</h1>
</div> </div>
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium" className="bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium"
> >
{saving ? 'Saving...' : 'Save Settings'} {saving ? 'Saving...' : 'Save Settings'}
</button> </button>
@@ -177,7 +177,7 @@ export default function AIConsoleSettingsPage() {
value={settings.ollama_endpoint} value={settings.ollama_endpoint}
onChange={e => update('ollama_endpoint', e.target.value)} onChange={e => update('ollama_endpoint', e.target.value)}
placeholder="http://192.168.1.x:11434 or https://your-tunnel.ngrok.io" placeholder="http://192.168.1.x:11434 or https://your-tunnel.ngrok.io"
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6] placeholder-[#333]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8] placeholder-[#333]"
/> />
<p className="text-[#444] text-[10px] mt-1"> <p className="text-[#444] text-[10px] mt-1">
Use direct IP if on the same network, or a tunnel URL (ngrok, Cloudflare Tunnel, Tailscale) for remote access. Use direct IP if on the same network, or a tunnel URL (ngrok, Cloudflare Tunnel, Tailscale) for remote access.
@@ -212,7 +212,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.ollama_model_default} value={settings.ollama_model_default}
onChange={e => update('ollama_model_default', e.target.value)} onChange={e => update('ollama_model_default', e.target.value)}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8]"
> >
<option value="">Auto (largest available)</option> <option value="">Auto (largest available)</option>
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)} {connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
@@ -223,7 +223,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.ollama_model_translation} value={settings.ollama_model_translation}
onChange={e => update('ollama_model_translation', e.target.value)} onChange={e => update('ollama_model_translation', e.target.value)}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8]"
> >
<option value="">Same as default</option> <option value="">Same as default</option>
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)} {connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
@@ -234,7 +234,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.ollama_model_background} value={settings.ollama_model_background}
onChange={e => update('ollama_model_background', e.target.value)} onChange={e => update('ollama_model_background', e.target.value)}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8]"
> >
<option value="">Same as default</option> <option value="">Same as default</option>
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)} {connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
@@ -250,7 +250,7 @@ export default function AIConsoleSettingsPage() {
</div> </div>
<button <button
onClick={() => update('auto_failover', !settings.auto_failover)} onClick={() => update('auto_failover', !settings.auto_failover)}
className={`w-10 h-5 rounded-full transition-colors relative ${settings.auto_failover ? 'bg-[#3b82f6]' : 'bg-[#2a3a50]'}`} className={`w-10 h-5 rounded-full transition-colors relative ${settings.auto_failover ? 'bg-[#1D4ED8]' : 'bg-[#2a3a50]'}`}
> >
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.auto_failover ? 'translate-x-5' : 'translate-x-0.5'}`} /> <div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.auto_failover ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button> </button>
@@ -263,7 +263,7 @@ export default function AIConsoleSettingsPage() {
value={settings.failover_timeout_seconds} value={settings.failover_timeout_seconds}
onChange={e => update('failover_timeout_seconds', Number(e.target.value))} onChange={e => update('failover_timeout_seconds', Number(e.target.value))}
min={3} max={30} min={3} max={30}
className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#3b82f6]" className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
</div> </div>
@@ -301,7 +301,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.default_site_id} value={settings.default_site_id}
onChange={e => update('default_site_id', Number(e.target.value))} onChange={e => update('default_site_id', Number(e.target.value))}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8]"
> >
<option value={1}>AV Beat</option> <option value={1}>AV Beat</option>
<option value={2}>AV Beat</option> <option value={2}>AV Beat</option>
@@ -312,7 +312,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.default_pen_name_bb} value={settings.default_pen_name_bb}
onChange={e => update('default_pen_name_bb', e.target.value)} onChange={e => update('default_pen_name_bb', e.target.value)}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8]"
> >
{BB_PEN_NAMES.map(n => <option key={n} value={n}>{n}</option>)} {BB_PEN_NAMES.map(n => <option key={n} value={n}>{n}</option>)}
</select> </select>
@@ -322,7 +322,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.default_pen_name_av} value={settings.default_pen_name_av}
onChange={e => update('default_pen_name_av', e.target.value)} onChange={e => update('default_pen_name_av', e.target.value)}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8]"
> >
{AV_PEN_NAMES.map(n => <option key={n} value={n}>{n}</option>)} {AV_PEN_NAMES.map(n => <option key={n} value={n}>{n}</option>)}
</select> </select>
@@ -335,7 +335,7 @@ export default function AIConsoleSettingsPage() {
</div> </div>
<button <button
onClick={() => update('save_session_history', !settings.save_session_history)} onClick={() => update('save_session_history', !settings.save_session_history)}
className={`w-10 h-5 rounded-full transition-colors relative ${settings.save_session_history ? 'bg-[#3b82f6]' : 'bg-[#2a3a50]'}`} className={`w-10 h-5 rounded-full transition-colors relative ${settings.save_session_history ? 'bg-[#1D4ED8]' : 'bg-[#2a3a50]'}`}
> >
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.save_session_history ? 'translate-x-5' : 'translate-x-0.5'}`} /> <div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.save_session_history ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button> </button>
@@ -346,7 +346,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.history_retention_days} value={settings.history_retention_days}
onChange={e => update('history_retention_days', Number(e.target.value))} onChange={e => update('history_retention_days', Number(e.target.value))}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8]"
> >
<option value={30}>30 days</option> <option value={30}>30 days</option>
<option value={60}>60 days</option> <option value={60}>60 days</option>
@@ -361,7 +361,7 @@ export default function AIConsoleSettingsPage() {
</div> </div>
<button <button
onClick={() => update('streaming_enabled', !settings.streaming_enabled)} onClick={() => update('streaming_enabled', !settings.streaming_enabled)}
className={`w-10 h-5 rounded-full transition-colors relative ${settings.streaming_enabled ? 'bg-[#3b82f6]' : 'bg-[#2a3a50]'}`} className={`w-10 h-5 rounded-full transition-colors relative ${settings.streaming_enabled ? 'bg-[#1D4ED8]' : 'bg-[#2a3a50]'}`}
> >
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.streaming_enabled ? 'translate-x-5' : 'translate-x-0.5'}`} /> <div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.streaming_enabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button> </button>
@@ -380,7 +380,7 @@ export default function AIConsoleSettingsPage() {
</div> </div>
<button <button
onClick={() => update('background_use_local', !settings.background_use_local)} onClick={() => update('background_use_local', !settings.background_use_local)}
className={`w-10 h-5 rounded-full transition-colors relative ${settings.background_use_local ? 'bg-[#3b82f6]' : 'bg-[#2a3a50]'}`} className={`w-10 h-5 rounded-full transition-colors relative ${settings.background_use_local ? 'bg-[#1D4ED8]' : 'bg-[#2a3a50]'}`}
> >
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.background_use_local ? 'translate-x-5' : 'translate-x-0.5'}`} /> <div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.background_use_local ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button> </button>
@@ -393,7 +393,7 @@ export default function AIConsoleSettingsPage() {
value={settings.background_timeout_seconds} value={settings.background_timeout_seconds}
onChange={e => update('background_timeout_seconds', Number(e.target.value))} onChange={e => update('background_timeout_seconds', Number(e.target.value))}
min={30} max={300} min={30} max={300}
className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#3b82f6]" className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
@@ -404,7 +404,7 @@ export default function AIConsoleSettingsPage() {
</div> </div>
<button <button
onClick={() => update('priority_queue_enabled', !settings.priority_queue_enabled)} onClick={() => update('priority_queue_enabled', !settings.priority_queue_enabled)}
className={`w-10 h-5 rounded-full transition-colors relative ${settings.priority_queue_enabled ? 'bg-[#3b82f6]' : 'bg-[#2a3a50]'}`} className={`w-10 h-5 rounded-full transition-colors relative ${settings.priority_queue_enabled ? 'bg-[#1D4ED8]' : 'bg-[#2a3a50]'}`}
> >
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.priority_queue_enabled ? 'translate-x-5' : 'translate-x-0.5'}`} /> <div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.priority_queue_enabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button> </button>
@@ -422,7 +422,7 @@ export default function AIConsoleSettingsPage() {
'Classification and extraction tasks', 'Classification and extraction tasks',
].map((item, i) => ( ].map((item, i) => (
<li key={i} className="flex items-center gap-2 text-xs text-[#555]"> <li key={i} className="flex items-center gap-2 text-xs text-[#555]">
<span className="w-4 h-4 rounded-full bg-[#1e2a3a] text-[#3b82f6] text-[10px] flex items-center justify-center font-bold flex-shrink-0">{i + 1}</span> <span className="w-4 h-4 rounded-full bg-[#1e2a3a] text-[#1D4ED8] text-[10px] flex items-center justify-center font-bold flex-shrink-0">{i + 1}</span>
{item} {item}
</li> </li>
))} ))}

View File

@@ -130,9 +130,9 @@ export default function OverviewTab({ companyId, selectedCompany }: Props) {
<Line <Line
type="monotone" type="monotone"
dataKey="revenue" dataKey="revenue"
stroke={selectedCompany === 'rmp' ? '#3b82f6' : '#a855f7'} stroke={selectedCompany === 'rmp' ? '#1D4ED8' : '#a855f7'}
strokeWidth={2} strokeWidth={2}
dot={{ fill: selectedCompany === 'rmp' ? '#3b82f6' : '#a855f7', r: 3 }} dot={{ fill: selectedCompany === 'rmp' ? '#1D4ED8' : '#a855f7', r: 3 }}
/> />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>

View File

@@ -269,13 +269,13 @@ export default function EditorialDashboard() {
function SortIcon({ col }: { col: typeof sortBy }) { function SortIcon({ col }: { col: typeof sortBy }) {
if (sortBy !== col) return <span className="text-[#444] ml-1"></span>; if (sortBy !== col) return <span className="text-[#444] ml-1"></span>;
return <span className="text-[#3b82f6] ml-1">{sortDir === 'desc' ? '↓' : '↑'}</span>; return <span className="text-[#1D4ED8] ml-1">{sortDir === 'desc' ? '↓' : '↑'}</span>;
} }
if (loading || !authChecked) { if (loading || !authChecked) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -285,7 +285,7 @@ export default function EditorialDashboard() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0]"> <div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0]">
{/* Header */} {/* Header */}
<div className="border-b border-[#1e1e1e] bg-[#0d0d0d]"> <div className="border-b border-[#1e1e1e] bg-[#F8FAFC]">
<div className="max-w-[1400px] mx-auto px-6 py-5 flex items-center justify-between gap-4"> <div className="max-w-[1400px] mx-auto px-6 py-5 flex items-center justify-between gap-4">
<div> <div>
<h1 className="text-xl font-bold text-white tracking-tight">Editorial Dashboard</h1> <h1 className="text-xl font-bold text-white tracking-tight">Editorial Dashboard</h1>
@@ -294,13 +294,13 @@ export default function EditorialDashboard() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link <Link
href="/admin/articles" href="/admin/articles"
className="px-3 py-1.5 text-xs font-semibold bg-[#3b82f6] hover:bg-blue-500 text-white rounded transition-colors" className="px-3 py-1.5 text-xs font-semibold bg-[#1D4ED8] hover:bg-blue-500 text-white rounded transition-colors"
> >
+ New Article + New Article
</Link> </Link>
<Link <Link
href="/dashboard/adops" href="/dashboard/adops"
className="px-3 py-1.5 text-xs font-semibold bg-[#1a1a1a] hover:bg-[#252525] text-[#aaa] border border-[#2a2a2a] rounded transition-colors" className="px-3 py-1.5 text-xs font-semibold bg-[#FFFFFF] hover:bg-[#DCE6F2] text-[#aaa] border border-[#DCE6F2] rounded transition-colors"
> >
Ad Ops Ad Ops
</Link> </Link>
@@ -325,7 +325,7 @@ export default function EditorialDashboard() {
<button <button
key={card.label} key={card.label}
onClick={card.onClick} onClick={card.onClick}
className="bg-[#111] border border-[#1e1e1e] rounded-lg p-4 text-left hover:border-[#2a2a2a] transition-colors cursor-pointer" className="bg-[#111] border border-[#1e1e1e] rounded-lg p-4 text-left hover:border-[#DCE6F2] transition-colors cursor-pointer"
> >
<div className={`text-xl font-bold ${card.color}`}>{card.value}</div> <div className={`text-xl font-bold ${card.color}`}>{card.value}</div>
<div className="text-[11px] text-[#666] mt-1 uppercase tracking-wider">{card.label}</div> <div className="text-[11px] text-[#666] mt-1 uppercase tracking-wider">{card.label}</div>
@@ -344,7 +344,7 @@ export default function EditorialDashboard() {
onClick={() => setStatusFilter(s)} onClick={() => setStatusFilter(s)}
className={`px-3 py-1.5 text-xs font-semibold capitalize transition-colors ${ className={`px-3 py-1.5 text-xs font-semibold capitalize transition-colors ${
statusFilter === s statusFilter === s
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'text-[#666] hover:text-[#aaa]' : 'text-[#666] hover:text-[#aaa]'
}`} }`}
> >
@@ -360,7 +360,7 @@ export default function EditorialDashboard() {
placeholder="Search articles, clients, campaigns..." placeholder="Search articles, clients, campaigns..."
value={searchQuery} value={searchQuery}
onChange={e => setSearchQuery(e.target.value)} onChange={e => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#1e1e1e] rounded-lg px-3 py-1.5 text-xs text-[#ccc] placeholder-[#444] focus:outline-none focus:border-[#3b82f6] transition-colors" className="w-full bg-[#111] border border-[#1e1e1e] rounded-lg px-3 py-1.5 text-xs text-[#ccc] placeholder-[#444] focus:outline-none focus:border-[#1D4ED8] transition-colors"
/> />
{searchQuery && ( {searchQuery && (
<button <button
@@ -376,10 +376,10 @@ export default function EditorialDashboard() {
</div> </div>
{/* Pipeline Table */} {/* Pipeline Table */}
<div className="bg-[#0d0d0d] border border-[#1e1e1e] rounded-xl overflow-hidden"> <div className="bg-[#F8FAFC] border border-[#1e1e1e] rounded-xl overflow-hidden">
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : filtered.length === 0 ? ( ) : filtered.length === 0 ? (
<div className="text-center py-20 text-[#444] text-sm">No articles found</div> <div className="text-center py-20 text-[#444] text-sm">No articles found</div>
@@ -387,7 +387,7 @@ export default function EditorialDashboard() {
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr className="border-b border-[#1a1a1a] bg-[#0a0a0a]"> <tr className="border-b border-[#FFFFFF] bg-[#0a0a0a]">
<th className="text-left px-4 py-3 text-[#555] font-semibold uppercase tracking-wider w-[30%]">Article</th> <th className="text-left px-4 py-3 text-[#555] font-semibold uppercase tracking-wider w-[30%]">Article</th>
<th className="text-left px-3 py-3 text-[#555] font-semibold uppercase tracking-wider">Status</th> <th className="text-left px-3 py-3 text-[#555] font-semibold uppercase tracking-wider">Status</th>
<th <th
@@ -437,7 +437,7 @@ export default function EditorialDashboard() {
<span className="text-[#555]">{article.author_name}</span> <span className="text-[#555]">{article.author_name}</span>
)} )}
{article.category && ( {article.category && (
<span className="text-[#3b82f6]/70 bg-[#3b82f6]/10 px-1.5 py-0.5 rounded text-[10px]"> <span className="text-[#1D4ED8]/70 bg-[#1D4ED8]/10 px-1.5 py-0.5 rounded text-[10px]">
{article.category} {article.category}
</span> </span>
)} )}

View File

@@ -256,7 +256,7 @@ export default function ShowCalendarPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -281,7 +281,7 @@ export default function ShowCalendarPage() {
)} )}
{/* Header */} {/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0f0f0f]"> <div className="border-b border-[#FFFFFF] bg-[#0f0f0f]">
<div className="max-w-7xl mx-auto px-4 py-4"> <div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -294,13 +294,13 @@ export default function ShowCalendarPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
onClick={() => { setEditingEvent({ site_id: 1, year: new Date().getFullYear(), seo_tier: 3, event_type: 'trade_show' }); setShowEventModal(true); }} onClick={() => { setEditingEvent({ site_id: 1, year: new Date().getFullYear(), seo_tier: 3, event_type: 'trade_show' }); setShowEventModal(true); }}
className="px-3 py-1.5 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors" className="px-3 py-1.5 bg-[#1D4ED8] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors"
> >
+ Add Event + Add Event
</button> </button>
<button <button
onClick={() => setShowGenerateModal(true)} onClick={() => setShowGenerateModal(true)}
className="px-3 py-1.5 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-white text-sm rounded-lg transition-colors" className="px-3 py-1.5 bg-[#FFFFFF] hover:bg-[#DCE6F2] border border-[#333] text-white text-sm rounded-lg transition-colors"
> >
Generate Story Generate Story
</button> </button>
@@ -316,8 +316,8 @@ export default function ShowCalendarPage() {
onClick={() => setSiteFilter(s)} onClick={() => setSiteFilter(s)}
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${ className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
siteFilter === s siteFilter === s
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]' : 'bg-[#FFFFFF] text-[#888] hover:text-white border border-[#333]'
}`} }`}
> >
{s === '' ? 'All Sites' : s === '1' ? '🔵 AV Beat' : '🟠 AV Beat'} {s === '' ? 'All Sites' : s === '1' ? '🔵 AV Beat' : '🟠 AV Beat'}
@@ -326,14 +326,14 @@ export default function ShowCalendarPage() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 mt-4 border-b border-[#1a1a1a]"> <div className="flex gap-1 mt-4 border-b border-[#FFFFFF]">
{tabs.map(tab => ( {tabs.map(tab => (
<button <button
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${ className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
activeTab === tab.id activeTab === tab.id
? 'border-[#3b82f6] text-white' ? 'border-[#1D4ED8] text-white'
: 'border-transparent text-[#666] hover:text-white' : 'border-transparent text-[#666] hover:text-white'
}`} }`}
> >
@@ -347,7 +347,7 @@ export default function ShowCalendarPage() {
<div className="max-w-7xl mx-auto px-4 py-6"> <div className="max-w-7xl mx-auto px-4 py-6">
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : ( ) : (
<> <>
@@ -383,7 +383,7 @@ export default function ShowCalendarPage() {
<span>Engine: {formatDate(event.story_engine_start_date)} {formatDate(event.story_engine_end_date)}</span> <span>Engine: {formatDate(event.story_engine_start_date)} {formatDate(event.story_engine_end_date)}</span>
{event.official_site && ( {event.official_site && (
<a href={`https://${event.official_site}`} target="_blank" rel="noopener noreferrer" <a href={`https://${event.official_site}`} target="_blank" rel="noopener noreferrer"
className="text-[#3b82f6] hover:underline">{event.official_site}</a> className="text-[#1D4ED8] hover:underline">{event.official_site}</a>
)} )}
</div> </div>
<div className="flex items-center gap-3 mt-2 text-xs text-[#555]"> <div className="flex items-center gap-3 mt-2 text-xs text-[#555]">
@@ -400,13 +400,13 @@ export default function ShowCalendarPage() {
<button <button
onClick={() => handleExtractStyle(event.event_name)} onClick={() => handleExtractStyle(event.event_name)}
disabled={actionLoading === 'style-' + event.event_name} disabled={actionLoading === 'style-' + event.event_name}
className="px-2 py-1 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors disabled:opacity-50" className="px-2 py-1 bg-[#FFFFFF] hover:bg-[#DCE6F2] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors disabled:opacity-50"
> >
{actionLoading === 'style-' + event.event_name ? 'Extracting...' : 'Extract Style'} {actionLoading === 'style-' + event.event_name ? 'Extracting...' : 'Extract Style'}
</button> </button>
<button <button
onClick={() => { setEditingEvent(event); setShowEventModal(true); }} onClick={() => { setEditingEvent(event); setShowEventModal(true); }}
className="px-2 py-1 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors" className="px-2 py-1 bg-[#FFFFFF] hover:bg-[#DCE6F2] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors"
> >
Edit Dates Edit Dates
</button> </button>
@@ -440,8 +440,8 @@ export default function ShowCalendarPage() {
onClick={() => setQueueStatus(s)} onClick={() => setQueueStatus(s)}
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${ className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
queueStatus === s queueStatus === s
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]' : 'bg-[#FFFFFF] text-[#888] hover:text-white border border-[#333]'
}`} }`}
> >
{s.replace('_', ' ')} {s.replace('_', ' ')}
@@ -460,7 +460,7 @@ export default function ShowCalendarPage() {
<div className="flex items-center gap-2 mb-1 flex-wrap"> <div className="flex items-center gap-2 mb-1 flex-wrap">
<SiteBadge siteId={item.site_id} /> <SiteBadge siteId={item.site_id} />
{item.source_show && ( {item.source_show && (
<span className="text-xs text-[#555] bg-[#1a1a1a] px-1.5 py-0.5 rounded">{item.source_show}</span> <span className="text-xs text-[#555] bg-[#FFFFFF] px-1.5 py-0.5 rounded">{item.source_show}</span>
)} )}
{item.show_phase && ( {item.show_phase && (
<span className={`text-xs ${PHASE_LABELS[item.show_phase]?.color || 'text-gray-500'}`}> <span className={`text-xs ${PHASE_LABELS[item.show_phase]?.color || 'text-gray-500'}`}>
@@ -470,7 +470,7 @@ export default function ShowCalendarPage() {
<span className={`text-xs px-1.5 py-0.5 rounded ${ <span className={`text-xs px-1.5 py-0.5 rounded ${
item.status === 'published' ? 'bg-green-900/20 text-green-400' : item.status === 'published' ? 'bg-green-900/20 text-green-400' :
item.status === 'rejected' ? 'bg-red-900/20 text-red-400' : item.status === 'rejected' ? 'bg-red-900/20 text-red-400' :
item.status === 'generated'? 'bg-blue-900/20 text-blue-400' : 'bg-[#1a1a1a] text-[#888]' item.status === 'generated'? 'bg-blue-900/20 text-blue-400' : 'bg-[#FFFFFF] text-[#888]'
}`}>{item.status}</span> }`}>{item.status}</span>
{item.quality_confidence !== null && ( {item.quality_confidence !== null && (
<span className={`text-xs ${item.quality_confidence >= 0.85 ? 'text-green-400' : item.quality_confidence >= 0.7 ? 'text-yellow-400' : 'text-red-400'}`}> <span className={`text-xs ${item.quality_confidence >= 0.85 ? 'text-green-400' : item.quality_confidence >= 0.7 ? 'text-yellow-400' : 'text-red-400'}`}>
@@ -490,7 +490,7 @@ export default function ShowCalendarPage() {
{/* Expanded content */} {/* Expanded content */}
{expandedItem === item.id && item.generated_content && ( {expandedItem === item.id && item.generated_content && (
<div className="mt-3 p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="mt-3 p-3 bg-[#0a0a0a] rounded-lg border border-[#FFFFFF]">
<div <div
className="text-[#aaa] text-xs prose prose-invert max-w-none" className="text-[#aaa] text-xs prose prose-invert max-w-none"
dangerouslySetInnerHTML={{ __html: item.generated_content.slice(0, 1500) + (item.generated_content.length > 1500 ? '...' : '') }} dangerouslySetInnerHTML={{ __html: item.generated_content.slice(0, 1500) + (item.generated_content.length > 1500 ? '...' : '') }}
@@ -501,7 +501,7 @@ export default function ShowCalendarPage() {
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<button <button
onClick={() => setExpandedItem(expandedItem === item.id ? null : item.id)} onClick={() => setExpandedItem(expandedItem === item.id ? null : item.id)}
className="px-2 py-1 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors" className="px-2 py-1 bg-[#FFFFFF] hover:bg-[#DCE6F2] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors"
> >
{expandedItem === item.id ? 'Hide' : 'Preview'} {expandedItem === item.id ? 'Hide' : 'Preview'}
</button> </button>
@@ -541,7 +541,7 @@ export default function ShowCalendarPage() {
<select <select
value={showFilter} value={showFilter}
onChange={e => setShowFilter(e.target.value)} onChange={e => setShowFilter(e.target.value)}
className="bg-[#1a1a1a] border border-[#333] text-white text-xs rounded px-2 py-1" className="bg-[#FFFFFF] border border-[#333] text-white text-xs rounded px-2 py-1"
> >
<option value="">All Shows</option> <option value="">All Shows</option>
{events.map(e => <option key={e.id} value={e.id}>{e.event_name}</option>)} {events.map(e => <option key={e.id} value={e.id}>{e.event_name}</option>)}
@@ -564,13 +564,13 @@ export default function ShowCalendarPage() {
<tr><td colSpan={5} className="text-center py-8 text-[#555]">No exhibitors found.</td></tr> <tr><td colSpan={5} className="text-center py-8 text-[#555]">No exhibitors found.</td></tr>
)} )}
{exhibitors.map(ex => ( {exhibitors.map(ex => (
<tr key={ex.id} className="border-b border-[#1a1a1a] hover:bg-[#0f0f0f]"> <tr key={ex.id} className="border-b border-[#FFFFFF] hover:bg-[#0f0f0f]">
<td className="px-4 py-3 text-white font-medium">{ex.company_name}</td> <td className="px-4 py-3 text-white font-medium">{ex.company_name}</td>
<td className="px-4 py-3 text-[#888]">{ex.booth_number || '—'}</td> <td className="px-4 py-3 text-[#888]">{ex.booth_number || '—'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`text-xs px-1.5 py-0.5 rounded ${ <span className={`text-xs px-1.5 py-0.5 rounded ${
ex.preview_story_status === 'published' ? 'bg-green-900/20 text-green-400' : ex.preview_story_status === 'published' ? 'bg-green-900/20 text-green-400' :
ex.preview_story_status === 'generated'? 'bg-blue-900/20 text-blue-400' : 'bg-[#1a1a1a] text-[#888]' ex.preview_story_status === 'generated'? 'bg-blue-900/20 text-blue-400' : 'bg-[#FFFFFF] text-[#888]'
}`}>{ex.preview_story_status}</span> }`}>{ex.preview_story_status}</span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
@@ -613,14 +613,14 @@ export default function ShowCalendarPage() {
Updated: {formatDate(profile.last_updated)} Updated: {formatDate(profile.last_updated)}
</p> </p>
{expandedItem === profile.id && profile.style_guide_text && ( {expandedItem === profile.id && profile.style_guide_text && (
<div className="mt-3 p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="mt-3 p-3 bg-[#0a0a0a] rounded-lg border border-[#FFFFFF]">
<p className="text-[#aaa] text-xs whitespace-pre-wrap">{profile.style_guide_text}</p> <p className="text-[#aaa] text-xs whitespace-pre-wrap">{profile.style_guide_text}</p>
</div> </div>
)} )}
</div> </div>
<button <button
onClick={() => setExpandedItem(expandedItem === profile.id ? null : profile.id)} onClick={() => setExpandedItem(expandedItem === profile.id ? null : profile.id)}
className="px-2 py-1 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors flex-shrink-0" className="px-2 py-1 bg-[#FFFFFF] hover:bg-[#DCE6F2] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors flex-shrink-0"
> >
{expandedItem === profile.id ? 'Hide' : 'View Guide'} {expandedItem === profile.id ? 'Hide' : 'View Guide'}
</button> </button>
@@ -637,24 +637,24 @@ export default function ShowCalendarPage() {
<div className="bg-[#111] border border-[#1e1e1e] rounded-xl p-6"> <div className="bg-[#111] border border-[#1e1e1e] rounded-xl p-6">
<h2 className="text-white font-semibold mb-4">Show Engine Settings</h2> <h2 className="text-white font-semibold mb-4">Show Engine Settings</h2>
<div className="space-y-4 text-sm text-[#888]"> <div className="space-y-4 text-sm text-[#888]">
<div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#FFFFFF]">
<p className="text-white font-medium mb-1">Annual Date Discovery</p> <p className="text-white font-medium mb-1">Annual Date Discovery</p>
<p>Runs automatically on January 1 each year. Searches official show websites and Google for confirmed dates. Sends admin notification when dates are set or when manual review is needed.</p> <p>Runs automatically on January 1 each year. Searches official show websites and Google for confirmed dates. Sends admin notification when dates are set or when manual review is needed.</p>
</div> </div>
<div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#FFFFFF]">
<p className="text-white font-medium mb-1">Pen Name Rosters</p> <p className="text-white font-medium mb-1">Pen Name Rosters</p>
<p className="mb-2"><strong className="text-[#aaa]">AV Beat (12 writers):</strong> Michael Strand, David Harlow, Karen Fielding, James Mercer (primary NAB), Peter Calloway, Sandra Voss, Brian Kowalski, Laura Pennington, Thomas Reeves, Christine Vale, Marcus Webb, Ellen Forsythe</p> <p className="mb-2"><strong className="text-[#aaa]">AV Beat (12 writers):</strong> Michael Strand, David Harlow, Karen Fielding, James Mercer (primary NAB), Peter Calloway, Sandra Voss, Brian Kowalski, Laura Pennington, Thomas Reeves, Christine Vale, Marcus Webb, Ellen Forsythe</p>
<p><strong className="text-[#aaa]">AV Beat (7 writers):</strong> Rex Chandler, Dana Flux, Derek Wainwright, Sloane Rigging, Chip Crosspoint, Blair Presenter, Jordan Lumen</p> <p><strong className="text-[#aaa]">AV Beat (7 writers):</strong> Rex Chandler, Dana Flux, Derek Wainwright, Sloane Rigging, Chip Crosspoint, Blair Presenter, Jordan Lumen</p>
</div> </div>
<div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#FFFFFF]">
<p className="text-white font-medium mb-1">SEO Tier Priority</p> <p className="text-white font-medium mb-1">SEO Tier Priority</p>
<p>Tier 1 (Max): NAB Las Vegas, IBC, InfoComm, ISE · Tier 2 (High): NAB NY, BroadcastAsia, CABSAT, AES, InfoComm EDGE/China/LatAm · Tier 3 (Standard): MPTS, SET Expo, NATPE, InfoComm India, FORTÉ LIVE · Tier 4 (Light): NewsTECHForum, TAB, SVVS, Hamburg Open, AVIXA Regional</p> <p>Tier 1 (Max): NAB Las Vegas, IBC, InfoComm, ISE · Tier 2 (High): NAB NY, BroadcastAsia, CABSAT, AES, InfoComm EDGE/China/LatAm · Tier 3 (Standard): MPTS, SET Expo, NATPE, InfoComm India, FORTÉ LIVE · Tier 4 (Light): NewsTECHForum, TAB, SVVS, Hamburg Open, AVIXA Regional</p>
</div> </div>
<div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#FFFFFF]">
<p className="text-white font-medium mb-1">NAB Cross-Post to AV Beat</p> <p className="text-white font-medium mb-1">NAB Cross-Post to AV Beat</p>
<p>NAB exhibitors with AV-eligible products (projectors, displays, conferencing, control systems, digital signage, installed audio, AV-over-IP) automatically generate stories on AV Beat using AV Beat pen names.</p> <p>NAB exhibitors with AV-eligible products (projectors, displays, conferencing, control systems, digital signage, installed audio, AV-over-IP) automatically generate stories on AV Beat using AV Beat pen names.</p>
</div> </div>
<div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#FFFFFF]">
<p className="text-white font-medium mb-1">Blocked Competitor Sources</p> <p className="text-white font-medium mb-1">Blocked Competitor Sources</p>
<p>BB: tvtechnology.com, broadcastingcable.com, sportsvideo.org, digitaltveurope.com, nexttv.com · AV: ravepubs.com, avnetwork.com, commercialintegrator.com, systemscontractor.com, avinteractive.com · Note: infocommshow.org, iseurope.org, nabshow.com, avixa.org are PRIMARY sources not blocked.</p> <p>BB: tvtechnology.com, broadcastingcable.com, sportsvideo.org, digitaltveurope.com, nexttv.com · AV: ravepubs.com, avnetwork.com, commercialintegrator.com, systemscontractor.com, avinteractive.com · Note: infocommshow.org, iseurope.org, nabshow.com, avixa.org are PRIMARY sources not blocked.</p>
</div> </div>
@@ -779,14 +779,14 @@ export default function ShowCalendarPage() {
<button <button
type="submit" type="submit"
disabled={actionLoading === 'save-event'} disabled={actionLoading === 'save-event'}
className="flex-1 py-2 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50" className="flex-1 py-2 bg-[#1D4ED8] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
> >
{actionLoading === 'save-event' ? 'Saving...' : 'Save Event'} {actionLoading === 'save-event' ? 'Saving...' : 'Save Event'}
</button> </button>
<button <button
type="button" type="button"
onClick={() => { setShowEventModal(false); setEditingEvent(null); }} onClick={() => { setShowEventModal(false); setEditingEvent(null); }}
className="px-4 py-2 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-white text-sm rounded-lg transition-colors" className="px-4 py-2 bg-[#FFFFFF] hover:bg-[#DCE6F2] border border-[#333] text-white text-sm rounded-lg transition-colors"
> >
Cancel Cancel
</button> </button>
@@ -857,14 +857,14 @@ export default function ShowCalendarPage() {
<button <button
type="submit" type="submit"
disabled={actionLoading === 'generate'} disabled={actionLoading === 'generate'}
className="flex-1 py-2 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50" className="flex-1 py-2 bg-[#1D4ED8] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
> >
{actionLoading === 'generate' ? 'Generating...' : 'Generate Story'} {actionLoading === 'generate' ? 'Generating...' : 'Generate Story'}
</button> </button>
<button <button
type="button" type="button"
onClick={() => setShowGenerateModal(false)} onClick={() => setShowGenerateModal(false)}
className="px-4 py-2 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-white text-sm rounded-lg transition-colors" className="px-4 py-2 bg-[#FFFFFF] hover:bg-[#DCE6F2] border border-[#333] text-white text-sm rounded-lg transition-colors"
> >
Cancel Cancel
</button> </button>

View File

@@ -13,25 +13,25 @@ export default function Error({
console.error("[route error]", error); console.error("[route error]", error);
}, [error]); }, [error]);
return ( return (
<div className="min-h-screen flex items-center justify-center bg-[#0d0d0d] text-[#e0e0e0] p-6"> <div className="min-h-screen flex items-center justify-center bg-[#F8FAFC] text-[#0F172A] p-6">
<div className="max-w-md text-center"> <div className="max-w-md text-center">
<h2 className="font-heading text-2xl mb-3">Something went wrong</h2> <h2 className="font-heading text-2xl mb-3">Something went wrong</h2>
<p className="text-[#888] text-sm mb-4 font-body"> <p className="text-[#475569] text-sm mb-4 font-body">
We hit an error rendering this page. Try again, or head back to the home page. We hit an error rendering this page. Try again, or head back to the home page.
</p> </p>
{error?.digest && ( {error?.digest && (
<p className="text-[#444] text-xs font-mono mb-4">ref: {error.digest}</p> <p className="text-[#666] text-xs font-mono mb-4">ref: {error.digest}</p>
)} )}
<div className="flex gap-3 justify-center"> <div className="flex gap-3 justify-center">
<button <button
onClick={() => reset()} onClick={() => reset()}
className="px-4 py-2 bg-[#3b82f6] text-white text-sm rounded-sm hover:bg-[#2563eb] transition-colors" className="px-4 py-2 bg-[#1D4ED8] text-white text-sm rounded-sm hover:bg-[#1E3A8A] transition-colors"
> >
Try again Try again
</button> </button>
<Link <Link
href="/home-page" href="/home-page"
className="px-4 py-2 border border-[#333] text-sm rounded-sm hover:border-[#3b82f6] transition-colors" className="px-4 py-2 border border-[#DCE6F2] text-[#0F172A] text-sm rounded-sm hover:border-[#1D4ED8] transition-colors"
> >
Home Home
</Link> </Link>

View File

@@ -74,27 +74,27 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
const getMetricValue = (thread: ForumThread) => { const getMetricValue = (thread: ForumThread) => {
if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' }; if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' };
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#3b82f6]' }; if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#1D4ED8]' };
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' }; return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
}; };
return ( return (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-xl overflow-hidden mb-6"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-xl overflow-hidden mb-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-[#252525]"> <div className="flex items-center justify-between px-5 py-4 border-b border-[#DCE6F2]">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-lg">🏆</span> <span className="text-lg">🏆</span>
<h2 className="text-white font-heading font-bold text-base">Top Performing Threads</h2> <h2 className="text-[#0F172A] font-heading font-bold text-base">Top Performing Threads</h2>
</div> </div>
{/* Metric Tabs */} {/* Metric Tabs */}
<div className="flex gap-1 bg-[#111] border border-[#2a2a2a] rounded-lg p-1"> <div className="flex gap-1 bg-[#111] border border-[#DCE6F2] rounded-lg p-1">
{metrics.map(m => ( {metrics.map(m => (
<button <button
key={m.value} key={m.value}
onClick={() => setActiveMetric(m.value)} onClick={() => setActiveMetric(m.value)}
className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${ className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
activeMetric === m.value activeMetric === m.value
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'text-[#666] hover:text-[#aaa]' : 'text-[#666] hover:text-[#aaa]'
}`} }`}
> >
@@ -111,12 +111,12 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
<> <>
{[...Array(5)].map((_, i) => ( {[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center gap-3 px-5 py-3"> <div key={i} className="flex items-center gap-3 px-5 py-3">
<div className="w-6 h-6 bg-[#252525] rounded animate-pulse flex-shrink-0" /> <div className="w-6 h-6 bg-[#DCE6F2] rounded animate-pulse flex-shrink-0" />
<div className="flex-1 space-y-1.5"> <div className="flex-1 space-y-1.5">
<div className="h-3.5 bg-[#252525] rounded animate-pulse w-3/4" /> <div className="h-3.5 bg-[#DCE6F2] rounded animate-pulse w-3/4" />
<div className="h-2.5 bg-[#1e1e1e] rounded animate-pulse w-1/3" /> <div className="h-2.5 bg-[#1e1e1e] rounded animate-pulse w-1/3" />
</div> </div>
<div className="w-10 h-8 bg-[#252525] rounded animate-pulse flex-shrink-0" /> <div className="w-10 h-8 bg-[#DCE6F2] rounded animate-pulse flex-shrink-0" />
</div> </div>
))} ))}
</> </>
@@ -132,7 +132,7 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
<Link <Link
key={thread.id} key={thread.id}
href={`/forum/thread/${thread.id}`} href={`/forum/thread/${thread.id}`}
className="flex items-center gap-3 px-5 py-3 hover:bg-[#1e2a3a] transition-colors group" className="flex items-center gap-3 px-5 py-3 hover:bg-[#e2e8f0] transition-colors group"
> >
{/* Rank */} {/* Rank */}
<span className={`flex-shrink-0 w-6 text-center font-heading font-bold text-sm ${ <span className={`flex-shrink-0 w-6 text-center font-heading font-bold text-sm ${
@@ -142,11 +142,11 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
</span> </span>
{/* Thread info */} {/* Thread info */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-white font-body text-sm font-semibold group-hover:text-[#3b82f6] transition-colors truncate leading-snug"> <p className="text-[#0F172A] font-body text-sm font-semibold group-hover:text-[#1D4ED8] transition-colors truncate leading-snug">
{thread.title} {thread.title}
</p> </p>
<p className="text-[#555] font-body text-xs mt-0.5"> <p className="text-[#555] font-body text-xs mt-0.5">
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#666] hover:text-[#3b82f6] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link> <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#666] hover:text-[#1D4ED8] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
{' · '}{timeAgo(thread.created_at)} {' · '}{timeAgo(thread.created_at)}
</p> </p>
</div> </div>
@@ -277,36 +277,36 @@ export default function ForumCategoryPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#F8FAFC]">
{/* Breadcrumb + Header */} {/* Breadcrumb + Header */}
<div className="bg-[#1a2535] border-b border-[#2a3a50]"> <div className="bg-[#FFFFFF] border-b border-[#2a3a50]">
<div className="max-w-container mx-auto px-4 py-8"> <div className="max-w-container mx-auto px-4 py-8">
<nav className="text-sm font-body text-[#666] mb-3"> <nav className="text-sm font-body text-[#666] mb-3">
<Link href="/forum" className="hover:text-[#3b82f6] transition-colors">Forum</Link> <Link href="/forum" className="hover:text-[#1D4ED8] transition-colors">Forum</Link>
<span className="mx-2"></span> <span className="mx-2"></span>
<span className="text-[#aab4c4]">{category?.name || slug}</span> <span className="text-[#0F172A]">{category?.name || slug}</span>
</nav> </nav>
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-2xl">{category?.icon || '💬'}</span> <span className="text-2xl">{category?.icon || '💬'}</span>
<div> <div>
<h1 className="text-2xl font-heading font-bold text-white">{category?.name || 'Loading...'}</h1> <h1 className="text-2xl font-heading font-bold text-[#0F172A]">{category?.name || 'Loading...'}</h1>
{category && ( {category && (
<p className="text-[#aab4c4] font-body text-sm mt-0.5">{category.thread_count} threads</p> <p className="text-[#475569] font-body text-sm mt-0.5">{category.thread_count} threads</p>
)} )}
</div> </div>
</div> </div>
{isAuthenticated ? ( {isAuthenticated ? (
<button <button
onClick={() => setShowNewThread(!showNewThread)} onClick={() => setShowNewThread(!showNewThread)}
className="flex-shrink-0 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors" className="brand-gradient flex-shrink-0 font-body font-semibold text-sm px-4 py-2 rounded-full"
> >
+ New Thread + New Thread
</button> </button>
) : ( ) : (
<Link <Link
href={`/forum/login?next=${typeof window === 'undefined' ? '/forum' : encodeURIComponent(window.location.pathname)}`} href={`/forum/login?next=${typeof window === 'undefined' ? '/forum' : encodeURIComponent(window.location.pathname)}`}
className="flex-shrink-0 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors" className="brand-gradient flex-shrink-0 font-body font-semibold text-sm px-4 py-2 rounded-full"
> >
Sign in to post Sign in to post
</Link> </Link>
@@ -318,8 +318,8 @@ export default function ForumCategoryPage() {
<div className="max-w-container mx-auto px-4 py-6"> <div className="max-w-container mx-auto px-4 py-6">
{/* New Thread Form — auth-gated */} {/* New Thread Form — auth-gated */}
{showNewThread && isAuthenticated && ( {showNewThread && isAuthenticated && (
<form onSubmit={handleNewThread} className="bg-[#1a2535] border border-[#2a3a50] rounded-lg p-5 mb-6"> <form onSubmit={handleNewThread} className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-5 mb-6">
<h3 className="text-white font-heading font-semibold mb-4">Start a New Thread</h3> <h3 className="text-[#0F172A] font-heading font-semibold mb-4">Start a New Thread</h3>
<div className="space-y-3"> <div className="space-y-3">
<input <input
type="text" type="text"
@@ -327,7 +327,7 @@ export default function ForumCategoryPage() {
value={newTitle} value={newTitle}
onChange={e => setNewTitle(e.target.value)} onChange={e => setNewTitle(e.target.value)}
required required
className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] font-body text-sm placeholder-[#888] focus:outline-none focus:border-[#1D4ED8]"
/> />
<textarea <textarea
placeholder="What would you like to discuss? *" placeholder="What would you like to discuss? *"
@@ -335,14 +335,14 @@ export default function ForumCategoryPage() {
onChange={e => setNewBody(e.target.value)} onChange={e => setNewBody(e.target.value)}
required required
rows={5} rows={5}
className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#3b82f6] resize-none" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] font-body text-sm placeholder-[#888] focus:outline-none focus:border-[#1D4ED8] resize-none"
/> />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<ImageAttachButton onInsert={(md) => setNewBody((b) => (b || "") + md)} /> <ImageAttachButton onInsert={(md) => setNewBody((b) => (b || "") + md)} />
<span className="text-[10px] text-[#666]">Auto-optimized, never &gt; 20 MB</span> <span className="text-[10px] text-[#666]">Auto-optimized, never &gt; 20 MB</span>
</div> </div>
{newError && ( {newError && (
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#1D4ED8] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{newError} {newError}
</div> </div>
)} )}
@@ -350,14 +350,14 @@ export default function ForumCategoryPage() {
<button <button
type="submit" type="submit"
disabled={submitting} disabled={submitting}
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors" className="bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
> >
{submitting ? 'Posting...' : 'Post Thread'} {submitting ? 'Posting...' : 'Post Thread'}
</button> </button>
<button <button
type="button" type="button"
onClick={() => setShowNewThread(false)} onClick={() => setShowNewThread(false)}
className="bg-[#252525] hover:bg-[#333] text-[#aaa] font-body text-sm px-5 py-2 rounded-lg transition-colors" className="bg-[#DCE6F2] hover:bg-[#cbd5e1] text-[#0F172A] font-body text-sm px-5 py-2 rounded-lg transition-colors"
> >
Cancel Cancel
</button> </button>
@@ -378,7 +378,7 @@ export default function ForumCategoryPage() {
placeholder="Search threads in this category..." placeholder="Search threads in this category..."
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#3b82f6] rounded-lg pl-9 pr-4 py-2.5 text-white font-body text-sm placeholder-[#555] focus:outline-none transition-colors" className="w-full bg-[#FFFFFF] border border-[#DCE6F2] focus:border-[#1D4ED8] rounded-lg pl-9 pr-4 py-2.5 text-[#0F172A] font-body text-sm placeholder-[#888] focus:outline-none transition-colors"
/> />
{search && ( {search && (
<button <button
@@ -393,16 +393,16 @@ export default function ForumCategoryPage() {
)} )}
</div> </div>
{/* Sort Options */} {/* Sort Options — segmented pill control; active uses brand-gradient */}
<div className="flex gap-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg p-1 flex-shrink-0"> <div className="flex gap-1 bg-[#FFFFFF] border border-[#DCE6F2] rounded-full p-1 flex-shrink-0">
{sortOptions.map(opt => ( {sortOptions.map(opt => (
<button <button
key={opt.value} key={opt.value}
onClick={() => setSort(opt.value)} onClick={() => setSort(opt.value)}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${ className={`px-3 py-1 rounded-full text-xs font-body font-semibold transition-colors whitespace-nowrap ${
sort === opt.value sort === opt.value
? 'bg-[#3b82f6] text-white' ? 'brand-gradient'
: 'text-[#888] hover:text-white' : 'text-[#475569] hover:text-[#0F172A]'
}`} }`}
> >
{opt.label} {opt.label}
@@ -414,7 +414,7 @@ export default function ForumCategoryPage() {
{(loading || threadsLoading) && ( {(loading || threadsLoading) && (
<div className="space-y-2"> <div className="space-y-2">
{[...Array(10)].map((_, i) => ( {[...Array(10)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-lg h-16 animate-pulse border border-[#252525]" /> <div key={i} className="bg-[#FFFFFF] rounded-lg h-16 animate-pulse border border-[#DCE6F2]" />
))} ))}
</div> </div>
)} )}
@@ -426,8 +426,8 @@ export default function ForumCategoryPage() {
)} )}
{!loading && !threadsLoading && !error && threads.length === 0 && ( {!loading && !threadsLoading && !error && threads.length === 0 && (
<div className="text-center py-16 text-[#555] font-body"> <div className="text-center py-16 text-[#475569] font-body">
<p className="text-lg mb-2"> <p className="text-lg mb-2 text-[#0F172A] font-semibold">
{search ? 'No threads match your search' : sort === 'unanswered' ? 'No unanswered threads' : 'No threads yet'} {search ? 'No threads match your search' : sort === 'unanswered' ? 'No unanswered threads' : 'No threads yet'}
</p> </p>
<p className="text-sm"> <p className="text-sm">
@@ -445,12 +445,12 @@ export default function ForumCategoryPage() {
<Link <Link
key={thread.id} key={thread.id}
href={`/forum/thread/${thread.id}`} href={`/forum/thread/${thread.id}`}
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#3b82f6] rounded-lg px-4 py-3 transition-all group" className="flex items-center gap-4 bg-[#FFFFFF] hover:bg-[#e2e8f0] border border-[#DCE6F2] hover:border-[#1D4ED8] rounded-lg px-4 py-3 transition-all group"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
{thread.status === 'pinned' && ( {thread.status === 'pinned' && (
<span className="text-xs bg-[#3b82f6]/20 text-[#3b82f6] px-2 py-0.5 rounded font-body font-semibold">PINNED</span> <span className="text-xs bg-[#1D4ED8]/20 text-[#1D4ED8] px-2 py-0.5 rounded font-body font-semibold">PINNED</span>
)} )}
{thread.status === 'closed' && ( {thread.status === 'closed' && (
<span className="text-xs bg-[#555]/20 text-[#888] px-2 py-0.5 rounded font-body">CLOSED</span> <span className="text-xs bg-[#555]/20 text-[#888] px-2 py-0.5 rounded font-body">CLOSED</span>
@@ -458,23 +458,23 @@ export default function ForumCategoryPage() {
{thread.reply_count === 0 && ( {thread.reply_count === 0 && (
<span className="text-xs bg-[#f59e0b]/10 text-[#f59e0b] px-2 py-0.5 rounded font-body">UNANSWERED</span> <span className="text-xs bg-[#f59e0b]/10 text-[#f59e0b] px-2 py-0.5 rounded font-body">UNANSWERED</span>
)} )}
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#3b82f6] transition-colors truncate"> <h3 className="text-[#0F172A] font-body font-semibold text-sm group-hover:text-[#1D4ED8] transition-colors truncate">
{thread.title} {thread.title}
</h3> </h3>
</div> </div>
<p className="text-[#666] font-body text-xs mt-0.5"> <p className="text-[#666] font-body text-xs mt-0.5">
by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#3b82f6] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link> by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#1D4ED8] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
{' · '}{timeAgo(thread.created_at)} {' · '}{timeAgo(thread.created_at)}
</p> </p>
</div> </div>
<div className="flex-shrink-0 text-right hidden sm:block"> <div className="flex-shrink-0 text-right hidden sm:block">
<div className="text-[#aaa] font-body text-sm font-semibold">{thread.reply_count}</div> <div className="text-[#0F172A] font-body text-sm font-semibold">{thread.reply_count}</div>
<div className="text-[#555] font-body text-xs">replies</div> <div className="text-[#475569] font-body text-xs">replies</div>
</div> </div>
{sort === 'top-voted' && ( {sort === 'top-voted' && (
<div className="flex-shrink-0 text-right hidden md:block"> <div className="flex-shrink-0 text-right hidden md:block">
<div className="text-[#22c55e] font-body text-sm font-semibold">+{thread.upvotes ?? 0}</div> <div className="text-[#22c55e] font-body text-sm font-semibold">+{thread.upvotes ?? 0}</div>
<div className="text-[#555] font-body text-xs">votes</div> <div className="text-[#475569] font-body text-xs">votes</div>
</div> </div>
)} )}
<div className="flex-shrink-0 text-right hidden md:block"> <div className="flex-shrink-0 text-right hidden md:block">

View File

@@ -30,21 +30,21 @@ function ForgotInner() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]"> <main className="min-h-screen bg-gradient-to-b from-[#F8FAFC] via-[#F8FAFC] to-[#F8FAFC]">
<div className="max-w-md mx-auto px-4 py-16"> <div className="max-w-md mx-auto px-4 py-16">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="text-[10px] uppercase tracking-widest text-[#3b82f6] font-mono mb-2">Account recovery</div> <div className="text-[10px] uppercase tracking-widest text-[#1D4ED8] font-mono mb-2">Account recovery</div>
<h1 className="text-3xl font-heading font-bold text-white">Reset your password</h1> <h1 className="text-3xl font-heading font-bold text-[#0F172A]">Reset your password</h1>
<p className="text-[#888] font-body text-sm mt-2"> <p className="text-[#888] font-body text-sm mt-2">
Enter your email we&rsquo;ll send a link to set a new password. Enter your email we&rsquo;ll send a link to set a new password.
</p> </p>
</div> </div>
<form <form
onSubmit={submit} onSubmit={submit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl" className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-6 space-y-4 shadow-2xl"
> >
{err && ( {err && (
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#1D4ED8] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{err} {err}
</div> </div>
)} )}
@@ -63,19 +63,19 @@ function ForgotInner() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
autoComplete="email" autoComplete="email"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={busy || !email.trim()} disabled={busy || !email.trim()}
className="w-full bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-3 rounded-lg transition-colors" className="w-full bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-3 rounded-lg transition-colors"
> >
{busy ? "Sending…" : "Send reset link"} {busy ? "Sending…" : "Send reset link"}
</button> </button>
<div className="text-center text-xs font-body text-[#888] pt-2 border-t border-[#252525]"> <div className="text-center text-xs font-body text-[#888] pt-2 border-t border-[#DCE6F2]">
Remembered it?{" "} Remembered it?{" "}
<Link href="/forum/login" className="text-[#3b82f6] hover:underline font-semibold"> <Link href="/forum/login" className="text-[#1D4ED8] hover:underline font-semibold">
Back to sign in Back to sign in
</Link> </Link>
</div> </div>

View File

@@ -1,19 +1,19 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'The Crew Lounge — AV Beat', title: 'The Integration Room — AV Beat',
description: 'The Crew Lounge: connect with broadcast, motion picture and post production professionals worldwide. Discuss live production, IP workflows, streaming, audio, cameras, AI automation, and more.', description: 'The Integration Room: connect with pro AV integrators, live production engineers, and display tech specialists worldwide. Discuss AV-over-IP, control systems, projection & display, audio engineering, conferencing, AI automation, and more.',
alternates: { canonical: '/forum' }, alternates: { canonical: '/forum' },
openGraph: { openGraph: {
title: 'The Crew Lounge — AV Beat', title: 'The Integration Room — AV Beat',
description: 'The Crew Lounge: connect with broadcast, motion picture and post production professionals worldwide. Ask questions, share expertise, and discuss broadcast technology.', description: 'The Integration Room: connect with pro AV integrators, live production engineers, and display tech specialists worldwide. Ask questions, share expertise, and discuss AV technology.',
url: '/forum', url: '/forum',
type: 'website', type: 'website',
}, },
twitter: { twitter: {
card: 'summary', card: 'summary',
title: 'The Crew Lounge — AV Beat', title: 'The Integration Room — AV Beat',
description: 'The Crew Lounge: connect with broadcast, motion picture and post production professionals worldwide on AV Beat.', description: 'The Integration Room: connect with pro AV integrators, live production engineers, and display tech specialists worldwide on AV Beat.',
}, },
}; };
@@ -21,8 +21,8 @@ export default function ForumLayout({ children }: { children: React.ReactNode })
const schema = { const schema = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'WebPage', '@type': 'WebPage',
name: 'AV Beat — The Crew Lounge', name: 'AV Beat — The Integration Room',
description: 'The Crew Lounge: an industry community for broadcast, motion picture and post production professionals worldwide.', description: 'The Integration Room: an industry community for pro AV integrators, live production engineers, and display tech professionals worldwide.',
url: 'https://avbeat.com/forum', url: 'https://avbeat.com/forum',
isPartOf: { isPartOf: {
'@type': 'WebSite', '@type': 'WebSite',
@@ -33,7 +33,7 @@ export default function ForumLayout({ children }: { children: React.ReactNode })
'@type': 'BreadcrumbList', '@type': 'BreadcrumbList',
itemListElement: [ itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://avbeat.com' }, { '@type': 'ListItem', position: 1, name: 'Home', item: 'https://avbeat.com' },
{ '@type': 'ListItem', position: 2, name: 'The Crew Lounge', item: 'https://avbeat.com/forum' }, { '@type': 'ListItem', position: 2, name: 'The Integration Room', item: 'https://avbeat.com/forum' },
], ],
}, },
}; };

View File

@@ -51,21 +51,21 @@ function ForumLoginInner() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]"> <main className="min-h-screen bg-gradient-to-b from-[#F8FAFC] via-[#F8FAFC] to-[#F8FAFC]">
<div className="max-w-md mx-auto px-4 py-16"> <div className="max-w-md mx-auto px-4 py-16">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="text-[10px] uppercase tracking-widest text-[#3b82f6] font-mono mb-2">Welcome back</div> <div className="text-[10px] uppercase tracking-widest text-[#1D4ED8] font-mono mb-2">Welcome back</div>
<h1 className="text-3xl font-heading font-bold text-white">Sign in to AV Beat</h1> <h1 className="text-3xl font-heading font-bold text-[#0F172A]">Sign in to AV Beat</h1>
<p className="text-[#888] font-body text-sm mt-2"> <p className="text-[#888] font-body text-sm mt-2">
Your account covers the forum, newsletter preferences, and saved articles. Your account covers the forum, newsletter preferences, and saved articles.
</p> </p>
</div> </div>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl" className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-6 space-y-4 shadow-2xl"
> >
{error && ( {error && (
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#1D4ED8] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{error} {error}
</div> </div>
)} )}
@@ -79,7 +79,7 @@ function ForumLoginInner() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
autoComplete="email" autoComplete="email"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<div> <div>
@@ -92,20 +92,20 @@ function ForumLoginInner() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password" autoComplete="current-password"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors" className="w-full bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors"
> >
{loading ? 'Signing in…' : 'Sign In'} {loading ? 'Signing in…' : 'Sign In'}
</button> </button>
<div className="text-right text-xs font-body"> <div className="text-right text-xs font-body">
<Link <Link
href={`/forum/forgot-password?email=${encodeURIComponent(email)}`} href={`/forum/forgot-password?email=${encodeURIComponent(email)}`}
className="text-[#888] hover:text-[#3b82f6] hover:underline" className="text-[#888] hover:text-[#1D4ED8] hover:underline"
> >
Forgot password? Forgot password?
</Link> </Link>
@@ -115,7 +115,7 @@ function ForumLoginInner() {
No account yet?{' '} No account yet?{' '}
<Link <Link
href={`/forum/register?next=${encodeURIComponent(next)}`} href={`/forum/register?next=${encodeURIComponent(next)}`}
className="text-[#3b82f6] hover:underline" className="text-[#1D4ED8] hover:underline"
> >
Create one Create one
</Link> </Link>

View File

@@ -21,11 +21,17 @@ interface ForumCategory {
last_activity_at?: string | null; last_activity_at?: string | null;
} }
function profileHref(thread: { author_username?: string | null; author_name: string }): string {
const slug = thread.author_username || thread.author_name;
return `/forum/user/${encodeURIComponent(slug)}`;
}
interface ForumThread { interface ForumThread {
id: string; id: string;
title: string; title: string;
author_id?: string | null; author_id?: string | null;
author_name: string; author_name: string;
author_username?: string | null;
reply_count: number; reply_count: number;
view_count: number; view_count: number;
last_reply_at: string; last_reply_at: string;
@@ -103,27 +109,27 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
const getMetricValue = (thread: ForumThread) => { const getMetricValue = (thread: ForumThread) => {
if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' }; if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' };
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#3b82f6]' }; if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#1D4ED8]' };
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' }; return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
}; };
return ( return (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-xl overflow-hidden mb-8"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-xl overflow-hidden mb-8">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-[#252525]"> <div className="flex items-center justify-between px-5 py-4 border-b border-[#DCE6F2]">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-lg">🏆</span> <span className="text-lg">🏆</span>
<h2 className="text-white font-heading font-bold text-base">Top Performing Threads</h2> <h2 className="text-[#0F172A] font-heading font-bold text-base">Top Performing Threads</h2>
</div> </div>
{/* Metric Tabs */} {/* Metric Tabs */}
<div className="flex gap-1 bg-[#111] border border-[#2a2a2a] rounded-lg p-1"> <div className="flex gap-1 bg-[#111] border border-[#DCE6F2] rounded-lg p-1">
{metrics.map(m => ( {metrics.map(m => (
<button <button
key={m.value} key={m.value}
onClick={() => setActiveMetric(m.value)} onClick={() => setActiveMetric(m.value)}
className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${ className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
activeMetric === m.value activeMetric === m.value
? 'bg-[#3b82f6] text-white' ? 'bg-[#1D4ED8] text-white'
: 'text-[#666] hover:text-[#aaa]' : 'text-[#666] hover:text-[#aaa]'
}`} }`}
> >
@@ -140,18 +146,18 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
<> <>
{[...Array(5)].map((_, i) => ( {[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center gap-3 px-5 py-3"> <div key={i} className="flex items-center gap-3 px-5 py-3">
<div className="w-6 h-6 bg-[#252525] rounded animate-pulse flex-shrink-0" /> <div className="w-6 h-6 bg-[#DCE6F2] rounded animate-pulse flex-shrink-0" />
<div className="flex-1 space-y-1.5"> <div className="flex-1 space-y-1.5">
<div className="h-3.5 bg-[#252525] rounded animate-pulse w-3/4" /> <div className="h-3.5 bg-[#DCE6F2] rounded animate-pulse w-3/4" />
<div className="h-2.5 bg-[#1e1e1e] rounded animate-pulse w-1/3" /> <div className="h-2.5 bg-[#1e1e1e] rounded animate-pulse w-1/3" />
</div> </div>
<div className="w-10 h-8 bg-[#252525] rounded animate-pulse flex-shrink-0" /> <div className="w-10 h-8 bg-[#DCE6F2] rounded animate-pulse flex-shrink-0" />
</div> </div>
))} ))}
</> </>
)} )}
{!loading && threads.length === 0 && ( {!loading && threads.length === 0 && (
<div className="px-5 py-8 text-center text-[#555] font-body text-sm"> <div className="px-5 py-8 text-center text-[#475569] font-body text-sm">
No threads yet. Be the first to start a discussion! No threads yet. Be the first to start a discussion!
</div> </div>
)} )}
@@ -161,7 +167,7 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
<Link <Link
key={thread.id} key={thread.id}
href={`/forum/thread/${thread.id}`} href={`/forum/thread/${thread.id}`}
className="flex items-center gap-3 px-5 py-3 hover:bg-[#1e2a3a] transition-colors group" className="flex items-center gap-3 px-5 py-3 hover:bg-[#e2e8f0] transition-colors group"
> >
{/* Rank */} {/* Rank */}
<span className={`flex-shrink-0 w-6 text-center font-heading font-bold text-sm ${ <span className={`flex-shrink-0 w-6 text-center font-heading font-bold text-sm ${
@@ -171,13 +177,13 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
</span> </span>
{/* Thread info */} {/* Thread info */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-white font-body text-sm font-semibold group-hover:text-[#3b82f6] transition-colors truncate leading-snug"> <p className="text-[#0F172A] font-body text-sm font-semibold group-hover:text-[#1D4ED8] transition-colors truncate leading-snug">
{thread.title} {thread.title}
</p> </p>
<p className="text-[#555] font-body text-xs mt-0.5"> <p className="text-[#475569] font-body text-xs mt-0.5">
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#666] hover:text-[#3b82f6] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link> <Link href={profileHref(thread)} className="text-[#475569] hover:text-[#1D4ED8] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
{thread.forum_categories && ( {thread.forum_categories && (
<> · <span className="text-[#3b82f6]">{thread.forum_categories.name}</span></> <> · <span className="text-[#1D4ED8]">{thread.forum_categories.name}</span></>
)} )}
{' · '}{timeAgo(thread.created_at)} {' · '}{timeAgo(thread.created_at)}
</p> </p>
@@ -283,25 +289,25 @@ export default function ForumIndexPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#F8FAFC]">
{/* Hero — width-aligned with content below + featured-box pattern */} {/* Hero — width-aligned with content below + featured-box pattern */}
<div className="max-w-container mx-auto px-4"> <div className="max-w-container mx-auto px-4">
<div className="bg-[#1a2535] border-b border-[#2a3a50] py-5 px-4"> <div className="bg-[#FFFFFF] border-b border-[#2a3a50] py-5 px-4">
<div className="flex items-start justify-between gap-4 flex-wrap"> <div className="flex items-start justify-between gap-4 flex-wrap">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-3 mb-1">
<img src="/logos/crew-lounge-logo.svg" alt="The Crew Lounge" className="w-8 h-8" /> <img src="/logos/crew-lounge-logo.svg" alt="The Integration Room" className="w-8 h-8" />
<h1 className="text-3xl font-heading font-bold text-white">The Crew Lounge</h1> <h1 className="text-3xl font-heading font-bold text-[#0F172A]">The Integration Room</h1>
</div> </div>
<p className="text-[#aab4c4] font-body text-sm max-w-2xl"> <p className="text-[#475569] font-body text-sm max-w-2xl">
Connect with broadcast, motion picture, and post production pros worldwide. Connect with pro AV integrators, live production engineers, and display tech specialists worldwide.
</p> </p>
</div> </div>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
{currentUserId ? ( {currentUserId ? (
<Link <Link
href="/forum/new" href="/forum/new"
className="inline-flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors" className="brand-gradient inline-flex items-center gap-2 font-body font-semibold text-sm rounded-full px-5 py-2"
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M12 5v14M5 12h14" /> <path d="M12 5v14M5 12h14" />
@@ -311,7 +317,7 @@ export default function ForumIndexPage() {
) : ( ) : (
<Link <Link
href="/forum/login?next=/forum" href="/forum/login?next=/forum"
className="inline-flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors" className="brand-gradient inline-flex items-center gap-2 font-body font-semibold text-sm rounded-full px-5 py-2"
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3" /> <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3" />
@@ -328,12 +334,12 @@ export default function ForumIndexPage() {
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-8"> <div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-8">
<div> <div>
{/* Feed Mode Tabs */} {/* Feed Mode Tabs — slim pill toggle; active uses brand-gradient */}
<div className="flex items-center gap-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg p-1 mb-6 w-fit"> <div className="flex items-center gap-1 bg-[#FFFFFF] border border-[#DCE6F2] rounded-full p-1 mb-6 w-fit">
<button <button
onClick={() => setFeedMode('discover')} onClick={() => setFeedMode('discover')}
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${ className={`px-4 py-1.5 rounded-full text-sm font-body font-semibold transition-colors flex items-center gap-2 ${
feedMode === 'discover' ? 'bg-[#3b82f6] text-white' : 'text-[#888] hover:text-white' feedMode === 'discover' ? 'brand-gradient' : 'text-[#475569] hover:text-[#0F172A]'
}`} }`}
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
@@ -343,8 +349,8 @@ export default function ForumIndexPage() {
</button> </button>
<button <button
onClick={() => setFeedMode('following')} onClick={() => setFeedMode('following')}
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${ className={`px-4 py-1.5 rounded-full text-sm font-body font-semibold transition-colors flex items-center gap-2 ${
feedMode === 'following' ? 'bg-[#3b82f6] text-white' : 'text-[#888] hover:text-white' feedMode === 'following' ? 'brand-gradient' : 'text-[#475569] hover:text-[#0F172A]'
}`} }`}
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
@@ -359,11 +365,11 @@ export default function ForumIndexPage() {
{feedMode === 'following' && ( {feedMode === 'following' && (
<div className="mb-8"> <div className="mb-8">
{!currentUserId && ( {!currentUserId && (
<div className="bg-[#1a2535] border border-[#2a3a50] rounded-lg p-8 text-center"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-8 text-center">
<p className="text-[#aab4c4] font-body text-sm mb-3">Sign in to see threads from people you follow.</p> <p className="text-[#475569] font-body text-sm mb-3">Sign in to see threads from people you follow.</p>
<Link <Link
href="/login" href="/login"
className="inline-block bg-[#3b82f6] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#2563eb] transition-colors" className="brand-gradient inline-block font-body text-sm font-bold px-5 py-2 rounded-full"
> >
Sign In Sign In
</Link> </Link>
@@ -372,19 +378,19 @@ export default function ForumIndexPage() {
{currentUserId && followingLoading && ( {currentUserId && followingLoading && (
<div className="space-y-2"> <div className="space-y-2">
{[...Array(6)].map((_, i) => ( {[...Array(6)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-lg h-16 animate-pulse border border-[#252525]" /> <div key={i} className="bg-[#FFFFFF] rounded-lg h-16 animate-pulse border border-[#DCE6F2]" />
))} ))}
</div> </div>
)} )}
{currentUserId && !followingLoading && followingThreads.length === 0 && ( {currentUserId && !followingLoading && followingThreads.length === 0 && (
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg p-10 text-center"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-10 text-center">
<div className="text-3xl mb-3">👥</div> <div className="text-3xl mb-3">👥</div>
<p className="text-[#aaa] font-body text-sm font-semibold mb-1">Your following feed is empty</p> <p className="text-[#0F172A] font-body text-sm font-semibold mb-1">Your following feed is empty</p>
<p className="text-[#555] font-body text-sm mb-4">Follow other members to see their threads here.</p> <p className="text-[#475569] font-body text-sm mb-4">Follow other members to see their threads here.</p>
<Link <Link
href="/forum" href="/forum"
onClick={() => setFeedMode('discover')} onClick={() => setFeedMode('discover')}
className="inline-block bg-[#3b82f6] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#2563eb] transition-colors" className="inline-block bg-[#1D4ED8] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#1E3A8A] transition-colors"
> >
Discover Members Discover Members
</Link> </Link>
@@ -398,18 +404,18 @@ export default function ForumIndexPage() {
<Link <Link
key={thread.id} key={thread.id}
href={`/forum/thread/${thread.id}`} href={`/forum/thread/${thread.id}`}
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#3b82f6] rounded-lg px-4 py-3 transition-all group" className="flex items-center gap-4 bg-[#FFFFFF] hover:bg-[#e2e8f0] border border-[#DCE6F2] hover:border-[#1D4ED8] rounded-lg px-4 py-3 transition-all group"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#3b82f6] transition-colors truncate"> <h3 className="text-[#0F172A] font-body font-semibold text-sm group-hover:text-[#1D4ED8] transition-colors truncate">
{thread.title} {thread.title}
</h3> </h3>
<p className="text-[#666] font-body text-xs mt-0.5"> <p className="text-[#666] font-body text-xs mt-0.5">
by{' '} by{' '}
<Link <Link
href={`/profile/${thread.author_id}`} href={profileHref(thread)}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
className="text-[#3b82f6] hover:underline" className="text-[#1D4ED8] hover:underline"
> >
{thread.author_name} {thread.author_name}
</Link> </Link>
@@ -420,12 +426,12 @@ export default function ForumIndexPage() {
</p> </p>
</div> </div>
<div className="flex-shrink-0 text-right hidden sm:block"> <div className="flex-shrink-0 text-right hidden sm:block">
<div className="text-[#aaa] font-body text-sm font-semibold">{thread.reply_count}</div> <div className="text-[#0F172A] font-body text-sm font-semibold">{thread.reply_count}</div>
<div className="text-[#555] font-body text-xs">replies</div> <div className="text-[#475569] font-body text-xs">replies</div>
</div> </div>
<div className="flex-shrink-0 text-right hidden md:block"> <div className="flex-shrink-0 text-right hidden md:block">
<div className="text-[#aaa] font-body text-sm font-semibold">{thread.view_count}</div> <div className="text-[#0F172A] font-body text-sm font-semibold">{thread.view_count}</div>
<div className="text-[#555] font-body text-xs">views</div> <div className="text-[#475569] font-body text-xs">views</div>
</div> </div>
</Link> </Link>
))} ))}
@@ -450,7 +456,7 @@ export default function ForumIndexPage() {
placeholder="Search threads..." placeholder="Search threads..."
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#3b82f6] rounded-lg pl-9 pr-4 py-2.5 text-white font-body text-sm placeholder-[#555] focus:outline-none transition-colors" className="w-full bg-[#FFFFFF] border border-[#DCE6F2] focus:border-[#1D4ED8] rounded-lg pl-9 pr-4 py-2.5 text-[#0F172A] font-body text-sm placeholder-[#888] focus:outline-none transition-colors"
/> />
{search && ( {search && (
<button <button
@@ -470,7 +476,7 @@ export default function ForumIndexPage() {
<select <select
value={selectedCategory} value={selectedCategory}
onChange={e => setSelectedCategory(e.target.value)} onChange={e => setSelectedCategory(e.target.value)}
className="appearance-none bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#3b82f6] rounded-lg pl-3 pr-8 py-2.5 text-sm font-body text-white focus:outline-none transition-colors cursor-pointer min-w-[160px]" className="appearance-none bg-[#FFFFFF] border border-[#DCE6F2] focus:border-[#1D4ED8] rounded-lg pl-3 pr-8 py-2.5 text-sm font-body text-[#0F172A] focus:outline-none transition-colors cursor-pointer min-w-[160px]"
> >
<option value="">All Categories</option> <option value="">All Categories</option>
{categories.map(cat => ( {categories.map(cat => (
@@ -482,16 +488,16 @@ export default function ForumIndexPage() {
</svg> </svg>
</div> </div>
{/* Sort Options */} {/* Sort Options — segmented pill control; active uses brand-gradient */}
<div className="flex gap-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg p-1"> <div className="flex gap-1 bg-[#FFFFFF] border border-[#DCE6F2] rounded-full p-1">
{sortOptions.map(opt => ( {sortOptions.map(opt => (
<button <button
key={opt.value} key={opt.value}
onClick={() => setSort(opt.value)} onClick={() => setSort(opt.value)}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${ className={`px-3 py-1 rounded-full text-xs font-body font-semibold transition-colors whitespace-nowrap ${
sort === opt.value sort === opt.value
? 'bg-[#3b82f6] text-white' ? 'brand-gradient'
: 'text-[#888] hover:text-white' : 'text-[#475569] hover:text-[#0F172A]'
}`} }`}
> >
{opt.label} {opt.label}
@@ -506,13 +512,13 @@ export default function ForumIndexPage() {
{threadsLoading && ( {threadsLoading && (
<div className="space-y-2"> <div className="space-y-2">
{[...Array(6)].map((_, i) => ( {[...Array(6)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-lg h-14 animate-pulse border border-[#252525]" /> <div key={i} className="bg-[#FFFFFF] rounded-lg h-14 animate-pulse border border-[#DCE6F2]" />
))} ))}
</div> </div>
)} )}
{!threadsLoading && threads.length === 0 && ( {!threadsLoading && threads.length === 0 && (
<div className="text-center py-12 text-[#555] font-body"> <div className="text-center py-12 text-[#475569] font-body">
<p className="text-base mb-1">No threads found</p> <p className="text-base mb-1 text-[#0F172A] font-semibold">No threads found</p>
<p className="text-sm">Try adjusting your search or filters.</p> <p className="text-sm">Try adjusting your search or filters.</p>
</div> </div>
)} )}
@@ -523,28 +529,28 @@ export default function ForumIndexPage() {
<Link <Link
key={thread.id} key={thread.id}
href={`/forum/thread/${thread.id}`} href={`/forum/thread/${thread.id}`}
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#3b82f6] rounded-lg px-4 py-3 transition-all group" className="flex items-center gap-4 bg-[#FFFFFF] hover:bg-[#e2e8f0] border border-[#DCE6F2] hover:border-[#1D4ED8] rounded-lg px-4 py-3 transition-all group"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#3b82f6] transition-colors truncate"> <h3 className="text-[#0F172A] font-body font-semibold text-sm group-hover:text-[#1D4ED8] transition-colors truncate">
{thread.title} {thread.title}
</h3> </h3>
<p className="text-[#666] font-body text-xs mt-0.5"> <p className="text-[#666] font-body text-xs mt-0.5">
by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#3b82f6] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link> by <Link href={profileHref(thread)} className="text-[#888] hover:text-[#1D4ED8] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
{thread.forum_categories && ( {thread.forum_categories && (
<> · <span className="text-[#3b82f6]">{thread.forum_categories.name}</span></> <> · <span className="text-[#1D4ED8]">{thread.forum_categories.name}</span></>
)} )}
{' · '}{timeAgo(thread.created_at)} {' · '}{timeAgo(thread.created_at)}
</p> </p>
</div> </div>
<div className="flex-shrink-0 text-right hidden sm:block"> <div className="flex-shrink-0 text-right hidden sm:block">
<div className="text-[#aaa] font-body text-sm font-semibold">{thread.reply_count}</div> <div className="text-[#0F172A] font-body text-sm font-semibold">{thread.reply_count}</div>
<div className="text-[#555] font-body text-xs">replies</div> <div className="text-[#475569] font-body text-xs">replies</div>
</div> </div>
{sort === 'top-voted' && ( {sort === 'top-voted' && (
<div className="flex-shrink-0 text-right hidden md:block"> <div className="flex-shrink-0 text-right hidden md:block">
<div className="text-[#22c55e] font-body text-sm font-semibold">+{thread.upvotes ?? 0}</div> <div className="text-[#22c55e] font-body text-sm font-semibold">+{thread.upvotes ?? 0}</div>
<div className="text-[#555] font-body text-xs">votes</div> <div className="text-[#475569] font-body text-xs">votes</div>
</div> </div>
)} )}
</Link> </Link>
@@ -560,7 +566,7 @@ export default function ForumIndexPage() {
{loading && ( {loading && (
<div className="space-y-3 mb-8"> <div className="space-y-3 mb-8">
{[...Array(8)].map((_, i) => ( {[...Array(8)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-lg h-20 animate-pulse border border-[#252525]" /> <div key={i} className="bg-[#FFFFFF] rounded-lg h-20 animate-pulse border border-[#DCE6F2]" />
))} ))}
</div> </div>
)} )}
@@ -578,39 +584,39 @@ export default function ForumIndexPage() {
{!loading && !error && ( {!loading && !error && (
<> <>
<h2 className="text-white font-heading font-bold text-base mb-3">Browse Categories</h2> <h2 className="text-[#0F172A] font-heading font-bold text-base mb-3">Browse Categories</h2>
<div className="space-y-2"> <div className="space-y-2">
{categories.map(cat => ( {categories.map(cat => (
<Link <Link
key={cat.id} key={cat.id}
href={`/forum/${cat.slug}`} href={`/forum/${cat.slug}`}
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#3b82f6] rounded-lg p-4 transition-all group" className="flex items-center gap-4 bg-[#FFFFFF] hover:bg-[#e2e8f0] border border-[#DCE6F2] hover:border-[#1D4ED8] rounded-lg p-4 transition-all group"
> >
<span className="text-3xl flex-shrink-0 w-10 text-center">{cat.icon}</span> <span className="text-3xl flex-shrink-0 w-10 text-center">{cat.icon}</span>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h2 className="text-white font-heading font-semibold text-lg group-hover:text-[#3b82f6] transition-colors"> <h2 className="text-[#0F172A] font-heading font-semibold text-lg group-hover:text-[#1D4ED8] transition-colors">
{cat.name} {cat.name}
</h2> </h2>
<p className="text-[#888] font-body text-sm mt-0.5 truncate">{cat.description}</p> <p className="text-[#475569] font-body text-sm mt-0.5 truncate">{cat.description}</p>
{cat.last_thread_title && ( {cat.last_thread_title && (
<p className="text-[10px] text-[#666] font-body mt-1.5 truncate"> <p className="text-[10px] text-[#475569] font-body mt-1.5 truncate">
<span className="text-[#555]">Last post:</span>{' '} <span className="text-[#666]">Last post:</span>{' '}
<span className="text-[#aaa]">{cat.last_thread_title}</span> <span className="text-[#1e293b]">{cat.last_thread_title}</span>
{cat.last_activity_at && ( {cat.last_activity_at && (
<span className="text-[#555]"> · {timeAgoFresh(cat.last_activity_at, cat.id)}</span> <span className="text-[#666]"> · {timeAgoFresh(cat.last_activity_at, cat.id)}</span>
)} )}
</p> </p>
)} )}
</div> </div>
<div className="flex-shrink-0 text-right hidden sm:block"> <div className="flex-shrink-0 text-right hidden sm:block">
<div className="text-white font-body font-semibold text-sm">{cat.thread_count}</div> <div className="text-[#0F172A] font-body font-semibold text-sm">{cat.thread_count}</div>
<div className="text-[#666] font-body text-xs">threads</div> <div className="text-[#666] font-body text-xs">threads</div>
</div> </div>
<div className="flex-shrink-0 text-right hidden md:block"> <div className="flex-shrink-0 text-right hidden md:block">
<div className="text-white font-body font-semibold text-sm">{cat.post_count}</div> <div className="text-[#0F172A] font-body font-semibold text-sm">{cat.post_count}</div>
<div className="text-[#666] font-body text-xs">posts</div> <div className="text-[#666] font-body text-xs">posts</div>
</div> </div>
<svg className="w-4 h-4 text-[#555] group-hover:text-[#3b82f6] flex-shrink-0 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 text-[#555] group-hover:text-[#1D4ED8] flex-shrink-0 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg> </svg>
</Link> </Link>
@@ -624,12 +630,11 @@ export default function ForumIndexPage() {
)} )}
{/* Forum guidelines */} {/* Forum guidelines */}
<div className="mt-8 bg-[#1a2535] border border-[#2a3a50] rounded-lg p-5"> <div className="mt-8 bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-5">
<h3 className="text-white font-heading font-semibold mb-2">Community Guidelines</h3> <h3 className="text-[#0F172A] font-heading font-semibold mb-2">Community Guidelines</h3>
<ul className="text-[#aab4c4] font-body text-sm space-y-1"> <ul className="text-[#475569] font-body text-sm space-y-1">
<li> Be respectful and professional this is a community of peers</li> <li> Be respectful and professional this is a community of peers</li>
<li> Share real-world experience and cite sources when possible</li> <li> Share real-world experience and cite sources when possible</li>
<li> The <span className="text-[#3b82f6]">AV Beat AI</span> assistant is available in threads to help answer questions it is clearly labeled</li>
<li> No spam, self-promotion, or vendor pitches without disclosure</li> <li> No spam, self-promotion, or vendor pitches without disclosure</li>
</ul> </ul>
</div> </div>

View File

@@ -73,7 +73,7 @@ export default function EditForumProfilePage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#F8FAFC]">
<div className="max-w-2xl mx-auto px-4 py-12 text-[#666] font-body text-sm">Loading</div> <div className="max-w-2xl mx-auto px-4 py-12 text-[#666] font-body text-sm">Loading</div>
</main> </main>
<Footer /> <Footer />
@@ -85,7 +85,7 @@ export default function EditForumProfilePage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#F8FAFC]">
<div className="max-w-2xl mx-auto px-4 py-12 text-[#888] font-body text-sm"> <div className="max-w-2xl mx-auto px-4 py-12 text-[#888] font-body text-sm">
Profile unavailable. Profile unavailable.
</div> </div>
@@ -98,13 +98,13 @@ export default function EditForumProfilePage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#F8FAFC]">
<div className="max-w-2xl mx-auto px-4 py-8"> <div className="max-w-2xl mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-heading font-bold text-white">Edit Profile</h1> <h1 className="text-2xl font-heading font-bold text-white">Edit Profile</h1>
<Link <Link
href={`/forum/user/${profile.username}`} href={`/forum/user/${profile.username}`}
className="text-xs font-body text-[#3b82f6] hover:underline" className="text-xs font-body text-[#1D4ED8] hover:underline"
> >
View public profile View public profile
</Link> </Link>
@@ -112,10 +112,10 @@ export default function EditForumProfilePage() {
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4" className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-6 space-y-4"
> >
{error && ( {error && (
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#1D4ED8] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{error} {error}
</div> </div>
)} )}
@@ -132,7 +132,7 @@ export default function EditForumProfilePage() {
onChange={(e) => update('username', e.target.value)} onChange={(e) => update('username', e.target.value)}
pattern="[A-Za-z0-9_]{3,24}" pattern="[A-Za-z0-9_]{3,24}"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
<p className="text-[11px] font-body text-[#666] mt-1"> <p className="text-[11px] font-body text-[#666] mt-1">
3-24 characters. Letters, numbers, underscore. 3-24 characters. Letters, numbers, underscore.
@@ -145,7 +145,7 @@ export default function EditForumProfilePage() {
value={profile.display_name} value={profile.display_name}
onChange={(e) => update('display_name', e.target.value)} onChange={(e) => update('display_name', e.target.value)}
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</Field> </Field>
@@ -154,7 +154,7 @@ export default function EditForumProfilePage() {
value={profile.bio || ''} value={profile.bio || ''}
onChange={(e) => update('bio', e.target.value)} onChange={(e) => update('bio', e.target.value)}
rows={4} rows={4}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6] resize-none" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#1D4ED8] resize-none"
/> />
</Field> </Field>
@@ -164,7 +164,7 @@ export default function EditForumProfilePage() {
type="text" type="text"
value={profile.role_title || ''} value={profile.role_title || ''}
onChange={(e) => update('role_title', e.target.value)} onChange={(e) => update('role_title', e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</Field> </Field>
<Field label="Company"> <Field label="Company">
@@ -172,7 +172,7 @@ export default function EditForumProfilePage() {
type="text" type="text"
value={profile.company || ''} value={profile.company || ''}
onChange={(e) => update('company', e.target.value)} onChange={(e) => update('company', e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</Field> </Field>
</div> </div>
@@ -183,7 +183,7 @@ export default function EditForumProfilePage() {
type="text" type="text"
value={profile.location_city || ''} value={profile.location_city || ''}
onChange={(e) => update('location_city', e.target.value)} onChange={(e) => update('location_city', e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</Field> </Field>
<Field label="Country"> <Field label="Country">
@@ -191,7 +191,7 @@ export default function EditForumProfilePage() {
type="text" type="text"
value={profile.location_country || ''} value={profile.location_country || ''}
onChange={(e) => update('location_country', e.target.value)} onChange={(e) => update('location_country', e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</Field> </Field>
</div> </div>
@@ -202,14 +202,14 @@ export default function EditForumProfilePage() {
value={profile.avatar_url || ''} value={profile.avatar_url || ''}
onChange={(e) => update('avatar_url', e.target.value)} onChange={(e) => update('avatar_url', e.target.value)}
placeholder="https://…" placeholder="https://…"
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</Field> </Field>
<button <button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors" className="bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors"
> >
{saving ? 'Saving…' : 'Save Profile'} {saving ? 'Saving…' : 'Save Profile'}
</button> </button>

View File

@@ -101,12 +101,12 @@ function ForumRegisterInner() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]"> <main className="min-h-screen bg-gradient-to-b from-[#F8FAFC] via-[#F8FAFC] to-[#F8FAFC]">
<div className="max-w-5xl mx-auto px-4 py-12 grid grid-cols-1 lg:grid-cols-[1fr_440px] gap-10 items-start"> <div className="max-w-5xl mx-auto px-4 py-12 grid grid-cols-1 lg:grid-cols-[1fr_440px] gap-10 items-start">
{/* Left rail — what membership gets you */} {/* Left rail — what membership gets you */}
<aside className="hidden lg:block"> <aside className="hidden lg:block">
<div className="text-[10px] uppercase tracking-widest text-[#3b82f6] font-mono mb-3">Membership</div> <div className="text-[10px] uppercase tracking-widest text-[#1D4ED8] font-mono mb-3">Membership</div>
<h1 className="font-heading text-white text-4xl xl:text-5xl font-bold leading-tight mb-4"> <h1 className="font-heading text-[#0F172A] text-4xl xl:text-5xl font-bold leading-tight mb-4">
Join AV Beat Join AV Beat
</h1> </h1>
<p className="text-[#cbd5e1] text-base leading-relaxed mb-6 max-w-md"> <p className="text-[#cbd5e1] text-base leading-relaxed mb-6 max-w-md">
@@ -122,22 +122,22 @@ function ForumRegisterInner() {
<li key={String(title)} className="flex gap-3 items-start"> <li key={String(title)} className="flex gap-3 items-start">
<span className="text-xl leading-none flex-shrink-0">{icon}</span> <span className="text-xl leading-none flex-shrink-0">{icon}</span>
<div> <div>
<div className="font-semibold text-white">{title}</div> <div className="font-semibold text-[#0F172A]">{title}</div>
<div className="text-xs text-[#9ca3af] mt-0.5">{desc}</div> <div className="text-xs text-[#9ca3af] mt-0.5">{desc}</div>
</div> </div>
</li> </li>
))} ))}
</ul> </ul>
<div className="mt-6 text-[11px] text-[#666] leading-relaxed max-w-md border-t border-[#252525] pt-4"> <div className="mt-6 text-[11px] text-[#666] leading-relaxed max-w-md border-t border-[#DCE6F2] pt-4">
<strong className="text-[#888]">PR firms &amp; contributors:</strong> a separate contributor application is required to submit press releases. <strong className="text-[#888]">PR firms &amp; contributors:</strong> a separate contributor application is required to submit press releases.
{" "} {" "}
<Link href="/about/advertise" className="text-[#3b82f6] hover:underline">Contact us</Link> if you represent a manufacturer or agency. <Link href="/about/advertise" className="text-[#1D4ED8] hover:underline">Contact us</Link> if you represent a manufacturer or agency.
</div> </div>
</aside> </aside>
{/* Mobile-only hero (collapsed copy) */} {/* Mobile-only hero (collapsed copy) */}
<div className="lg:hidden text-center mb-2"> <div className="lg:hidden text-center mb-2">
<h1 className="font-heading text-white text-2xl font-bold">Join AV Beat</h1> <h1 className="font-heading text-[#0F172A] text-2xl font-bold">Join AV Beat</h1>
<p className="text-[#888] font-body text-sm mt-1"> <p className="text-[#888] font-body text-sm mt-1">
One account for the forum, newsletter preferences, and saved articles. One account for the forum, newsletter preferences, and saved articles.
</p> </p>
@@ -145,14 +145,14 @@ function ForumRegisterInner() {
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl" className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-6 space-y-4 shadow-2xl"
> >
<div className="hidden lg:block mb-2"> <div className="hidden lg:block mb-2">
<div className="text-[10px] uppercase tracking-widest text-[#3b82f6] font-mono mb-1">Create your account</div> <div className="text-[10px] uppercase tracking-widest text-[#1D4ED8] font-mono mb-1">Create your account</div>
<div className="text-[#888] text-xs">Takes 30 seconds. Free, no card needed.</div> <div className="text-[#888] text-xs">Takes 30 seconds. Free, no card needed.</div>
</div> </div>
{error && ( {error && (
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#1D4ED8] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{error} {error}
</div> </div>
)} )}
@@ -171,7 +171,7 @@ function ForumRegisterInner() {
onChange={(e) => setFullName(e.target.value)} onChange={(e) => setFullName(e.target.value)}
autoComplete="name" autoComplete="name"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<div> <div>
@@ -184,7 +184,7 @@ function ForumRegisterInner() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
autoComplete="email" autoComplete="email"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<div> <div>
@@ -198,7 +198,7 @@ function ForumRegisterInner() {
autoComplete="new-password" autoComplete="new-password"
required required
minLength={6} minLength={6}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<div> <div>
@@ -211,29 +211,29 @@ function ForumRegisterInner() {
onChange={(e) => setConfirm(e.target.value)} onChange={(e) => setConfirm(e.target.value)}
autoComplete="new-password" autoComplete="new-password"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] font-body text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-3 rounded-lg transition-colors" className="w-full bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-3 rounded-lg transition-colors"
> >
{loading ? 'Creating account…' : 'Create Free Account'} {loading ? 'Creating account…' : 'Create Free Account'}
</button> </button>
<p className="text-[10px] text-[#666] leading-relaxed text-center px-2"> <p className="text-[10px] text-[#666] leading-relaxed text-center px-2">
By creating an account you agree to our{' '} By creating an account you agree to our{' '}
<Link href="/terms" className="text-[#888] hover:text-[#3b82f6] underline">terms</Link> <Link href="/terms" className="text-[#888] hover:text-[#1D4ED8] underline">terms</Link>
{' '}and{' '} {' '}and{' '}
<Link href="/privacy" className="text-[#888] hover:text-[#3b82f6] underline">privacy policy</Link>. <Link href="/privacy" className="text-[#888] hover:text-[#1D4ED8] underline">privacy policy</Link>.
We&rsquo;ll never sell your email. We&rsquo;ll never sell your email.
</p> </p>
<SocialAuthButtons next={next} /> <SocialAuthButtons next={next} />
<div className="text-center text-xs font-body text-[#888] pt-2 border-t border-[#252525]"> <div className="text-center text-xs font-body text-[#888] pt-2 border-t border-[#DCE6F2]">
Already have an account?{' '} Already have an account?{' '}
<Link <Link
href={`/forum/login?next=${encodeURIComponent(next)}`} href={`/forum/login?next=${encodeURIComponent(next)}`}
className="text-[#3b82f6] hover:underline font-semibold" className="text-[#1D4ED8] hover:underline font-semibold"
> >
Sign in Sign in
</Link> </Link>

View File

@@ -40,20 +40,20 @@ function ResetInner() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]"> <main className="min-h-screen bg-gradient-to-b from-[#F8FAFC] via-[#F8FAFC] to-[#F8FAFC]">
<div className="max-w-md mx-auto px-4 py-16"> <div className="max-w-md mx-auto px-4 py-16">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="text-[10px] uppercase tracking-widest text-[#3b82f6] font-mono mb-2">Reset password</div> <div className="text-[10px] uppercase tracking-widest text-[#1D4ED8] font-mono mb-2">Reset password</div>
<h1 className="text-3xl font-heading font-bold text-white">Set a new password</h1> <h1 className="text-3xl font-heading font-bold text-[#0F172A]">Set a new password</h1>
</div> </div>
{hasSession === false ? ( {hasSession === false ? (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 text-center"> <div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-6 text-center">
<p className="text-[#cbd5e1] mb-4 text-sm"> <p className="text-[#cbd5e1] mb-4 text-sm">
This reset link is expired or invalid. This reset link is expired or invalid.
</p> </p>
<Link <Link
href="/forum/forgot-password" href="/forum/forgot-password"
className="inline-block bg-[#3b82f6] hover:bg-[#2563eb] text-white font-semibold text-sm px-5 py-2 rounded" className="inline-block bg-[#1D4ED8] hover:bg-[#1E3A8A] text-white font-semibold text-sm px-5 py-2 rounded"
> >
Request a new link Request a new link
</Link> </Link>
@@ -61,10 +61,10 @@ function ResetInner() {
) : ( ) : (
<form <form
onSubmit={submit} onSubmit={submit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl" className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-6 space-y-4 shadow-2xl"
> >
{err && ( {err && (
<div className="bg-[#2a0a0a] border border-[#cc0000] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#1D4ED8] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{err} {err}
</div> </div>
)} )}
@@ -84,7 +84,7 @@ function ResetInner() {
autoComplete="new-password" autoComplete="new-password"
minLength={8} minLength={8}
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<div> <div>
@@ -97,13 +97,13 @@ function ResetInner() {
onChange={(e) => setConfirm(e.target.value)} onChange={(e) => setConfirm(e.target.value)}
autoComplete="new-password" autoComplete="new-password"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#3b82f6]" className="w-full bg-[#F8FAFC] border border-[#DCE6F2] rounded-lg px-3 py-2 text-[#0F172A] text-sm focus:outline-none focus:border-[#1D4ED8]"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={busy} disabled={busy}
className="w-full bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-semibold text-sm px-5 py-3 rounded-lg" className="w-full bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 text-white font-semibold text-sm px-5 py-3 rounded-lg"
> >
{busy ? "Saving…" : "Update password"} {busy ? "Saving…" : "Update password"}
</button> </button>

Some files were not shown because too many files have changed in this diff Show More