Skip to content

Commit 567dbcf

Browse files
authored
Merge pull request #5 from ayushedith/impt/web-if
feat(web): implement CORS support for API routes, enhance landing pag…
2 parents 46362b0 + 7924a3c commit 567dbcf

File tree

6 files changed

+222
-74
lines changed

6 files changed

+222
-74
lines changed

pkg/api/server.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,29 @@ func NewAPIServer(basePath, env, apiKey string) (*APIServer, error) {
4646

4747
// RegisterRoutes registers HTTP handlers on the provided mux.
4848
func (s *APIServer) RegisterRoutes(mux *http.ServeMux) {
49-
mux.HandleFunc("/api/collections", s.handleCollections)
50-
mux.HandleFunc("/api/collections/get", s.handleGetCollection)
51-
mux.HandleFunc("/api/collections/save", s.handleSaveCollection)
52-
mux.HandleFunc("/api/run", s.handleRun)
53-
mux.HandleFunc("/api/mock/add", s.handleMockAdd)
54-
mux.HandleFunc("/api/ai/generate-body", s.handleAIGenerateBody)
49+
mux.HandleFunc("/api/collections", s.corsWrap(s.handleCollections))
50+
mux.HandleFunc("/api/collections/get", s.corsWrap(s.handleGetCollection))
51+
mux.HandleFunc("/api/collections/save", s.corsWrap(s.handleSaveCollection))
52+
mux.HandleFunc("/api/run", s.corsWrap(s.handleRun))
53+
mux.HandleFunc("/api/mock/add", s.corsWrap(s.handleMockAdd))
54+
mux.HandleFunc("/api/ai/generate-body", s.corsWrap(s.handleAIGenerateBody))
5555
mux.Handle("/metrics", metrics.Handler())
5656
}
5757

58+
// corsWrap adds permissive CORS headers for local development and handles OPTIONS preflight.
59+
func (s *APIServer) corsWrap(h http.HandlerFunc) http.HandlerFunc {
60+
return func(w http.ResponseWriter, r *http.Request) {
61+
w.Header().Set("Access-Control-Allow-Origin", "*")
62+
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
63+
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
64+
if r.Method == http.MethodOptions {
65+
w.WriteHeader(http.StatusOK)
66+
return
67+
}
68+
h(w, r)
69+
}
70+
}
71+
5872
func writeJSON(w http.ResponseWriter, v interface{}) {
5973
w.Header().Set("Content-Type", "application/json")
6074
_ = json.NewEncoder(w).Encode(v)

web/.env.local

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
NEXT_PUBLIC_BACKEND_URL=http://localhost:8080
2+
NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws

web/assets/nexus.jpg

96.1 KB
Loading

web/pages/_app.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import '../styles/globals.css'
2+
import '../styles/landing.css'
23
import type { AppProps } from 'next/app'
34

45
export default function App({ Component, pageProps }: AppProps) {

web/pages/index.tsx

Lines changed: 137 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,154 @@
11
import React from 'react'
22
import Head from 'next/head'
3-
import Image from 'next/image'
43
import Link from 'next/link'
5-
import '../styles/landing.css'
64

75
export default function Landing() {
8-
return (
9-
<div className="landing-root">
10-
<header className="hero">
11-
<div className="hero-inner">
12-
<div className="hero-copy">
13-
<h1>Nexus — Terminal-first API toolkit</h1>
14-
<p className="tagline">Build, test, mock and collaborate on APIs — fast, Git-native, and privacy-first.</p>
6+
import React, {useEffect, useRef, useState} from 'react'
7+
import Head from 'next/head'
8+
import Link from 'next/link'
159

16-
<div className="cta">
17-
<a className="btn primary" href="/collections">Open the app</a>
18-
<a className="btn ghost" href="https://github.com/ayushedith/nexus" target="_blank" rel="noreferrer">Star on GitHub</a>
19-
</div>
20-
</div>
10+
export default function Landing() {
11+
const [fontCombo, setFontCombo] = useState<'combo1'|'combo2'>('combo1')
12+
const revealRef = useRef<HTMLDivElement | null>(null)
2113

22-
<div className="hero-image">
23-
<Image src="/assets/nexus.jpg" alt="Nexus" width={420} height={300} />
24-
</div>
25-
</div>
26-
</header>
14+
useEffect(() => {
15+
const root = document.querySelector('.landing-root')
16+
if (!root) return
17+
root.classList.remove('font-combo-1','font-combo-2')
18+
root.classList.add(fontCombo === 'combo1' ? 'font-combo-1' : 'font-combo-2')
19+
}, [fontCombo])
20+
21+
useEffect(() => {
22+
const obs = new IntersectionObserver((entries)=>{
23+
entries.forEach(e=>{
24+
if (e.isIntersecting) e.target.classList.add('visible')
25+
})
26+
}, {threshold: 0.12})
27+
const nodes = revealRef.current?.querySelectorAll('.reveal') || []
28+
nodes.forEach(n=>obs.observe(n))
29+
return ()=>obs.disconnect()
30+
}, [])
2731

28-
<Head>
29-
<title>Nexus — API Collections Runner</title>
30-
<meta name="description" content="Run, test and mock APIs locally — with collaboration and AI helpers." />
31-
<link rel="icon" href="/favicon.ico" />
32-
</Head>
32+
function copyCLI() {
33+
try {
34+
navigator.clipboard.writeText('nexus run examples/collections/demo.yaml')
35+
alert('Copied to clipboard')
36+
} catch {
37+
alert('Copy failed')
38+
}
39+
}
40+
41+
return (
42+
<div className={`landing-root ${fontCombo === 'combo1' ? 'font-combo-1' : 'font-combo-2'}`}>
43+
<Head>
44+
<title>Nexus — API Collections Runner</title>
45+
<meta name="description" content="Run, test and mock APIs locally — with collaboration and AI helpers." />
46+
<link rel="icon" href="/favicon.ico" />
47+
</Head>
3348

34-
<section className="features">
3549
<div className="container">
36-
<h2>What it does</h2>
37-
<div className="grid">
38-
<div className="card">
39-
<h3>Terminal-first TUI</h3>
40-
<p>Interactive Bubbletea-based UI for building and running collections with keyboard-friendly controls.</p>
41-
</div>
50+
<div style={{display:'flex',justifyContent:'flex-end',marginBottom:8}}>
51+
<button className="btn ghost" onClick={()=>setFontCombo(f=>f==='combo1'?'combo2':'combo1')} aria-label="Toggle font combo">Switch font</button>
52+
</div>
4253

43-
<div className="card">
44-
<h3>Git-native storage</h3>
45-
<p>Store collections as files in your repo, with commits and history for reproducible tests.</p>
46-
</div>
54+
<section className="hero">
55+
<div className="hero-inner">
56+
<div className="hero-copy">
57+
<h1>Nexus — API tooling, reimagined</h1>
58+
<p className="tagline">A fast, Git-native toolkit to build, run and collaborate on API collections — locally and privately.</p>
4759

48-
<div className="card">
49-
<h3>Mock & Load</h3>
50-
<p>Run a mock server for integration tests and basic load testing out of the box.</p>
51-
</div>
60+
<div className="cta">
61+
<a className="btn primary" href="/collections">Open the app</a>
62+
<a className="btn ghost" href="https://github.com/ayushedith/nexus" target="_blank" rel="noreferrer">Star on GitHub</a>
63+
</div>
64+
65+
<div style={{display:'flex',alignItems:'center',marginTop:14}}>
66+
<div className="code-sample">$ nexus run examples/collections/demo.yaml</div>
67+
<button className="copy-btn" onClick={copyCLI}>Copy</button>
68+
</div>
69+
</div>
5270

53-
<div className="card">
54-
<h3>AI helpers</h3>
55-
<p>Generate request bodies and test assertions using pluggable AI adapters.</p>
71+
<div className="hero-image">
72+
<div className="illustration" aria-hidden>
73+
<svg width="340" height="220" viewBox="0 0 340 220" fill="none" xmlns="http://www.w3.org/2000/svg">
74+
<defs>
75+
<linearGradient id="g1" x1="0" x2="1">
76+
<stop offset="0%" stopColor="#7c3aed" stopOpacity="0.9" />
77+
<stop offset="100%" stopColor="#5b21b6" stopOpacity="0.9" />
78+
</linearGradient>
79+
</defs>
80+
<rect x="8" y="16" width="320" height="188" rx="12" fill="url(#g1)" opacity="0.12"/>
81+
<rect x="26" y="34" width="288" height="140" rx="8" fill="#061025" />
82+
<g transform="translate(40,54)" fill="#0ea5a4">
83+
<rect x="0" y="0" width="220" height="10" rx="4" />
84+
<rect x="0" y="22" width="180" height="10" rx="4" />
85+
<rect x="0" y="44" width="260" height="10" rx="4" />
86+
</g>
87+
<circle cx="280" cy="50" r="18" fill="#f97316" opacity="0.95" />
88+
</svg>
89+
</div>
90+
</div>
5691
</div>
92+
</section>
93+
94+
<div ref={revealRef}>
95+
<section className="features">
96+
<h2 className="reveal">What Nexus gives you</h2>
97+
<div className="grid">
98+
<div className="card reveal">
99+
<h3>Fast local runs</h3>
100+
<p>Execute collections from files with rich results and built-in metrics and retries.</p>
101+
</div>
102+
<div className="card reveal">
103+
<h3>Private mocks</h3>
104+
<p>Run isolated mock servers for integration tests, CI, and local development.</p>
105+
</div>
106+
<div className="card reveal">
107+
<h3>AI-assisted</h3>
108+
<p>Generate request bodies, test assertions, and quick API drafts via OpenAI adapters.</p>
109+
</div>
110+
</div>
111+
</section>
112+
113+
<section style={{padding:'30px 0'}} className="reveal">
114+
<h2>How it works</h2>
115+
<div className="grid" style={{gridTemplateColumns:'repeat(3,1fr)'}}>
116+
<div className="card">
117+
<h3>1. Store Collections</h3>
118+
<p>Keep HTTP requests as YAML files in your repo for reproducible runs and diffs.</p>
119+
</div>
120+
<div className="card">
121+
<h3>2. Run Locally</h3>
122+
<p>Execute from CLI or UI; view responses, timings, and assertion results instantly.</p>
123+
</div>
124+
<div className="card">
125+
<h3>3. Collaborate</h3>
126+
<p>Share realtime edits via WebSockets and attach mock endpoints for integration tests.</p>
127+
</div>
128+
</div>
129+
</section>
130+
131+
<section style={{padding:'30px 0'}} className="reveal">
132+
<h2>What people say</h2>
133+
<div className="grid" style={{gridTemplateColumns:'1fr',gap:12}}>
134+
<div className="card">
135+
<strong>"Nexus replaced several ad-hoc scripts — instant productivity win."</strong>
136+
<div style={{marginTop:8,color:'var(--muted)'}}>— Dev Team at ExampleCorp</div>
137+
</div>
138+
</div>
139+
</section>
57140
</div>
58-
</div>
59-
</section>
60141

61-
<footer className="site-footer">
62-
<div className="container">
63-
<div>Made with ❤️ — <a href="https://github.com/ayushedith/nexus" target="_blank" rel="noreferrer">ayushedith/nexus</a></div>
142+
<footer className="site-footer">
143+
<div style={{display:'flex',flexDirection:'column',gap:8}}>
144+
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
145+
<div>© {new Date().getFullYear()} Nexus — <a href="https://github.com/ayushedith/nexus" target="_blank" rel="noreferrer">ayushedith/nexus</a></div>
146+
<div style={{color:'var(--muted)'}}>Local-first • Git-native • Privacy-first</div>
147+
</div>
148+
<div style={{color:'var(--muted)',fontSize:13}}>Try the CLI: <span style={{fontFamily:'var(--font-mono)'}}>nexus run examples/collections/demo.yaml</span></div>
149+
</div>
150+
</footer>
64151
</div>
65-
</footer>
66-
</div>
67-
)
68-
}
152+
</div>
153+
)
154+
}

web/styles/landing.css

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,75 @@
55
--accent:#7c3aed;
66
}
77

8-
.landing-root{font-family:Inter,ui-sans-serif,system-ui, -apple-system, 'Segoe UI', Roboto; color:#e6eef6; background: linear-gradient(180deg, #071025 0%, #071629 60%); min-height:100vh}
9-
.container{max-width:1100px;margin:0 auto;padding:40px 20px}
8+
/* Import two font combos and a mono font for code */
9+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=Poppins:wght@600;700&family=Montserrat:wght@600;700&family=Roboto:wght@300;400;700&family=JetBrains+Mono:wght@400;600&display=swap');
1010

11-
.hero{padding:80px 0}
11+
/* Font combo variables
12+
Combo 1: Poppins (headings) + Inter (body)
13+
Combo 2: Montserrat (headings) + Roboto (body)
14+
*/
15+
:root{
16+
--font-heading-1: 'Poppins', Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
17+
--font-body-1: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
18+
--font-heading-2: 'Montserrat', Roboto, system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
19+
--font-body-2: 'Roboto', system-ui, -apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
20+
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
21+
}
22+
23+
/* Utility classes to switch combos */
24+
.font-combo-1 { --font-heading: var(--font-heading-1); --font-body: var(--font-body-1); }
25+
.font-combo-2 { --font-heading: var(--font-heading-2); --font-body: var(--font-body-2); }
26+
27+
28+
29+
.landing-root{
30+
font-family: var(--font-body, Inter), ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto;
31+
color:#e6eef6;
32+
background: radial-gradient(1200px 600px at 10% 10%, rgba(124,58,237,0.12), transparent 12%),
33+
linear-gradient(180deg,#071025 0%, #071629 100%);
34+
min-height:100vh;
35+
-webkit-font-smoothing:antialiased;
36+
}
37+
38+
.container{max-width:1200px;margin:0 auto;padding:56px 20px}
39+
40+
.hero{padding:48px 0}
1241
.hero-inner{display:flex;gap:48px;align-items:center;justify-content:space-between}
13-
.hero-copy h1{font-size:48px;margin:0 0 12px}
14-
.tagline{color:var(--muted);margin:0 0 20px}
42+
.hero-copy{flex:1;min-width:0}
43+
.hero-copy h1{font-family:var(--font-heading, 'Poppins', Inter);font-size:48px;line-height:1.02;margin:0 0 10px;font-weight:700}
44+
.tagline{color:var(--muted);margin:0 0 20px;font-size:18px}
1545
.cta{display:flex;gap:12px}
16-
.btn{padding:10px 16px;border-radius:8px;text-decoration:none;color:inherit;border:1px solid rgba(255,255,255,0.06)}
17-
.btn.primary{background:linear-gradient(90deg,var(--accent),#5b21b6);box-shadow:0 6px 18px rgba(124,58,237,0.18)}
18-
.btn.ghost{background:transparent;border:1px solid rgba(255,255,255,0.06)}
46+
.btn{padding:12px 18px;border-radius:10px;text-decoration:none;color:#fff;border:0;font-weight:600;transition:transform .18s ease,box-shadow .18s ease,opacity .18s}
47+
.btn.primary{background:linear-gradient(90deg,var(--accent),#5b21b6);box-shadow:0 12px 30px rgba(92,45,186,0.18)}
48+
.btn.ghost{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted)}
49+
.btn:hover{transform:translateY(-4px);box-shadow:0 18px 40px rgba(92,45,186,0.16)}
1950

20-
.hero-image img{max-width:420px;border-radius:12px;box-shadow:0 10px 30px rgba(2,6,23,0.6)}
51+
/* Copy button next to code sample */
52+
.copy-btn{margin-left:12px;background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted);padding:6px 10px;border-radius:8px;cursor:pointer;transition:background .15s,transform .12s}
53+
.copy-btn:hover{background:rgba(255,255,255,0.02);transform:translateY(-2px)}
2154

22-
.features{padding:48px 0;background:linear-gradient(180deg, transparent 0%, rgba(255,255,255,0.02) 100%)}
55+
.hero-image{width:460px;flex:0 0 460px;display:flex;align-items:center;justify-content:center}
56+
.illustration{width:380px;height:260px;border-radius:18px;background:linear-gradient(180deg,rgba(255,255,255,0.03),rgba(255,255,255,0.02));display:flex;align-items:center;justify-content:center;box-shadow:0 10px 40px rgba(2,6,23,0.6);transition:transform .5s ease}
57+
.illustration:hover{transform:translateY(-6px) rotate(-1deg)}
58+
59+
.features{padding:40px 0;margin-top:12px}
2360
.features h2{color:#dbeafe;margin-bottom:18px}
24-
.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:18px}
25-
.card{background:rgba(255,255,255,0.02);padding:18px;border-radius:10px;border:1px solid rgba(255,255,255,0.02)}
26-
.card h3{margin:0 0 8px}
27-
.card p{margin:0;color:var(--muted)}
61+
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
62+
.card{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));padding:20px;border-radius:12px;border:1px solid rgba(255,255,255,0.02);transition:transform .18s ease,box-shadow .18s ease}
63+
.card:hover{transform:translateY(-6px);box-shadow:0 18px 40px rgba(2,6,23,0.5)}
64+
.card h3{margin:0 0 8px;font-size:16px}
65+
.card p{margin:0;color:var(--muted);font-size:14px}
66+
67+
/* Reveal animation for scroll */
68+
.reveal{opacity:0;transform:translateY(12px) scale(.995);transition:opacity .5s ease,transform .5s cubic-bezier(.2,.9,.2,1)}
69+
.reveal.visible{opacity:1;transform:none}
70+
71+
.code-sample{margin-top:18px;background:rgba(2,6,23,0.6);padding:14px;border-radius:8px;color:#cbd5e1;font-family:var(--font-mono);font-size:13px}
2872

29-
.site-footer{padding:28px 0;color:var(--muted);border-top:1px solid rgba(255,255,255,0.02)}
73+
.site-footer{padding:28px 0;color:var(--muted);border-top:1px solid rgba(255,255,255,0.02);margin-top:36px}
3074

31-
@media (max-width:900px){
32-
.hero-inner{flex-direction:column;align-items:flex-start}
75+
@media (max-width:1000px){
76+
.hero-inner{flex-direction:column-reverse;align-items:flex-start}
77+
.hero-image{width:100%;flex:0 0 auto;margin-bottom:18px}
3378
.grid{grid-template-columns:1fr}
3479
}

0 commit comments

Comments
 (0)