Skip to content

Commit ef411bb

Browse files
authored
Add initial HTML structure for Smithcv website
1 parent 2d6a454 commit ef411bb

File tree

1 file changed

+241
-0
lines changed

1 file changed

+241
-0
lines changed

index.html

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>Smithcv</title>
7+
<meta name="description" content="Coding projects" />
8+
<style>
9+
:root{
10+
--bg: #0b1020; --fg:#e6e8ef; --muted:#9aa3b2; --accent:#4fd1c5; --card:#12182b;
11+
--ring: 0 0 0 2px var(--accent);
12+
}
13+
:root.light{ --bg:#f8fafc; --fg:#0b1020; --muted:#475569; --accent:#2563eb; --card:#ffffff; }
14+
html,body{height:100%}
15+
body{
16+
margin:0; font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
17+
color:var(--fg); background:radial-gradient(1200px 800px at 70% -10%, rgba(79,209,197,.15), transparent 40%), var(--bg);
18+
overflow-x:hidden;
19+
}
20+
a{color:var(--accent); text-decoration:none}
21+
header{position:sticky; top:0; backdrop-filter:saturate(180%) blur(10px); background:color-mix(in oklab, var(--bg), transparent 60%); border-bottom:1px solid color-mix(in oklab, var(--fg), transparent 92%); z-index:20}
22+
.nav{max-width:1100px; margin:auto; display:flex; align-items:center; gap:16px; padding:12px 20px}
23+
.nav .brand{font-weight:700; letter-spacing:.2px}
24+
.nav .spacer{flex:1}
25+
.btn{display:inline-flex; align-items:center; gap:8px; padding:10px 14px; border-radius:14px; background:var(--card); border:1px solid color-mix(in oklab, var(--fg), transparent 88%); color:var(--fg); cursor:pointer}
26+
.btn:hover{box-shadow:var(--ring)}
27+
28+
.hero{position:relative; isolation:isolate}
29+
canvas#bg{position:absolute; inset:0; width:100%; height:520px; display:block; z-index:-1}
30+
.hero-inner{max-width:1100px; margin:0 auto; padding:80px 20px 40px}
31+
.kicker{color:var(--muted); text-transform:uppercase; letter-spacing:.18em; font-size:.85rem}
32+
.title{font-weight:800; font-size:clamp(2rem, 6vw, 3.4rem); line-height:1.05; margin:.3rem 0 .6rem}
33+
.subtitle{color:var(--muted); max-width:60ch}
34+
.cta{margin-top:22px; display:flex; gap:12px; flex-wrap:wrap}
35+
36+
.grid{max-width:1100px; margin:50px auto; padding:0 20px; display:grid; grid-template-columns:repeat(auto-fit, minmax(260px,1fr)); gap:18px}
37+
.card{background:var(--card); border:1px solid color-mix(in oklab, var(--fg), transparent 88%); border-radius:18px; padding:18px; transform:translateY(8px); opacity:0; transition:.6s ease; will-change:transform,opacity}
38+
.card.visible{transform:translateY(0); opacity:1}
39+
.card h3{margin:.2rem 0 .2rem}
40+
.muted{color:var(--muted)}
41+
42+
.tilt{perspective:800px}
43+
.tilt > .card{transform:rotateX(0) rotateY(0); transition:transform .1s ease, box-shadow .2s ease}
44+
45+
section{max-width:1100px; margin:70px auto; padding:0 20px}
46+
.searchbar{display:flex; gap:10px; align-items:center}
47+
input[type="search"]{flex:1; padding:12px 14px; border-radius:14px; border:1px solid color-mix(in oklab, var(--fg), transparent 85%); background:var(--card); color:var(--fg)}
48+
49+
footer{max-width:1100px; margin:80px auto 40px; padding:0 20px; color:var(--muted)}
50+
.badges{display:flex; gap:10px; flex-wrap:wrap}
51+
.badge{border:1px solid color-mix(in oklab, var(--fg), transparent 85%); padding:6px 10px; border-radius:999px}
52+
53+
@media (prefers-reduced-motion: reduce) { .card{transition:none} }
54+
</style>
55+
</head>
56+
<body>
57+
<header>
58+
<nav class="nav">
59+
<div class="brand">⚓ Smithcv</div>
60+
<a href="#features">Features</a>
61+
<a href="#demo">API Demo</a>
62+
<a href="#faq">FAQ</a>
63+
<div class="spacer"></div>
64+
<button id="themeToggle" class="btn" aria-label="Toggle theme">🌗 Theme</button>
65+
<a class="btn" href="https://github.com/new" target="_blank" rel="noopener">↗ Deploy</a>
66+
</nav>
67+
</header>
68+
69+
<section class="hero">
70+
<canvas id="bg" aria-hidden="true"></canvas>
71+
<div class="hero-inner">
72+
<div class="kicker">Static • Fast • Fancy</div>
73+
<h1 class="title">A cool JavaScript static website ✨</h1>
74+
<p class="subtitle">No backend needed. Canvas particles, scroll animations, tilt effects, theme toggle, client‑side API calls, and a searchable gallery — all GitHub Pages‑friendly.</p>
75+
<div class="cta">
76+
<a class="btn" href="#features">See features</a>
77+
<button id="installPWA" class="btn" hidden>⬇️ Install</button>
78+
</div>
79+
</div>
80+
</section>
81+
82+
<div id="features" class="grid tilt">
83+
<article class="card"><h3>Canvas Stars</h3><p class="muted">Animated particle field using <code>&lt;canvas&gt;</code>.</p></article>
84+
<article class="card"><h3>Theme Toggle</h3><p class="muted">Dark/light with persistent preference.</p></article>
85+
<article class="card"><h3>Scroll Reveal</h3><p class="muted">IntersectionObserver for subtle entrances.</p></article>
86+
<article class="card"><h3>Card Tilt</h3><p class="muted">3D parallax on hover with pointer tracking.</p></article>
87+
<article class="card"><h3>API Demo</h3><p class="muted">Client-side fetch; no servers required.</p></article>
88+
<article class="card"><h3>PWA Ready</h3><p class="muted">Offline cache via service worker.</p></article>
89+
</div>
90+
91+
<section id="demo">
92+
<h2>API demo (client‑side)</h2>
93+
<p class="muted">Click the button to fetch a random programming joke (from <a href="https://v2.jokeapi.dev" target="_blank" rel="noopener">JokeAPI</a>).</p>
94+
<p><button id="jokeBtn" class="btn">🎲 Get a joke</button></p>
95+
<blockquote id="joke" class="muted">(Your joke will appear here)</blockquote>
96+
</section>
97+
98+
<section id="gallery">
99+
<h2>Searchable gallery</h2>
100+
<div class="searchbar">
101+
<input id="search" type="search" placeholder="Filter cards by tag (e.g., ocean, robotics, ai)" />
102+
<span class="muted" id="count"></span>
103+
</div>
104+
<div class="grid" id="cards"></div>
105+
</section>
106+
107+
<footer>
108+
<div class="badges">
109+
<span class="badge">100% static</span>
110+
<span class="badge">No build needed</span>
111+
<span class="badge">Works on GitHub Pages</span>
112+
<span class="badge">Progressive Web App</span>
113+
</div>
114+
<p class="muted">© <span id="year"></span> Smithcv — Built with vanilla JS. <a href="#">Back to top ↑</a></p>
115+
</footer>
116+
117+
<script>
118+
// ——— Theme toggle ———
119+
const root = document.documentElement;
120+
const themeToggle = document.getElementById('themeToggle');
121+
const saved = localStorage.getItem('theme');
122+
if(saved === 'light') root.classList.add('light');
123+
themeToggle.addEventListener('click', () => {
124+
root.classList.toggle('light');
125+
localStorage.setItem('theme', root.classList.contains('light') ? 'light' : 'dark');
126+
});
127+
128+
// ——— Canvas particles (stars) ———
129+
const c = document.getElementById('bg');
130+
const ctx = c.getContext('2d');
131+
let pixels = [], w, h, dpr = Math.min(2, window.devicePixelRatio || 1);
132+
function resize(){ w = c.width = Math.floor(innerWidth * dpr); h = c.height = Math.floor(520 * dpr); c.style.height = '520px'; }
133+
function spawn(){
134+
pixels = Array.from({length: Math.floor((w*h)/(12000*dpr))}, () => ({
135+
x: Math.random()*w,
136+
y: Math.random()*h,
137+
z: Math.random()*0.8+0.2,
138+
vx:(Math.random()-.5)*.2, vy:(Math.random()-.5)*.2
139+
}));
140+
}
141+
function tick(){
142+
ctx.clearRect(0,0,w,h);
143+
ctx.fillStyle = '#ffffff';
144+
for(const p of pixels){
145+
p.x += p.vx; p.y += p.vy;
146+
if(p.x<0||p.x>w) p.vx*=-1; if(p.y<0||p.y>h) p.vy*=-1;
147+
const r = p.z * dpr;
148+
ctx.globalAlpha = 0.35 + p.z*0.65;
149+
ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI*2); ctx.fill();
150+
}
151+
requestAnimationFrame(tick);
152+
}
153+
addEventListener('resize', () => { resize(); spawn(); });
154+
resize(); spawn(); tick();
155+
156+
// ——— Scroll reveal ———
157+
const io = new IntersectionObserver(entries => {
158+
for(const e of entries){ if(e.isIntersecting){ e.target.classList.add('visible'); io.unobserve(e.target); } }
159+
}, {rootMargin: '0px 0px -10% 0px'});
160+
document.querySelectorAll('.card').forEach(el => io.observe(el));
161+
162+
// ——— Tilt effect on feature cards ———
163+
const tilt = document.querySelector('.tilt');
164+
tilt.addEventListener('mousemove', e => {
165+
const rect = tilt.getBoundingClientRect();
166+
const cx = e.clientX - rect.left, cy = e.clientY - rect.top;
167+
tilt.querySelectorAll('.card').forEach(card => {
168+
const r = card.getBoundingClientRect();
169+
const x = (cx - (r.left - rect.left) - r.width/2) / r.width;
170+
const y = (cy - (r.top - rect.top) - r.height/2) / r.height;
171+
card.style.transform = `rotateX(${-y*6}deg) rotateY(${x*8}deg)`;
172+
card.style.boxShadow = `0 10px 30px rgba(0,0,0,${0.2 + Math.abs(x*y)*0.2})`;
173+
});
174+
});
175+
tilt.addEventListener('mouseleave', () => {
176+
tilt.querySelectorAll('.card').forEach(card => {
177+
card.style.transform = 'rotateX(0) rotateY(0)'; card.style.boxShadow = '';
178+
});
179+
});
180+
181+
// ——— API demo ———
182+
const jokeBtn = document.getElementById('jokeBtn');
183+
const jokeEl = document.getElementById('joke');
184+
jokeBtn.addEventListener('click', async () => {
185+
jokeEl.textContent = 'Loading…';
186+
try{
187+
const r = await fetch('https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,sexist,explicit');
188+
const j = await r.json();
189+
jokeEl.textContent = j.type === 'single' ? j.joke : `${j.setup}${j.delivery}`;
190+
}catch(err){ jokeEl.textContent = 'Could not load a joke. (Check network/CORS)'; }
191+
});
192+
193+
// ——— Searchable gallery ———
194+
const data = [
195+
{ title:'NEPTUNE AUV', tags:['ocean','robotics','auv'] },
196+
{ title:'Bathymetry Map', tags:['mapping','gis','ocean'] },
197+
{ title:'Telemetry Dashboard', tags:['ui','charts','ai'] },
198+
{ title:'Hull CFD', tags:['simulation','fluid','robotics'] },
199+
{ title:'Docking Station', tags:['hardware','auv'] },
200+
{ title:'Mission Planner', tags:['ui','planning','robotics'] },
201+
];
202+
const cardsEl = document.getElementById('cards');
203+
const countEl = document.getElementById('count');
204+
const search = document.getElementById('search');
205+
function render(filter=''){
206+
const q = filter.trim().toLowerCase();
207+
const items = data.filter(x => !q || x.tags.some(t => t.includes(q)) || x.title.toLowerCase().includes(q));
208+
cardsEl.innerHTML = items.map(x => `<article class="card visible"><h3>${x.title}</h3><p class="muted">Tags: ${x.tags.join(', ')}</p></article>`).join('');
209+
countEl.textContent = `${items.length}/${data.length}`;
210+
}
211+
render();
212+
search.addEventListener('input', e => render(e.target.value));
213+
214+
// ——— PWA bits ———
215+
const year = document.getElementById('year'); year.textContent = new Date().getFullYear();
216+
let deferredPrompt;
217+
window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; document.getElementById('installPWA').hidden = false; });
218+
document.getElementById('installPWA').addEventListener('click', async () => { deferredPrompt?.prompt(); deferredPrompt = null; });
219+
220+
if('serviceWorker' in navigator){ navigator.serviceWorker.register('./sw.js').catch(()=>{}); }
221+
</script>
222+
223+
<!-- Minimal service worker for offline caching (create sw.js in the same folder) -->
224+
<script type="module">
225+
// If sw.js doesn't exist yet, dynamically create it on first run (Pages will serve it as a static file once committed)
226+
(async () => {
227+
try{
228+
const res = await fetch('./sw.js', {cache:'no-store'});
229+
if(!res.ok){
230+
const sw = `const CACHE='oa-cache-v1';
231+
self.addEventListener('install', e=>{ e.waitUntil(caches.open(CACHE).then(c=>c.addAll(['./','./index.html'])))});
232+
self.addEventListener('fetch', e=>{ e.respondWith(caches.match(e.request).then(r=> r || fetch(e.request))) });`;
233+
const blob = new Blob([sw], {type:'text/javascript'});
234+
const url = URL.createObjectURL(blob);
235+
// No filesystem write available here; this is just for local preview if running from a file server.
236+
}
237+
}catch{}
238+
})();
239+
</script>
240+
</body>
241+
</html>

0 commit comments

Comments
 (0)