Skip to content

Commit 4e1e328

Browse files
author
TechStack Global
committed
feat: upgrade homepage hero to a premium pull-out card stack slider
1 parent 7cb2e67 commit 4e1e328

File tree

1 file changed

+192
-68
lines changed

1 file changed

+192
-68
lines changed

index.html

Lines changed: 192 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -107,103 +107,227 @@ <h1>Make Smarter <br /><span class="gradient-text">Tech Decisions.</span>
107107
Just clear guidance on the tech that powers modern work.</p>
108108

109109
</div>
110-
<!-- Hero Featured Products Carousel -->
111-
<div class="hero-visual slider-container">
110+
<!-- Pull-Out Animated Deck -->
111+
<div class="hero-visual">
112112
<style>
113-
.product-slider {
114-
display: flex;
115-
gap: 1.5rem;
116-
overflow-x: auto;
117-
scroll-snap-type: x mandatory;
118-
padding-bottom: 1rem;
119-
scrollbar-width: none;
120-
/* Firefox */
121-
-ms-overflow-style: none;
122-
/* IE and Edge */
123-
max-width: 100%;
124-
}
125-
126-
.product-slider::-webkit-scrollbar {
127-
display: none;
113+
.deck-container {
114+
position: relative;
115+
width: 100%;
116+
max-width: 400px;
117+
height: 520px;
118+
margin: 0 auto;
119+
perspective: 1200px;
120+
transform-style: preserve-3d;
128121
}
129-
130-
.product-slider>.glass-card {
131-
scroll-snap-align: center;
132-
min-width: 280px;
133-
max-width: 300px;
134-
flex: 0 0 auto;
122+
.deck-card {
123+
position: absolute;
124+
top: 0; left: 0; right: 0;
125+
background: rgba(15, 23, 42, 0.7);
126+
backdrop-filter: blur(16px);
127+
border: 1px solid var(--border-glass);
128+
border-top: 4px solid var(--accent);
129+
border-radius: var(--border-radius);
135130
padding: 2rem;
136-
border-top: 4px solid var(--accent, #38bdf8);
137-
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
131+
height: 480px;
138132
display: flex;
139133
flex-direction: column;
134+
cursor: grab;
135+
user-select: none;
136+
will-change: transform, opacity;
137+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
138+
/* Smooth transitions when dropping or clicking next */
139+
transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.4s linear;
140140
}
141-
142-
.product-slider>.glass-card h3 {
143-
font-size: 1.3rem;
144-
margin-bottom: 0.5rem;
145-
line-height: 1.3;
141+
.deck-card:active {
142+
cursor: grabbing;
146143
}
147-
148-
.product-slider>.glass-card p {
149-
color: var(--text-secondary, #94a3b8);
150-
font-size: 0.9rem;
151-
margin-bottom: 1.5rem;
152-
line-height: 1.5;
153-
flex-grow: 1;
144+
.deck-card img {
145+
width: 100%; height: auto; border-radius: 8px; margin-bottom: 1rem; aspect-ratio: 16/9; object-fit: cover; pointer-events: none;
154146
}
147+
.deck-card h3 { font-size: 1.4rem; margin-bottom: 0.5rem; line-height: 1.3; color: white; }
148+
.deck-card p { color: var(--text-secondary); font-size: 0.95rem; margin-bottom: 1.5rem; line-height: 1.5; pointer-events: none; flex-grow: 1; }
149+
150+
/* The pull indicator */
151+
.pull-indicator {
152+
position: absolute; top: -30px; right: 10px; font-size: 0.85rem; color: var(--accent); opacity: 0;
153+
font-weight: 600; text-transform: uppercase; letter-spacing: 1px; z-index: 10;
154+
animation: bounceX 2s infinite ease-in-out;
155+
pointer-events: none;
156+
}
157+
@keyframes bounceX {
158+
0%, 100% { transform: translateX(0); opacity: 0.8;}
159+
50% { transform: translateX(-10px); opacity: 0.4;}
160+
}
161+
162+
/* Deck states based on data-depth attributes controlled by JS */
163+
.deck-card[data-depth="0"] { transform: translateZ(0) translateY(0) scale(1); z-index: 3; opacity: 1; }
164+
.deck-card[data-depth="1"] { transform: translateZ(-80px) translateY(25px) scale(0.95); z-index: 2; opacity: 0.8; }
165+
.deck-card[data-depth="2"] { transform: translateZ(-160px) translateY(50px) scale(0.9); z-index: 1; opacity: 0.4; }
166+
167+
/* Thrown away states */
168+
.deck-card.throw-left { transform: translateZ(0) translateX(-150%) rotate(-15deg) !important; opacity: 0 !important; }
169+
.deck-card.throw-right { transform: translateZ(0) translateX(150%) rotate(15deg) !important; opacity: 0 !important; }
155170

156-
.product-slider>.glass-card img {
157-
width: 100%;
158-
height: auto;
159-
border-radius: 8px;
160-
margin-bottom: 1rem;
161-
aspect-ratio: 16/9;
162-
object-fit: cover;
171+
/* Controls */
172+
.deck-controls {
173+
display: flex; justify-content: space-between; align-items: center; max-width: 400px; margin: 0 auto; position: relative; z-index: 10;
163174
}
175+
.deck-controls button {
176+
background: rgba(255,255,255,0.05); border: 1px solid var(--border-glass); color: white; width: 45px; height: 45px; border-radius: 50%; cursor: pointer; transition: 0.2s; display: flex; align-items: center; justify-content: center;
177+
}
178+
.deck-controls button:hover { background: var(--accent); color: #000; }
179+
.deck-controls button i { font-size: 1.1rem; }
164180
</style>
165-
<div class="product-slider">
166-
<!-- Sony WH-1000XM5 -->
167-
<div class="glass-card">
168-
<div
169-
style="font-size: 0.75rem; color: var(--accent, #38bdf8); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem;">
181+
182+
<div class="deck-container" id="reviewDeck">
183+
<!-- The indicator shows up to hint users to swipe -->
184+
<div class="pull-indicator" id="pullHint">&larr; Pull to next</div>
185+
186+
<!-- Card 0: Sony -->
187+
<div class="deck-card" data-index="0">
188+
<div style="font-size: 0.75rem; color: var(--accent); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem;">
170189
<i class="fa-solid fa-headphones"></i> Audio
171190
</div>
172191
<img src="posts/images/sony-wh-1000xm5-front.jpg" alt="Sony WH-1000XM5" loading="lazy">
173192
<h3>Sony WH-1000XM5</h3>
174-
<p>Top-tier active noise cancellation and supreme all-day comfort for deep work.</p>
175-
<a class="read-more" href="posts/sony-wh-1000xm5-review.html"
176-
style="font-weight: 700; margin-top: auto;">Read Review <i class="fa-solid fa-arrow-right"
177-
style="margin-left: 5px;"></i></a>
193+
<p>Top-tier active noise cancellation and supreme all-day comfort for deep focus sessions.</p>
194+
<a class="read-more" href="posts/sony-wh-1000xm5-review.html" style="font-weight: 700; display:inline-block; margin-top: auto;">Read Review <i class="fa-solid fa-arrow-right" style="margin-left: 5px;"></i></a>
178195
</div>
179196

180-
<!-- Shure SM7dB -->
181-
<div class="glass-card">
182-
<div
183-
style="font-size: 0.75rem; color: var(--accent, #38bdf8); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem;">
197+
<!-- Card 1: Shure -->
198+
<div class="deck-card" data-index="1">
199+
<div style="font-size: 0.75rem; color: var(--accent); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem;">
184200
<i class="fa-solid fa-microphone"></i> Broadcasting
185201
</div>
186202
<img src="posts/images/shure-sm7db-primary.jpg" alt="Shure SM7dB Microphone" loading="lazy">
187203
<h3>Shure SM7dB</h3>
188-
<p>The industry standard podcast microphone, now with a built-in clean preamp.</p>
189-
<a class="read-more" href="posts/shure-sm7db-review.html" style="font-weight: 700; margin-top: auto;">Read
190-
Review <i class="fa-solid fa-arrow-right" style="margin-left: 5px;"></i></a>
204+
<p>The industry standard podcast microphone, now powered with a built-in clean preamp.</p>
205+
<a class="read-more" href="posts/shure-sm7db-review.html" style="font-weight: 700; display:inline-block; margin-top: auto;">Read Review <i class="fa-solid fa-arrow-right" style="margin-left: 5px;"></i></a>
191206
</div>
192207

193-
<!-- Alienware AW3423DWF -->
194-
<div class="glass-card">
195-
<div
196-
style="font-size: 0.75rem; color: var(--accent, #38bdf8); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem;">
208+
<!-- Card 2: Alienware -->
209+
<div class="deck-card" data-index="2">
210+
<div style="font-size: 0.75rem; color: var(--accent); font-weight: bold; text-transform: uppercase; margin-bottom: 0.5rem;">
197211
<i class="fa-solid fa-desktop"></i> Displays
198212
</div>
199213
<img src="posts/images/alienware-aw3423dwf-front.jpg" alt="Alienware AW3423DWF" loading="lazy">
200214
<h3>Alienware AW3423DWF</h3>
201-
<p>Stunning QD-OLED ultrawide performance for flawless visual immersion.</p>
202-
<a class="read-more" href="posts/alienware-aw3423dwf-review.html"
203-
style="font-weight: 700; margin-top: auto;">Read Review <i class="fa-solid fa-arrow-right"
204-
style="margin-left: 5px;"></i></a>
215+
<p>Stunning QD-OLED ultrawide performance delivering flawless visual immersion.</p>
216+
<a class="read-more" href="posts/alienware-aw3423dwf-review.html" style="font-weight: 700; display:inline-block; margin-top: auto;">Read Review <i class="fa-solid fa-arrow-right" style="margin-left: 5px;"></i></a>
205217
</div>
206218
</div>
219+
220+
<div class="deck-controls">
221+
<button id="deckPrev" aria-label="Previous Review"><i class="fa-solid fa-arrow-left"></i></button>
222+
<span style="font-size: 0.9rem; color: var(--text-muted); font-weight: 600; letter-spacing: 1px;" id="deckCount">1 / 3</span>
223+
<button id="deckNext" aria-label="Next Review"><i class="fa-solid fa-arrow-right"></i></button>
224+
</div>
225+
226+
<script>
227+
document.addEventListener('DOMContentLoaded', () => {
228+
const deck = document.getElementById('reviewDeck');
229+
if(!deck) return;
230+
231+
const cards = Array.from(deck.querySelectorAll('.deck-card'));
232+
const prevBtn = document.getElementById('deckPrev');
233+
const nextBtn = document.getElementById('deckNext');
234+
const countEl = document.getElementById('deckCount');
235+
const hint = document.getElementById('pullHint');
236+
237+
let topIndex = 0;
238+
const total = cards.length;
239+
240+
function updateDeck() {
241+
cards.forEach((card, i) => {
242+
card.classList.remove('throw-left', 'throw-right');
243+
card.style.transition = ''; // reset to CSS transition
244+
card.style.transform = ''; // clears inline drag transforms
245+
246+
// Distance from current top card
247+
let depth = (i - topIndex + total) % total;
248+
card.setAttribute('data-depth', depth.toString());
249+
});
250+
countEl.textContent = `${topIndex + 1} / ${total}`;
251+
}
252+
253+
function throwCard(direction) {
254+
const currentTopCard = cards[topIndex];
255+
// Apply the throw animation class
256+
currentTopCard.classList.add(direction === 'left' ? 'throw-left' : 'throw-right');
257+
258+
// Hide hint after first interaction
259+
if(hint) hint.style.display = 'none';
260+
261+
// Wait enough time for the throw animation to clear visually before re-stacking
262+
setTimeout(() => {
263+
if(direction === 'left' || direction === 'next') {
264+
topIndex = (topIndex + 1) % total;
265+
} else {
266+
topIndex = (topIndex - 1 + total) % total;
267+
}
268+
updateDeck();
269+
}, 300); // 300ms matches CSS transition timing
270+
}
271+
272+
nextBtn.addEventListener('click', () => throwCard('left'));
273+
prevBtn.addEventListener('click', () => throwCard('right'));
274+
275+
// Mouse / Touch Drag Support
276+
let isDragging = false;
277+
let startX = 0;
278+
let currentX = 0;
279+
280+
function onStart(x, y, target) {
281+
// Ignore clicks on links/buttons
282+
if(target.closest('a') || target.closest('button')) return;
283+
isDragging = true;
284+
startX = x;
285+
currentX = x;
286+
// Remove transition visually to make dragging instantaneous
287+
cards[topIndex].style.transition = 'none';
288+
289+
if(hint) hint.style.display = 'none';
290+
}
291+
292+
function onMove(x) {
293+
if(!isDragging) return;
294+
currentX = x;
295+
const diffX = currentX - startX;
296+
const rotate = diffX * 0.05;
297+
// Instantly update transform
298+
cards[topIndex].style.transform = `translateZ(0) translateY(0) translateX(${diffX}px) rotate(${rotate}deg)`;
299+
}
300+
301+
function onEnd() {
302+
if(!isDragging) return;
303+
isDragging = false;
304+
const diffX = currentX - startX;
305+
306+
if(Math.abs(diffX) > 80) { // threshold passed
307+
throwCard(diffX < 0 ? 'left' : 'right');
308+
} else {
309+
// return to center smoothly
310+
updateDeck();
311+
}
312+
}
313+
314+
deck.addEventListener('mousedown', (e) => onStart(e.clientX, e.clientY, e.target));
315+
window.addEventListener('mousemove', (e) => onMove(e.clientX));
316+
window.addEventListener('mouseup', onEnd);
317+
318+
deck.addEventListener('touchstart', (e) => onStart(e.touches[0].clientX, e.touches[0].clientY, e.target), {passive: false});
319+
deck.addEventListener('touchmove', (e) => {
320+
if(isDragging) { e.preventDefault(); onMove(e.touches[0].clientX); }
321+
}, {passive: false});
322+
deck.addEventListener('touchend', onEnd);
323+
324+
// Init
325+
updateDeck();
326+
327+
// Show hint briefly on load
328+
setTimeout(() => { if(hint) hint.style.opacity = '1'; }, 1500);
329+
});
330+
</script>
207331
</div>
208332
</section>
209333
<!-- Featured Categories-->

0 commit comments

Comments
 (0)