diff --git a/.eslintrc.json b/.eslintrc.json
index 8798e28..54ce2d6 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,6 +1,7 @@
{
"extends": ["@finsweet"],
"rules": {
- "no-console": "off"
+ "no-console": "off",
+ "simple-import-sort/imports": "off"
}
}
diff --git a/README.md b/README.md
index 04388d9..d0cb429 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,21 @@ Note: The setup won't automatically clean up deleted files that already exist in
5. As changes are made to the code locally and saved, the [localhost:3000](http://localhost:3000) will then serve those files
+#### Code execution
+
+To execute code after all the scripts have loaded, the script loader emits an event listener on the `window` object. This can come in handy when you want to ensure a certain imported library from another script file has loaded before executing it.
+
+You can use that as following:
+
+ ```html
+
+ ```
+
#### Debugging
There is an opt-in debugging setup that turns on logs in the console. The preference can be toggled via browser console, and is stored in browser localStorage.
diff --git a/home-projects-lightbox.ts b/home-projects-lightbox.ts
deleted file mode 100644
index bc8619a..0000000
--- a/home-projects-lightbox.ts
+++ /dev/null
@@ -1 +0,0 @@
-window.Webflow?.push(() => {});
diff --git a/package.json b/package.json
index 02da93c..3314e64 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
},
"dependencies": {
"@finsweet/ts-utils": "^0.40.0",
+ "fslightbox": "^3.4.1",
"gsap": "^3.12.5",
"htmx.org": "^1.9.10",
"swiper": "^11.0.7"
diff --git a/src/components/cursor.ts b/src/components/cursor.ts
new file mode 100644
index 0000000..8367d5d
--- /dev/null
+++ b/src/components/cursor.ts
@@ -0,0 +1,102 @@
+const CURSOR_OUTER_SELECTOR = '.page-cursor.is-outer';
+const CURSOR_INNER_SELECTOR = '.page-cursor.is-inner';
+
+let scaleAnim: gsap.core.Timeline;
+let outerX: gsap.QuickToFunc;
+let outerY: gsap.QuickToFunc;
+let InnerX: gsap.QuickToFunc;
+let innerY: gsap.QuickToFunc;
+
+export function cursorInit() {
+ if (window.innerWidth < 992) {
+ return;
+ }
+
+ window.gsap.set(CURSOR_INNER_SELECTOR, { scale: 0.3 });
+ window.gsap.set(`${CURSOR_OUTER_SELECTOR}, ${CURSOR_INNER_SELECTOR}`, { opacity: 1 });
+
+ document.addEventListener('mousemove', cursorMove);
+
+ outerX = window.gsap.quickTo(CURSOR_OUTER_SELECTOR, 'left', {
+ duration: 0.2,
+ ease: 'power3',
+ });
+ outerY = window.gsap.quickTo(CURSOR_OUTER_SELECTOR, 'top', {
+ duration: 0.2,
+ ease: 'power3',
+ });
+
+ InnerX = window.gsap.quickTo(CURSOR_INNER_SELECTOR, 'left', {
+ duration: 0.6,
+ ease: 'power3',
+ });
+ innerY = window.gsap.quickTo(CURSOR_INNER_SELECTOR, 'top', {
+ duration: 0.6,
+ ease: 'power3',
+ });
+
+ setScaleAnimations();
+ setLinksInteraction();
+}
+
+function setScaleAnimations() {
+ scaleAnim = window.gsap.timeline({ paused: true });
+
+ scaleAnim.to(
+ CURSOR_OUTER_SELECTOR,
+ {
+ scale: 0.4,
+ duration: 0.35,
+ },
+ 0
+ );
+ scaleAnim.to(
+ CURSOR_INNER_SELECTOR,
+ {
+ opacity: 0,
+ duration: 0.35,
+ },
+ 0
+ );
+}
+
+function cursorMove(mouseEv: MouseEvent) {
+ const cursorPosition = {
+ left: mouseEv.clientX,
+ top: mouseEv.clientY,
+ };
+
+ outerX(cursorPosition.left);
+ outerY(cursorPosition.top);
+ InnerX(cursorPosition.left);
+ innerY(cursorPosition.top);
+}
+
+function setLinksInteraction() {
+ const TARGET_SELECTORS_LIST =
+ 'a, [data-cursor-link], .button.is-form-submit, .process_slider_pagination-bullet';
+
+ document.addEventListener(
+ 'mouseenter',
+ (e: MouseEvent) => {
+ const target = e.target as HTMLElement;
+ const isTargetMatch = target.matches(TARGET_SELECTORS_LIST);
+ if (!isTargetMatch) {
+ return;
+ }
+
+ window.DEBUG('link hover - cursor scale play');
+ scaleAnim.play();
+
+ target.addEventListener(
+ 'mouseleave',
+ (e) => {
+ window.DEBUG('link hover - cursor scale reverse');
+ scaleAnim.reverse();
+ },
+ { once: true }
+ );
+ },
+ true
+ );
+}
diff --git a/src/components/home-hero-image-reveal.ts b/src/components/home-hero-image-reveal.ts
index dcd5ca6..ee48399 100644
--- a/src/components/home-hero-image-reveal.ts
+++ b/src/components/home-hero-image-reveal.ts
@@ -1,16 +1,17 @@
export function homeHeroImageReveal() {
const COMPONENT_SELECTOR = '[data-image-reveal-parent]';
+ const BLOCKS_SELECTOR = `${COMPONENT_SELECTOR} > div`;
const boxes = 10 * 5;
- const DURATION_IN_SEC = 3;
+ const DURATION_IN_SEC = 1;
window.gsap.fromTo(
- `${COMPONENT_SELECTOR} > div`,
+ BLOCKS_SELECTOR,
{
opacity: 1,
},
{
opacity: 0,
- duration: DURATION_IN_SEC / boxes,
+ duration: 0.3,
stagger: {
each: DURATION_IN_SEC / boxes,
from: 'random',
diff --git a/src/components/home-projects-gallery.ts b/src/components/home-projects-gallery.ts
new file mode 100644
index 0000000..255fff9
--- /dev/null
+++ b/src/components/home-projects-gallery.ts
@@ -0,0 +1,118 @@
+import 'fslightbox';
+import { SCRIPTS_LOADED_EVENT } from 'src/constants';
+
+const PROJECTS_LIST_SELECTOR = '[data-projects-list]';
+const PROJECT_ITEM_SELECTOR = '.projects_item-link-wrapper';
+const LIGHTBOX_WRAPPER_SELECTOR = '[data-lightbox-wrapper]';
+const LIGHTBOX_CONTENT_WRAPPER_SELECTOR = '[data-lightbox-content-wrapper]';
+const LIGHTBOX_CLOSE_BTN_ATTRIBUTE = 'data-lightbox-close';
+
+let lightboxWrapperEl: HTMLElement | null;
+let projectsListEl: HTMLElement | null;
+let projectItemsList: NodeListOf;
+
+window.addEventListener(SCRIPTS_LOADED_EVENT, () => {
+ lightboxWrapperEl = document.querySelector(LIGHTBOX_WRAPPER_SELECTOR);
+ projectsListEl = document.querySelector(PROJECTS_LIST_SELECTOR);
+ projectItemsList = document.querySelectorAll(PROJECT_ITEM_SELECTOR);
+
+ if (!lightboxWrapperEl || !projectsListEl || !projectItemsList.length) {
+ window.DEBUG(
+ 'One of these elements not found on page - Lightbox wrapper, Projects List, or any Project items.',
+ 'Looking for',
+ { LIGHTBOX_WRAPPER_SELECTOR, PROJECTS_LIST_SELECTOR, PROJECT_ITEM_SELECTOR },
+ { lightboxWrapperEl, projectsListEl, projectItemsList }
+ );
+ return;
+ }
+
+ initLightbox();
+ onProjectsItemLoad();
+ refreshLightbox();
+});
+
+/**
+ * On loading new projects via CMS Load
+ */
+window.fsAttributes = window.fsAttributes || [];
+window.fsAttributes.push([
+ 'cmsload',
+ (listInstances) => {
+ const [listInstance] = listInstances;
+
+ listInstance.on('renderitems', (renderedItems: NodeListOf) => {
+ projectItemsList = renderedItems;
+
+ onProjectsItemLoad();
+ });
+ },
+]);
+
+function onProjectsItemLoad() {
+ document.querySelectorAll(`${PROJECT_ITEM_SELECTOR}[data-slug]`).forEach((item) => {
+ initHTMX(item);
+ });
+}
+
+/**
+ * Sets `hx-get` attribute on the project list items, dynamically joining the slug
+ */
+function initHTMX(item: HTMLElement) {
+ const slug = item.getAttribute('data-slug');
+ const currentGetPath = item.getAttribute('hx-get') || '/projects/';
+ item.setAttribute('hx-get', currentGetPath + slug);
+
+ item.removeAttribute('data-slug');
+
+ htmx.process(item);
+}
+
+function initLightbox() {
+ window.gsap.set(lightboxWrapperEl, { opacity: 0 });
+
+ const lightboxContentWrapperEl = lightboxWrapperEl?.querySelector(
+ LIGHTBOX_CONTENT_WRAPPER_SELECTOR
+ );
+
+ // open
+ projectsListEl?.addEventListener('click', (clickEv) => {
+ const target = clickEv.target as HTMLElement;
+ if (!target.closest(PROJECT_ITEM_SELECTOR)) {
+ return;
+ }
+
+ window.gsap.set(lightboxWrapperEl, { display: 'block' });
+ window.gsap.set('body', { overflow: 'hidden' });
+ window.gsap.to(lightboxWrapperEl, { opacity: 1, duration: 0.3 });
+ });
+
+ // close
+ lightboxWrapperEl?.addEventListener('click', (clickEv) => {
+ const target = clickEv.target as HTMLElement;
+ if (!target.closest(`[${LIGHTBOX_CLOSE_BTN_ATTRIBUTE}]`)) {
+ return;
+ }
+
+ window.gsap.to(lightboxWrapperEl, {
+ opacity: 0,
+ duration: 0.3,
+ onComplete: () => {
+ window.gsap.set(lightboxWrapperEl, { display: 'none' });
+ window.gsap.set('body', { overflow: 'auto' });
+
+ if (lightboxContentWrapperEl) {
+ lightboxContentWrapperEl.innerHTML = '';
+ }
+ },
+ });
+ });
+}
+
+function refreshLightbox() {
+ htmx.onLoad(function (content) {
+ window.DEBUG('htmx content loaded', content);
+ refreshFsLightbox();
+ });
+}
+
+export {};
diff --git a/src/components/home-projects-load.ts b/src/components/home-projects-load.ts
deleted file mode 100644
index 452f8cc..0000000
--- a/src/components/home-projects-load.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-window.fsAttributes = window.fsAttributes || [];
-window.fsAttributes.push([
- 'cmsload',
- (listInstances) => {
- const [listInstance] = listInstances;
-
- listInstance.on('renderitems', (renderedItems) => {
- console.log(renderedItems);
- });
- },
-]);
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..641827b
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1 @@
+export const SCRIPTS_LOADED_EVENT = 'scriptsLoaded';
diff --git a/src/entry.ts b/src/entry.ts
index d5e6435..97ca2ec 100644
--- a/src/entry.ts
+++ b/src/entry.ts
@@ -3,6 +3,7 @@
* Fetches scripts from localhost or production site depending on the setup
* Polls `localhost` on page load, else falls back to deriving code from production URL
*/
+import { SCRIPTS_LOADED_EVENT } from './constants';
import './dev/debug';
import './dev/env';
@@ -12,8 +13,6 @@ const PRODUCTION_BASE =
window.JS_SCRIPTS = new Set();
-export const SCRIPTS_LOADED_EVENT = 'scriptsLoaded';
-
const SCRIPT_LOAD_PROMISES: Array> = [];
// init adding scripts to the page
@@ -81,6 +80,3 @@ function fetchLocalScripts() {
appendScripts();
});
}
-
-// window.Webflow = window.Webflow || [];
-// window.Webflow.push(() => {});
diff --git a/src/global.ts b/src/global.ts
index 067be9d..84e9ca5 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -2,10 +2,12 @@ import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { animatedDetailsAccordions } from './components/accordions';
+import { cursorInit } from './components/cursor';
window.gsap = gsap;
window.gsap.registerPlugin(ScrollTrigger);
window.Webflow?.push(() => {
animatedDetailsAccordions();
+ cursorInit();
});
diff --git a/src/pages/home.ts b/src/pages/home.ts
index 68f5184..fcbb317 100644
--- a/src/pages/home.ts
+++ b/src/pages/home.ts
@@ -1,5 +1,9 @@
+import { homeHeroImageReveal } from 'src/components/home-hero-image-reveal';
import { initProcessSlider } from 'src/components/home-process-slider';
+import 'src/components/home-projects-gallery';
+import { SCRIPTS_LOADED_EVENT } from 'src/constants';
-window.Webflow?.push(() => {
+window.addEventListener(SCRIPTS_LOADED_EVENT, () => {
+ homeHeroImageReveal();
initProcessSlider();
});