FruitBank/Presentation/Nop.Web/Themes/CarHaven/Views/Home/Index.cshtml

726 lines
29 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@using Nop.Services.Localization
@using Nop.Core
@inject ILocalizationService localizationService
@inject IStoreContext storeContext
@{
Layout = "_ColumnsOne";
var homepageTitle = await localizationService.GetLocalizedAsync(await storeContext.GetCurrentStoreAsync(), s => s.HomepageTitle);
if (!string.IsNullOrEmpty(homepageTitle))
{
NopHtml.AddTitleParts(homepageTitle);
}
var homepageDescription = await localizationService.GetLocalizedAsync(await storeContext.GetCurrentStoreAsync(), s => s.HomepageDescription);
if (!string.IsNullOrEmpty(homepageDescription))
{
NopHtml.AddMetaDescriptionParts(homepageDescription);
}
NopHtml.AppendPageCssClassParts("html-home-page");
// Signal _Root.cshtml to render vault layers
ViewBag.IsHomePage = true;
}
<div class="page home-page">
<div class="page-body">
<!-- ==========================================
VAULT SCROLL SPACER — drives the animation
========================================== -->
<div class="vault-scroll-container" id="vaultScrollContainer"></div>
<!-- ==========================================
STANDARD HOMEPAGE CONTENT (solid bg)
========================================== -->
<div style="background: #fff; position: relative; z-index: 5; padding-top: 70px;">
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageTop })
@* Topic block replaced by immersive feature section below *@
</div>
<!-- ==========================================
MIÉRT A FRUITBANK — IMMERSIVE FEATURES
========================================== -->
<section class="fb-features" id="fbFeatures">
<div class="fb-features-header">
<h2>Miért a <em>FruitBank</em>?</h2>
<p>Nagykereskedelmi megoldás, ami másképp működik.</p>
</div>
<div class="fb-feature" data-align="left">
<div class="fb-feature-img">
<img src="/images/uploaded/freshness.jpg" alt="Farmról frissen" loading="lazy" />
</div>
<div class="fb-feature-content">
<span class="fb-feature-num">01</span>
<h3>Farmról frissen, minden nap</h3>
<p>A megbízható termelőktől történő közvetlen beszerzés garantálja, hogy termékeink a lehető legfrissebb állapotban és kiváló ízminőségben érkeznek Önhöz.</p>
</div>
</div>
<div class="fb-feature" data-align="right">
<div class="fb-feature-img">
<img src="/images/uploaded/delivery.jpg" alt="Másnapi kiszállítás" loading="lazy" />
</div>
<div class="fb-feature-content">
<span class="fb-feature-num">02</span>
<h3>Másnapi nagykereskedelmi kiszállítás</h3>
<p>A hőmérséklet-szabályozott logisztika biztosítja, hogy rendelése minden alkalommal frissen és pontosan érkezzen meg.</p>
</div>
</div>
<div class="fb-feature" data-align="left">
<div class="fb-feature-img">
<img src="/images/uploaded/variety.jpg" alt="Páratlan választék" loading="lazy" />
</div>
<div class="fb-feature-content">
<span class="fb-feature-num">03</span>
<h3>Páratlan választék</h3>
<p>A mindennapi alapanyagoktól az egzotikus különlegességekig egész évben több mint 500 féle gyümölcs és zöldség kínálatában.</p>
</div>
</div>
<div class="fb-feature" data-align="right">
<div class="fb-feature-img">
<img src="/images/uploaded/sustainable.jpg" alt="Fenntartható forrásból" loading="lazy" />
</div>
<div class="fb-feature-content">
<span class="fb-feature-num">04</span>
<h3>Fenntartható forrásból</h3>
<p>Olyan termelőkkel dolgozunk együtt, akik osztoznak a felelős gazdálkodás és az élelmiszer-pazarlás csökkentése iránti elkötelezettségünkben.</p>
</div>
</div>
</section>
<!-- ==========================================
STANDARD HOMEPAGE CONTENT continues
========================================== -->
<div class="homepage-products-section">
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBeforeCategories })
@await Component.InvokeAsync(typeof(HomepageCategoriesViewComponent))
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBeforeProducts })
@await Component.InvokeAsync(typeof(HomepageProductsViewComponent))
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBeforeBestSellers })
@await Component.InvokeAsync(typeof(HomepageBestSellersViewComponent))
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBeforeNews })
@await Component.InvokeAsync(typeof(HomepageNewsViewComponent))
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBeforePoll })
@await Component.InvokeAsync(typeof(HomepagePollsViewComponent))
@await Component.InvokeAsync(typeof(WidgetViewComponent), new { widgetZone = PublicWidgetZones.HomepageBottom })
</div>
<!-- ==========================================
FORKLIFT + STATS SECTION
========================================== -->
<section class="section-forklift">
<div class="forklift-scroll-container" id="forkliftScrollContainer">
<div class="forklift-sticky">
<div class="forklift-visual">
<div class="forklift-body">
<div class="forklift-mast"></div>
<div class="forklift-forks" id="forkliftForks">
<div class="fork-arms"></div>
<div class="forklift-pallet">🍈🍈</div>
</div>
<div class="forklift-chassis">
<div class="forklift-wheel front"></div>
<div class="forklift-wheel rear"></div>
</div>
</div>
</div>
<div class="stats-panel">
<div class="stat-item" id="stat1">
<div class="stat-number"><span class="stat-counter" data-target="12500">0</span>+</div>
<div class="stat-label">Tonnes of Fruit Sold Yearly</div>
</div>
<div class="stat-item" id="stat2">
<div class="stat-number"><span class="stat-counter" data-target="3200">0</span>+</div>
<div class="stat-label">Happy Partners & Clients</div>
</div>
<div class="stat-item" id="stat3">
<div class="stat-number"><span class="stat-counter" data-target="8400">0</span>+</div>
<div class="stat-label">Monthly Orders Fulfilled</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- ==========================================
VAULT + FORKLIFT SCROLL ANIMATION SCRIPT
========================================== -->
<script asp-location="Footer">
(function() {
// =============================================
// FRAME CONFIG
//
// Dial frames: 1-75 (indices 0-74)
// Vault open frames: 76-323 (indices 75-322)
//
// Scroll segments within vault-scroll-container:
// 0-20% → Dial forward (1→75) + Text 1
// 20-40% → Dial reverse (75→1) + Text 2
// 40-60% → Dial forward (1→75) + Text 3
// 60-78% → Dial reverse (75→1) + Text 4
// 78-100% → Vault opening (76→323)
// =============================================
var TOTAL_FRAMES = 323;
var DIAL_START = 0;
var DIAL_END = 74;
var OPEN_START = 75;
var OPEN_END = 322;
var LERP_SPEED = 0.07;
var CROSSFADE = true;
var frames = [];
var loadedCount = 0;
var lenisInstance = null;
// =============================================
// SNAP-TO-SEGMENT CONFIG
// After the user pauses scrolling, if they're
// past the threshold in a dial segment, Lenis
// smoothly snaps to the next segment start.
// =============================================
var SNAP_TRIGGER = 0.005; // 6% into segment triggers snap
var SNAP_DURATION = 1.6; // seconds for snap animation
var isSnapping = false;
var lastSnappedSeg = -1;
var userScrolling = false;
var userScrollTimer = null;
// =============================================
// CANVAS SETUP
// =============================================
var canvas = document.getElementById('vaultCanvas');
if (!canvas) return; // not on homepage
var ctx = canvas.getContext('2d');
var canvasW = 0, canvasH = 0;
function setupCanvas() {
var sample = frames[0];
if (!sample || !sample.naturalWidth) return;
var dpr = window.devicePixelRatio || 1;
var isMobile = window.innerWidth <= 768;
var imgAspect = sample.naturalWidth / sample.naturalHeight;
var vpW = window.innerWidth;
var displayW = vpW;
var displayH = vpW / imgAspect;
if (isMobile) {
displayW *= 1.7;
displayH *= 1.7;
}
canvas.style.width = displayW + 'px';
canvas.style.height = displayH + 'px';
canvas.width = Math.round(displayW * dpr);
canvas.height = Math.round(displayH * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
canvasW = displayW;
canvasH = displayH;
}
// =============================================
// SCROLL SEGMENTS
// =============================================
var segments = [
{ start: 0.00, end: 0.20, type: 'dial', dir: 1, textIdx: 0 },
{ start: 0.20, end: 0.40, type: 'dial', dir: -1, textIdx: 1 },
{ start: 0.40, end: 0.60, type: 'dial', dir: 1, textIdx: 2 },
{ start: 0.60, end: 0.78, type: 'dial', dir: -1, textIdx: 3 },
{ start: 0.78, end: 1.00, type: 'open', dir: 0, textIdx: -1 },
];
var textBlocks = [
document.getElementById('vaultText1'),
document.getElementById('vaultText2'),
document.getElementById('vaultText3'),
document.getElementById('vaultText4'),
];
var vaultContainer = document.getElementById('vaultScrollContainer');
var vaultLayer = document.getElementById('vaultLayer');
var scrollHint = document.getElementById('vaultScrollHint');
var directionLabel = document.getElementById('vaultDirectionLabel');
var progressBar = document.getElementById('vaultScrollProgress');
var textOverlay = document.getElementById('vaultTextOverlay');
// =============================================
// FRAME RENDERING WITH LERP + CROSSFADE
// =============================================
var currentFrame = 0;
var targetFrame = 0;
function getVaultProgress() {
var rect = vaultContainer.getBoundingClientRect();
var scrolled = -rect.top;
var total = rect.height - window.innerHeight;
return Math.max(0, Math.min(1, scrolled / total));
}
function calcTargetFrame(progress) {
var seg = null;
var segProgress = 0;
for (var i = 0; i < segments.length; i++) {
if (progress >= segments[i].start && progress < segments[i].end) {
seg = segments[i];
segProgress = (progress - seg.start) / (seg.end - seg.start);
break;
}
}
if (!seg && progress >= 1) {
seg = segments[segments.length - 1];
segProgress = 1;
}
if (!seg) return 0;
if (seg.type === 'dial') {
if (seg.dir === 1) {
return DIAL_START + segProgress * (DIAL_END - DIAL_START);
} else {
return DIAL_END - segProgress * (DIAL_END - DIAL_START);
}
} else {
return OPEN_START + segProgress * (OPEN_END - OPEN_START);
}
}
function renderFrame() {
if (canvasW === 0) return;
var idxA = Math.floor(currentFrame);
var idxB = Math.ceil(currentFrame);
idxA = Math.max(0, Math.min(TOTAL_FRAMES - 1, idxA));
idxB = Math.max(0, Math.min(TOTAL_FRAMES - 1, idxB));
var blend = currentFrame - Math.floor(currentFrame);
var imgA = frames[idxA];
var imgB = frames[idxB];
if (imgA && imgA.complete && imgA.naturalWidth) {
ctx.clearRect(0, 0, canvasW, canvasH);
if (CROSSFADE && blend > 0.01 && imgB && imgB.complete && imgB.naturalWidth && idxA !== idxB) {
ctx.globalAlpha = 1;
ctx.drawImage(imgA, 0, 0, canvasW, canvasH);
ctx.globalAlpha = blend;
ctx.drawImage(imgB, 0, 0, canvasW, canvasH);
ctx.globalAlpha = 1;
} else {
ctx.drawImage(imgA, 0, 0, canvasW, canvasH);
}
}
}
// =============================================
// MAIN LOOP
// =============================================
function mainLoop() {
var progress = getVaultProgress();
// Scroll progress bar
var totalScroll = document.documentElement.scrollHeight - window.innerHeight;
var scrollPct = (window.scrollY / totalScroll) * 100;
progressBar.style.width = scrollPct + '%';
// Scroll hint
scrollHint.style.opacity = Math.max(0, 1 - progress * 8);
// Target frame
targetFrame = calcTargetFrame(progress);
// Lerp
var diff = targetFrame - currentFrame;
if (Math.abs(diff) > 0.05) {
currentFrame += diff * LERP_SPEED;
} else {
currentFrame = targetFrame;
}
// Render
renderFrame();
// Direction label
var activeSeg = null;
for (var i = 0; i < 4; i++) {
if (progress >= segments[i].start && progress < segments[i].end) {
activeSeg = segments[i];
break;
}
}
if (activeSeg) {
directionLabel.style.opacity = '1';
directionLabel.textContent = activeSeg.dir > 0 ? '\u21BB Jobbra' : '\u21BA Balra';
} else {
directionLabel.style.opacity = '0';
}
// Text blocks
textBlocks.forEach(function(block, idx) {
if (!block) return;
var seg = null;
for (var j = 0; j < segments.length; j++) {
if (segments[j].textIdx === idx) { seg = segments[j]; break; }
}
if (!seg) return;
var textProgress = (progress - seg.start) / (seg.end - seg.start);
var isMobile = window.innerWidth <= 768;
var xPrefix = isMobile ? 'translateX(-50%) ' : '';
if (textProgress < 0 || textProgress > 1) {
block.style.opacity = '0';
block.style.transform = xPrefix + 'translateY(50px)';
} else {
// Fade in 0-60%, hold 60-85%, fade out 85-100%
var alpha = 0;
if (textProgress < 0.60) {
alpha = textProgress / 0.60;
} else if (textProgress < 0.85) {
alpha = 1;
} else {
alpha = 1 - (textProgress - 0.85) / 0.15;
}
block.style.opacity = alpha;
var yShift = (1 - Math.min(1, textProgress / 0.60)) * 50;
block.style.transform = xPrefix + 'translateY(' + yShift + 'px)';
}
});
// Hide vault layer when scrolled past
if (progress >= 1) {
var pastVault = window.scrollY - (vaultContainer.offsetTop + vaultContainer.offsetHeight);
if (pastVault > 200) {
var fadeOut = Math.max(0, 1 - (pastVault - 200) / 300);
vaultLayer.style.opacity = fadeOut;
}
} else {
vaultLayer.style.opacity = '1';
}
// Hide text overlay when not in vault section
var rect = vaultContainer.getBoundingClientRect();
var inVault = rect.top < window.innerHeight && rect.bottom > 0;
textOverlay.style.display = inVault ? 'flex' : 'none';
requestAnimationFrame(mainLoop);
}
// =============================================
// FORKLIFT + STATS
// =============================================
var forkliftContainer = document.getElementById('forkliftScrollContainer');
var forkliftForks = document.getElementById('forkliftForks');
var counters = document.querySelectorAll('.stat-counter');
var statItems = document.querySelectorAll('.stat-item');
var FORK_MIN_Y = 60;
var FORK_MAX_Y = 280;
var currentForkY = FORK_MIN_Y;
var counterValues = Array.from(counters).map(function() { return 0; });
function forkliftLoop() {
if (!forkliftContainer) { requestAnimationFrame(forkliftLoop); return; }
var rect = forkliftContainer.getBoundingClientRect();
var scrolled = -rect.top;
var total = rect.height - window.innerHeight;
var progress = Math.max(0, Math.min(1, scrolled / total));
var targetForkY = FORK_MIN_Y + (FORK_MAX_Y - FORK_MIN_Y) * progress;
currentForkY += (targetForkY - currentForkY) * 0.08;
if (forkliftForks) forkliftForks.style.bottom = currentForkY + 'px';
statItems.forEach(function(item, i) {
var staggerStart = i * 0.2;
var itemProgress = Math.max(0, Math.min(1,
(progress - staggerStart) / (1 - staggerStart)
));
if (itemProgress > 0.05) {
var fadeIn = Math.min(1, (itemProgress - 0.05) / 0.3);
item.style.opacity = fadeIn;
item.style.transform = 'translateY(' + ((1 - fadeIn) * 40) + 'px)';
}
var target = parseInt(counters[i].dataset.target);
var currentTarget = target * Math.min(1, itemProgress / 0.7);
counterValues[i] += (currentTarget - counterValues[i]) * 0.08;
counters[i].textContent = Math.round(counterValues[i]).toLocaleString();
});
requestAnimationFrame(forkliftLoop);
}
// =============================================
// PRELOAD FRAMES
// =============================================
var loadingBarFill = document.getElementById('vaultLoadingBarFill');
var loadingPct = document.getElementById('vaultLoadingPct');
var loadingScreen = document.getElementById('vaultLoadingScreen');
function preloadFrames() {
return new Promise(function(resolve) {
for (var i = 1; i <= TOTAL_FRAMES; i++) {
var img = new Image();
img.src = '/Themes/CarHaven/Content/frames/frame_' + String(i).padStart(4, '0') + '.jpg';
img.onload = img.onerror = function() {
loadedCount++;
var pct = Math.round((loadedCount / TOTAL_FRAMES) * 100);
if (loadingBarFill) loadingBarFill.style.width = pct + '%';
if (loadingPct) loadingPct.textContent = pct + '%';
if (loadedCount === TOTAL_FRAMES) resolve();
};
frames.push(img);
}
});
}
// =============================================
// INIT
// =============================================
preloadFrames().then(function() {
// Hide loading screen
if (loadingScreen) {
loadingScreen.classList.add('hidden');
setTimeout(function() { loadingScreen.style.display = 'none'; }, 700);
}
// Lenis smooth scroll
lenisInstance = new Lenis({
duration: 1.8,
easing: function(t) { return Math.min(1, 1.001 - Math.pow(2, -10 * t)); },
orientation: 'vertical',
smoothWheel: true,
smoothTouch: true,
wheelMultiplier: 0.8,
touchMultiplier: 0.8,
});
gsap.registerPlugin(ScrollTrigger);
lenisInstance.on('scroll', ScrollTrigger.update);
gsap.ticker.add(function(time) { lenisInstance.raf(time * 1000); });
gsap.ticker.lagSmoothing(0);
// Snap-to-segment: listen to RAW user input, not Lenis scroll
// Lenis scroll events fire continuously during interpolation,
// which kills debounce timers. Wheel/touch events only fire
// when the user is actually interacting.
function onUserScrollInput() {
userScrolling = true;
clearTimeout(userScrollTimer);
userScrollTimer = setTimeout(function() {
userScrolling = false;
trySnap();
}, 120); // 120ms after last input = user stopped
}
function trySnap() {
if (isSnapping) return;
var progress = getVaultProgress();
if (progress <= 0 || progress >= 0.78) return;
for (var s = 0; s < segments.length - 1; s++) {
var seg = segments[s];
if (seg.type !== 'dial') continue;
var segProg = (progress - seg.start) / (seg.end - seg.start);
if (segProg > SNAP_TRIGGER && segProg < 0.92 && lastSnappedSeg < s) {
lastSnappedSeg = s;
isSnapping = true;
var nextStart = segments[s + 1].start;
var containerTop = vaultContainer.offsetTop;
var containerScroll = vaultContainer.offsetHeight - window.innerHeight;
var targetScroll = containerTop + nextStart * containerScroll;
lenisInstance.scrollTo(targetScroll, {
duration: SNAP_DURATION,
easing: function(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2,3)/2; },
onComplete: function() { isSnapping = false; }
});
return;
}
}
// Reset tracker if scrolled backwards
for (var r = lastSnappedSeg; r >= 0; r--) {
if (progress < segments[r].start + 0.02) {
lastSnappedSeg = r - 1;
}
}
}
window.addEventListener('wheel', onUserScrollInput, { passive: true });
window.addEventListener('touchmove', onUserScrollInput, { passive: true });
window.addEventListener('touchend', function() {
userScrolling = false;
setTimeout(trySnap, 80);
}, { passive: true });
// Canvas
setupCanvas();
window.addEventListener('resize', setupCanvas);
// Draw first frame
currentFrame = 0;
renderFrame();
// Start loops
requestAnimationFrame(mainLoop);
requestAnimationFrame(forkliftLoop);
// =============================================
// SCROLL REVEAL ANIMATIONS
// Uses GSAP ScrollTrigger (already loaded)
// =============================================
(function initScrollReveal() {
// --- Section titles ---
document.querySelectorAll('.product-grid .title, .home-page-product-grid .title').forEach(function(el) {
el.classList.add('sr-fade-up');
});
// --- Immersive features header ---
var fbHeader = document.querySelector('.fb-features-header');
if (fbHeader) fbHeader.classList.add('sr-fade-up');
// --- Immersive feature rows: image parallax + content reveal ---
document.querySelectorAll('.fb-feature').forEach(function(feature) {
var img = feature.querySelector('.fb-feature-img img');
var content = feature.querySelector('.fb-feature-content');
var align = feature.getAttribute('data-align');
// Parallax on image
if (img) {
gsap.fromTo(img,
{ yPercent: -10 },
{
yPercent: 10,
ease: 'none',
scrollTrigger: {
trigger: feature,
start: 'top bottom',
end: 'bottom top',
scrub: 1
}
}
);
}
// Content slide in from the side
if (content) {
var xFrom = align === 'right' ? 60 : -60;
gsap.from(content, {
x: xFrom,
opacity: 0,
duration: 1,
ease: 'power3.out',
scrollTrigger: {
trigger: feature,
start: 'top 75%',
toggleActions: 'play none none none'
}
});
}
});
// --- Product cards (staggered separately, not sr-fade-up) ---
document.querySelectorAll('.product-grid .item-box').forEach(function(el) {
el.classList.add('sr-stagger-item');
});
// --- Footer (staggered separately) ---
document.querySelectorAll('.footer-block').forEach(function(el) {
el.classList.add('sr-stagger-item');
});
// ---- Animate all sr-fade-up elements ----
gsap.utils.toArray('.sr-fade-up').forEach(function(el) {
gsap.to(el, {
opacity: 1,
y: 0,
duration: 0.8,
ease: 'power3.out',
scrollTrigger: {
trigger: el,
start: 'top 88%',
toggleActions: 'play none none none'
}
});
});
// ---- Animate sr-fade-left elements ----
gsap.utils.toArray('.sr-fade-left').forEach(function(el) {
gsap.to(el, {
opacity: 1,
x: 0,
duration: 0.9,
ease: 'power3.out',
scrollTrigger: {
trigger: el,
start: 'top 88%',
toggleActions: 'play none none none'
}
});
});
// ---- Animate sr-fade-right elements ----
gsap.utils.toArray('.sr-fade-right').forEach(function(el) {
gsap.to(el, {
opacity: 1,
x: 0,
duration: 0.9,
ease: 'power3.out',
scrollTrigger: {
trigger: el,
start: 'top 88%',
toggleActions: 'play none none none'
}
});
});
// ---- Stagger product cards within each grid ----
document.querySelectorAll('.product-grid').forEach(function(grid) {
var cards = grid.querySelectorAll('.item-box.sr-stagger-item');
if (cards.length === 0) return;
gsap.to(cards, {
opacity: 1,
y: 0,
duration: 0.7,
ease: 'power3.out',
stagger: 0.1,
scrollTrigger: {
trigger: grid,
start: 'top 85%',
toggleActions: 'play none none none'
}
});
});
// ---- Stagger footer blocks ----
var footerBlocks = document.querySelectorAll('.footer-block.sr-stagger-item');
if (footerBlocks.length > 0) {
gsap.to(footerBlocks, {
opacity: 1,
y: 0,
duration: 0.6,
ease: 'power2.out',
stagger: 0.12,
scrollTrigger: {
trigger: footerBlocks[0],
start: 'top 90%',
toggleActions: 'play none none none'
}
});
}
})();
});
})();
</script>