<?php
require('variables.php');
?>
<!doctype html>
<html lang="en" class="loading-screen full-motion <?php echo IsMobileDevice(); ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Creator/Designer: Jeremy Katlic (Web and Software Developer)
Portfolio Website: showcasing my ability, passion, experience, and creativity">
<title>Jeremy Katlic | Developer</title>
<link rel="shortcut icon" href="images/pizza.svg" />
<!-- Fonts: -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400;1,700&family=Pacifico&family=Permanent+Marker&family=Poppins:ital,wght@0,400;0,600;0,800;1,400;1,600;1,800&display=swap" rel="stylesheet">
<link id="hljs-theme" rel="stylesheet" href=""> <!-- this will load in the JS when we can determine the sites theme -->
<!-- CSS Backbone -->
<link rel="stylesheet" type="text/css" href="css/modules.css" />
<link rel="stylesheet" type="text/css" href="css/main.css" />
<!-- just some google analytics stuff -->
</head>
<body>
<a href="#main-content" class="skip-to-main">Skip to main content</a>
<!--
Making it an img rather than a background-image to make the animating and styling a little bit easier
https://buntinglabs.com/tools/download-topo-map-contour-lines -->
<img class="page-bg" fetchpriority="high" src="images/page-bg.svg" alt="PA contour map - dark" />
<img class="page-bg for-dark-mode" src="images/page-bg-white.svg" loading="lazy" alt="PA contour map - light" />
<!-- Main Page Header -->
<header id="main-header">
<!-- Name -->
<section id="logo-badge">
<div>
<h1><span class="label">Hi! I'm </span><strong class="main-name"><span>Jeremy Katlic</span></strong><strong class="sticky-name">JK</strong></h1>
</div>
</section>
<!-- Main Menu - all page links and toggles -->
<section id="main-menu">
<nav id="main-menu-panel" class="menu-panel hidden" aria-hidden="true" role="menu">
<ul>
<?php
foreach ($menu_controls as $title => $settings) {
$item = "<li>";
if ($settings['type'] == 'link') { //Links
$item .= '<a class="' . $settings['class'] . '" href="' . $settings['url'] . '" target="_blank" tabindex="-1" role="menuitem"';
if (isset($settings['extra_attr'])) {
$item .= ' ' . $settings['extra_attr'];
}
$item .= '><i class="' . $settings['icon'] . '"></i><span class="label">' . $title . '</span></a>';
} else { //Buttons
$item .= '<button class="' . $settings['class'] . '" aria-label="' . $settings['aria_label'] . '" tabindex="-1" role="menuitem"';
if (isset($settings['extra_attr'])) {
$item .= ' ' . $settings['extra_attr'];
}
$item .= '><i class="' . $settings['icon'] . '"></i>';
if (isset($settings['toggle_icon'])) {
$item .= '<i class="toggle-icon ' . $settings['toggle_icon'] . '"></i>';
}
$item .= '<span class="label">' . $title . '</span></button>';
}
echo $item .= '</li>';
}
?>
</ul>
<button class="close-trigger" tabindex="-1" aria-expanded="true" aria-controls="main-menu-panel" aria-label="Close the main menu"><i class="icon-close"></i><span class="for-a11y">Close the Menu</span></button>
</nav>
<button id="menu-toggle" aria-expanded="false" aria-controls="main-menu-panel" aria-label="Toggle the main menu"><i class="control-icon"><span class="bar-1"></span><span class="bar-2"></span><span class="bar-3"></span><span class="bar-4"></span></i> <span class="for-a11y">Toggle Menu</span></button>
</section>
</header>
<!-- Code View - a look at the code from the site, since it's not going up on a public git repo -->
<section id="code-view">
<div id="code-view-tabs">
<?php //these aren't pulling live-code, this way we can remove some important information where necessary, and to obfuscate the live directory structure a little
BuildCodePanel('code-view/php', 'php');
BuildCodePanel('code-view/sass', 'scss');
BuildCodePanel('code-view/css', 'css');
BuildCodePanel('code-view/js', 'js');
?>
</div>
</section>
<!-- Main Page Content - Welcome and Portfolio -->
<main id="main-content">
<!-- Welcome Panel and Statement -->
<section id="welcome-panel">
<div id="welcome-title">
<!-- Image -->
<div class="bg-image">
<img fetchpriority="high" src="images/welcome-bg.jpeg" alt="View of Raystown Dam in PA" />
</div>
<!-- Welcome Message -->
<div class="title-box">
<span class="title-shadow" role="presentation">Welcome!</span>
<h2 class="title">Welcome!</h2>
</div>
</div>
<!-- Welcome Statement -->
<div class="statement">
<p>With over a decade of experience in web development, I've had the opportunity to work on a variety of projects across different industries and technologies. My focus is on developing web solutions that are not just functional, but also intuitive, inclusive, and genuinely impactful for both clients and users.</p>
<p>These are a few of my favorite projects from the past few years that I've been fortunate enough to help bring to life. Created in partnership with skilled teams and designers, they represent my ongoing commitment to building thoughtful and impactful digital experiences.</p>
<!-- <div id="personal-hobbies-ticker"></div> -->
</div>
</section>
<!-- Portfolio Projects -->
<section id="portfolio-work">
<?php
foreach ($portfolio_work as $name => $content) {
$item = '<section id="' . preg_replace('/\s+/', '-', strtolower($name)) . '-project" class="project ' . ((isset($content['award']) && $content['award'][0]) ? 'won-award' : '') . '">
<figure class="project-card" data-x="' . $content['animation']['x'] . '" data-y="' . $content['animation']['y'] . '" data-scale="' . $content['animation']['scale'] . '" data-start="' . $content['animation']['start'] . '" data-rotate="' . $content['animation']['rotateStart'] . '">';
//Main Link
$item .= '<a class="project-link" href="' . $content['url'] . '" target="_blank">';
//Image
$item .= '<div class="project-image"><img src="' . $content['img'] . '" loading="lazy" alt="Screenshot of ' . $name . '\'s homepage - built while at Finalsite" /></div>';
//Title/text
$item .= '<figcaption class="project-info">
<h2 class="project-title">' . $name . '</h2>
<p class="project-location">' . $content['location'] . '</p>
<p class="project-role"><strong>Role:</strong> ' . $content['role'] . '</p>
</figcaption>
';
$item .= '</a>';
//Award Link
if(isset($content['award']) && $content['award'][0]) {
$item .= '<a class="project-award" href="' . $content['award'][1] . '" target="_blank"><span class="for-a11y">Award Won</span></a>';
}
echo $item .= '</figure></section>';
}
?>
</section>
</main>
<!-- Main Page Footer -->
<footer id="main-footer">
<!-- Legal Disclaimer -->
<div id="disclaimer">
<p><?php echo $disclaimer; ?></p>
</div>
<!-- Code Stack for the Site -->
<section id="site-information">
<div id="site-stack">
<ul>
<?php
foreach ($site_stack as $resource) {
$item = "<li>";
$item .= '<p class="' . $resource[1] . '" title="' . $resource[0] . '"><span class="for-a11y">' . $resource[0] . '</span></p>';
echo $item .= '</li>';
}
?>
</ul>
</div>
<!-- Legal Copyright Notice -->
<div id="copyright">
<small><?php echo $copyright; ?></small>
</div>
</section>
<!-- Here to make DOM change happen with less overhead -->
<div id="mobile-disclaimer">
<p><?php echo $disclaimer; ?></p>
</div>
</footer>
<!--
The JS Backbone
-->
<!-- jQuery-->
<script
src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
crossorigin="anonymous"></script>
<!-- GSAP - https://gsap.com
Supported by Webflow.
Maintained by GSAP -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/Flip.min.js"></script>
<!-- Highlight.js - https://highlightjs.org
BSD 3-Clause License
Copyright (c) 2006, Ivan Sagalaev.
All rights reserved. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/php.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/scss.min.js"></script>
<!-- W3C for basic a11y ui -->
<script src="w3c/tabs.js"></script>
<!--
Jeremy Katlic Copyright 2025
If you're reading this then here's a little peek at another project of mine I'm still working on (www.e46project.com) - while it's not on the website, the car is running and racing I'm just behind on updating and building the site out currently.
Thanks for taking a deeper look :)
P.S. I love easter eggs.. sooo good luck hunting haha-->
<script src="js/main.js" type="text/javascript"></script>
</body>
</html>
gsap.registerPlugin(ScrollTrigger);
gsap.registerPlugin(Flip);
/// Adds :focusable - based on jQuery UI
$.extend($.expr[':'], {
focusable: function (elem) {
const $el = $(elem);
if (!$el.is(':visible') || $el.is(':disabled')) return false;
const nodeName = elem.nodeName.toLowerCase();
const tabIndex = $el.attr('tabindex');
const hasTabIndex = !isNaN(tabIndex) && parseInt(tabIndex, 10) >= 0;
const focusableElements = /^(input|select|textarea|button|object)$/;
return (
focusableElements.test(nodeName) ||
(nodeName === 'a' && $el.attr('href')) ||
hasTabIndex ||
$el.is('[contenteditable]')
);
}
});
//Adjust GSAP scrub value for mobile
const animationScrub = (document.getElementsByClassName('is-mobile-device').length) ? 1.2 : true;
// The Main Site Object
const PROFILE = {
animationsBackboneState: true, //this is used to check if the user as chosen to reduce the page animations
init () {
this.motionToggle(); //this runs first because it can have an effect on animations for everything
this.darkMode();
this.mainMenu();
this.generalPageAnimations();
this.projects();
this.codeView();
this.footer();
this.loadingScreen();
// welcomePanel - runs after the loading-screen so the loading-screen styles don't impact the calculations
///
/// Manage the Animation Backbone - breakpoints, img loads, user preference, etc.
$(window).on('load', (e) => {
ScrollTrigger.refresh();
});
$(window).on('resize', (e) => {
ScrollTrigger.refresh();
});
if (!PROFILE.animationsBackboneState) {
PROFILE.SetAnimationBackboneState(false);
}
},
//Return a random number between a range
GetRandomNumber (min, max) {
return Math.random() * (max - min) + min;
},
//Cookie management
GetCookie (name){
let nameP = encodeURIComponent(name) + "=";
let ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
let c = ca[i]
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameP) === 0) return decodeURIComponent(c.substring(nameP.length, c.length));
}
return null
},
SetCookie (name, value) {
document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + ";path=/";
},
//User Options
SetCodeTheme (isDark) {
//Will let us toggle between the different Highlight.js themes - there isn't a href predefined, so we only load a file once in theory
const linkElem = document.getElementById("hljs-theme");
const themes = {
light: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css',
dark: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css'
};
if (linkElem) {
if(isDark) {
linkElem.href = themes.dark;
} else {
linkElem.href = themes.light;
}
}
},
SetAnimationBackboneState (state) {
PROFILE.animationsBackboneState = state;
if(!state){
document.documentElement.classList.add('reduce-motion');
document.documentElement.classList.remove('full-motion');
ScrollTrigger.getAll().forEach(trigger => trigger.disable());
} else {
document.documentElement.classList.remove('reduce-motion');
document.documentElement.classList.add('full-motion');
ScrollTrigger.getAll().forEach(trigger => trigger.enable());
ScrollTrigger.refresh();
}
},
/// Loading Screen
loadingScreen () {
//Check that we are using a loading screen, and that the user hasn't reduced the motion
if ($('html').hasClass('loading-screen') && $('html').hasClass('full-motion')) {
//try to keep the user at the top for the loading animation - it can look broken otherwise
window.scrollTo(0, 0);
$(window).on('beforeunload', () => {
window.scrollTo(0, 0);
});
//stops the user from being able to scroll while we are animating - this is a bit dirty, but we can't run the GSAP animations until the sizing and position of the welcome-panel image is done
$('html').addClass('prevent-scrolling');
setTimeout(() => {
$('html').addClass('stage-1'); //Stage 1 - header elements show
setTimeout(() => {
$('html').addClass('stage-2'); //Stage 2 - welcome message shows, and background fills the window
//Cleanup
setTimeout(() => {
$('html').removeClass('prevent-scrolling loading-screen stage-1 stage-2');
PROFILE.welcomePanel();//running this here so it doesn't conflict with the positioning during the loading screen
ScrollTrigger.refresh(); //update all of the GSAP to be safe
}, 570);
}, 1250);
}, 500);
} else {
//The user has chosen a simpler path - reward them with +1 page access speed
$('html').removeClass('loading-screen');
PROFILE.welcomePanel();
}
},
/// Main Menu
mainMenu () {
//Link obfuscation technique
const user = 'hello.world';
const domain = 'jeremykatlic.dev';
const el = $('.em-link');
el.each((i, link) => {
link.href = 'mailto:' + user + '@' + domain;
$(link).attr('aria-label', 'Email: ' + user + '@' + domain);
$('.label', link).html('Email')
});
//**
// Main Menu
// */
const menuPanel = $('#main-menu-panel');
const menuOpenTrigger = $('#main-menu #menu-toggle');
const menuCloseTrigger = $('.close-trigger', menuPanel);
//this is placed in the html so that the animation can be set in the CSS without it being visible to the user
setTimeout(() => {
menuPanel.removeClass('hidden');
}, 800);
/// Events
$('#main-menu-panel').on('keydown', (e) => { //escape check
if (e.key == 'Escape') {
menuCloseTrigger.trigger('click');
}
});
menuOpenTrigger.on('click', (e) => {
const trigger = $(e.currentTarget);
const panel = $('#' + trigger.attr('aria-controls'));
trigger.attr('aria-expanded', true);
panel.attr('aria-hidden', false);
$(':focusable', panel).attr('tabindex', 0);
$(':focusable', panel).first()[0].focus({ preventScroll: true });
});
menuCloseTrigger
.on('click', (e) => {
const trigger = $(e.currentTarget);
const panel = $('#' + trigger.attr('aria-controls'));
trigger.attr('aria-expanded', false);
menuOpenTrigger.attr('aria-expanded', false).focus();
$(':focusable', panel).attr('tabindex', -1);
panel.attr('aria-hidden', 'true');
})
.on('keydown', (e) => { //menu trap
if (!e.shiftKey && e.key == 'Tab') {
e.preventDefault();
$('#' + e.currentTarget.getAttribute('aria-controls') + ' :focusable').first().focus();
}
});
//menu trap
$(':focusable', menuPanel).first().on('keydown', (e) => {
if (e.shiftKey && e.key == 'Tab') {
e.preventDefault();
menuCloseTrigger.focus();
}
});
},
/// General Page Animations
generalPageAnimations() {
//A11y skip-link
$('.skip-to-main').on('click', (e) => {
e.preventDefault();
//scroll the user to the first main content block on the page
$('#welcome-panel .statement')[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
$('#welcome-panel .statement').focus();
});
///Add a little parallax to the page background
gsap.to($('.page-bg'), {
top: -190,
left: -90,
scrollTrigger: {
trigger: $('body'),
start: "top top",
end: "bottom bottom",
scrub: animationScrub,
invalidateOnRefresh: true,
// markers: true
}
});
},
/// Welcome Panel
welcomePanel () {
const panel = $('#welcome-panel');
/// Welcome title Mouse Follow
$(document).on('mousemove', function (e) {
const vw = window.innerWidth;
const vh = window.innerHeight;
const percentX = (e.clientX / vw - 0.5) * -100; // from -50% to +50%
const percentY = (e.clientY / vh - 0.5) * -100;
// Translate from center, plus offset
$('#welcome-panel .title-shadow').css('transform', `translate(calc(-50% + ${percentX}px), calc(-50% + ${percentY}px))`);
});
//Sets up the welcome panel timeline
let welcomePanelTimeline = gsap.timeline({
scrollTrigger: {
trigger: panel, //this uses the parent as the scroll trigger
pin: $('.bg-image', panel),
pinSpacing: false,
start: "top top",
endTrigger: $('.statement', panel),
end: "center center",
scrub: animationScrub,
invalidateOnRefresh: true,
anticipatePin: 1
// markers: true
}
});
//pulls the focus from the image back to the about me
welcomePanelTimeline.to($('.bg-image', panel), {
scale: 0.8,
borderRadius: 15,
duration: 5
}, 0);
//creates a zoom-scale effect
welcomePanelTimeline.to($('.bg-image img', panel), {
scale: 1.42,
blur: '3px',
duration: 5
}, 0);
//add some parallax to the title, and overall depth to the whole animation
welcomePanelTimeline.to($('.title', panel), {
y: 220,
opacity: 0,
scale: 0.8,
duration: 5
}, 0);
//add some parallax to the title, and overall depth to the whole animation
welcomePanelTimeline.to($('.title-shadow', panel), {
opacity: 0,
duration: 1.2
}, 0);
//a little exit animation to pull the statement up over the background image to add more depth
gsap.to($('.statement', panel), {
yPercent: -50,
scale: 0.89,
scrollTrigger: {
trigger: $('.statement', panel),
start: "center-=15px center",
end: "bottom top",
scrub: true,
invalidateOnRefresh: true,
// markers: true
}
});
//now that its all set, check if we should now disable it all haha
if (!PROFILE.animationsBackboneState) {
PROFILE.SetAnimationBackboneState(false);
}
},
/// Portfolio Projects
projects () {
const projects = gsap.utils.toArray("#portfolio-work .project");
///Animates the card content into position
$('.project-card', projects).each((i, card) => {
/// These are setup server side to try and add a dynamic structure to the animations
const { x, y, scale, start, rotate } = card.dataset;
gsap.fromTo(card, {
xPercent: parseFloat(x) || -100,
yPercent: parseFloat(y) || -20,
scale: parseFloat(scale) || 0.2,
rotate: parseFloat(rotate) || -20
},
{
xPercent: 0,
yPercent: 0,
scale: 1,
rotate: 0,
scrollTrigger: {
trigger: card,
start: start || "top 60%",
end: (i < $('.project-card', projects).length - 1) ? "center center" : "center 70%",
pin: false,
scrub: animationScrub,
anticipatePin: 1,
// markers: true,
/// When we start to slide them back out, straighten out the last one so it's straight
onEnterBack: ({ progress, direction, isActive }) => {
gsap.to(projects[projects.indexOf($(card).closest('.project')[0]) - 1], {
x: 0,
y: 0,
rotate: 0,
duration: 1,
delay: 0.25
});
}
}
},
);
});
/// Stick the card to the center of the screen
projects.forEach((project, index) => {
ScrollTrigger.create({
trigger: project,
start: "center center",
endTrigger: $('#portfolio-work'),
end: 'bottom bottom',
pin: true,
pinSpacing: false,
invalidateOnRefresh: true,
// markers: true,
/// when we stick it rotate it
onEnter: ({ progress, direction, isActive }) => {
if (projects.indexOf(project) > 0 && progress < 0.8){
gsap.to(projects[index - 1], {
x: Math.floor(PROFILE.GetRandomNumber(-40, 32)),
y: Math.floor(PROFILE.GetRandomNumber(-32, 35)),
rotate: PROFILE.GetRandomNumber(-8, 9),
duration: 0.42
});
}
}
// markers: true
});
});
//For a11y - try to scroll the focused card into view - mainly to traverse up the page with the keyboard
$('a', projects).each((i, cardLink) => {
$(cardLink).on('focus', (e) => {
if(!$('.reduce-motion').length){
$(e.currentTarget).closest('.pin-spacer')[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
});
//for good measure
ScrollTrigger.refresh();
},
/// Dark Mode
darkMode() {
const classString = 'dark-mode';
/// Cookie Check - also loads in the appropriate highlight.js theme and sets the Profiles theme
if (PROFILE.GetCookie(classString) != null && PROFILE.GetCookie(classString) == 'true') {
$('html').addClass(classString);
$('.dark-mode-toggle').addClass(classString);
PROFILE.SetCodeTheme(true);
} else {
PROFILE.SetCodeTheme(false);
}
/// UI for this feature
$('.dark-mode-toggle').on('click', (e) => {
const toggle = $(e.currentTarget);
if (toggle.hasClass(classString)) {
$('html').removeClass(classString);
toggle.removeClass(classString);
$('.label', toggle).html('Dark Mode');
PROFILE.SetCodeTheme(false);
PROFILE.SetCookie(classString, false);
} else {
$('html').addClass(classString);
toggle.addClass(classString);
$('.label', toggle).html('Light Mode');
PROFILE.SetCodeTheme(true);
PROFILE.SetCookie(classString, true);
}
});
/// Cheeky keyboard shortcut to make dev work easier - another easter egg I suppose
$(document).on('keydown', (e) => {
if (e.ctrlKey && e.altKey && (e.key == 'd' || e.key == '∂')) {
$('.dark-mode-toggle').first().trigger('click');
}
});
},
/// Code View
codeView () {
hljs.highlightAll(); //runs Highlight.js to style all of our code
///
// Build the nested tabs
$('#code-view .code-files').each((i, codeFiles) => {
$(codeFiles).prepend('<div role="tablist" aria-label="Tabs for each of the code file types, each panel contains an accordion of each file\'s code"></div>');
$('.code-file', codeFiles).each((i, codeFile) => {
$('[role="tablist"]', codeFiles).append('<button id="' + codeFile.dataset.fileName + '-trigger" role="tab" aria-selected="false" aria-controls="' + codeFile.dataset.fileName + '-file" tabindex="-1"><span>' + $('h3', codeFile).html() + '</span></button>');
});
$('[role="tablist"]', codeFiles).each((i, tabElem) => {
new TabsAutomatic(tabElem);
});
});
///
// Build the main tabs
$('#code-view-tabs').prepend('<div role="tablist" aria-label="Tabs for each of the code file types, each panel contains an accordion of each file\'s code"></div>');
$('#code-view .code-files').each((i, codeFiles) => {
$('#code-view-tabs > [role="tablist"]').append('<button id="' + codeFiles.dataset.fileType + '-trigger" role="tab" aria-selected="false" aria-controls="' + codeFiles.dataset.fileType + '-files" tabindex="-1"><span>' + $('h2', codeFiles).html() + '</span></button>');
});
$('#code-view-tabs > [role="tablist"]').each((i, tabElem) => {
new TabsAutomatic(tabElem);
});
///
// View Toggle
let hideCodeViewTimeout = {},
showCodeViewTimeout = {},
tabFocusTimeout = {};
$('.code-view-toggle')
.on('keydown', (e) => { //a11y-trap
if(e.currentTarget.getAttribute('aria-expanded') == 'true' && e.key == 'Tab'){
e.preventDefault();
if(e.shiftKey){
$('#code-view-tabs .code-files.is-active :focusable').last().focus();
} else {
$('#code-view-tabs > [role="tablist"] > [role="tab"][aria-selected="true"]').focus();
}
}
})
.on('click', (e) => {
const trigger = $(e.currentTarget);
//clear old timeouts
clearTimeout(hideCodeViewTimeout);
clearTimeout(showCodeViewTimeout);
clearTimeout(tabFocusTimeout);
if(trigger.attr('aria-expanded') == 'true'){
trigger.attr('aria-expanded', false);
$('.label', trigger).html('Code View');
$('html').removeClass('animate');
hideCodeViewTimeout = setTimeout(() => {
$('html').removeClass('show-code-view forced-prevent-scrolling');
ScrollTrigger.refresh();
}, 820);
} else {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
});
trigger.attr('aria-expanded', true);
$('.label', trigger).html('Live View');
$('html').addClass('show-code-view forced-prevent-scrolling');
showCodeViewTimeout = setTimeout(() => {
$('html').addClass('animate');
tabFocusTimeout = setTimeout(() => { //waits for visibility to swap over
$('#code-view-tabs > [role="tablist"] > [role="tab"][aria-selected="true"]').focus();
}, 100);
}, 1);
}
});
///
// Code Fullscreen Toggle
$('#code-view .zoom-toggle')
.on('keydown', (e) => { //a11y trap
if (e.key == 'Tab') {
if (e.currentTarget.getAttribute('aria-expanded') == 'true') {
e.preventDefault();
$('code', $(e.currentTarget).closest('.zoom-box')).focus();
} else if (!e.shiftKey) {
e.preventDefault();
$('.code-view-toggle').focus();
}
}
})
.on('click', (e) => {
const trigger = e.currentTarget;
const code = document.getElementById(trigger.getAttribute('aria-controls'));
const state = Flip.getState(code);
const active = (trigger.getAttribute('aria-expanded') == 'true') ? true : false;
if(active){
code.classList.remove('full-screen');
trigger.setAttribute('aria-expanded', false);
} else {
$('#code-view').addClass('full-screen-showing');
code.classList.add('full-screen');
trigger.setAttribute('aria-expanded', true);
}
Flip.from(state, {
duration: 0.7,
ease: 'power2.inOut',
absolute: true,
onComplete: () => {
if (active){
$('#code-view').removeClass('full-screen-showing');
}
}
});
});
/// Cheeky keyboard shortcut to make dev work easier - another easter egg I suppose
$(document).on('keydown', (e) => {
if (e.ctrlKey && e.altKey && (e.key == 'c' || e.key == 'ç')){
$('.code-view-toggle').first().trigger('click');
}
});
///
// A11y trap
$('#code-view').on('keydown', (e) => {
if(e.key == 'Escape') {
e.preventDefault();
if($('#code-view').hasClass('full-screen-showing')) {
$('.zoom-toggle[aria-expanded="true"]', e.currentTarget).trigger('click').focus();
} else {
$('.code-view-toggle').trigger('click').focus();
}
}
});
$('#code-view-tabs > [role="tablist"] > [role="tab"][aria-selected="true"]').on('keydown', (e) => {
if(e.shiftKey && e.key == 'Tab'){
e.preventDefault();
$('.code-view-toggle').focus();
}
});
$('#code-view code').on('keydown', (e) => {
const parent = $(e.currentTarget).closest('.zoom-box');
if(parent.hasClass('full-screen') && e.key == 'Tab'){
e.preventDefault();
$('.zoom-toggle', parent).focus();
}
});
},
/// Motion Toggle (reduce motion on the page)
motionToggle () {
/// UI for this feature
$('.motion-toggle').on("click", (e) => {
const trigger = e.currentTarget;
if($(e.currentTarget).hasClass('animate-mode')){
trigger.classList.add('reduce-mode');
trigger.classList.remove('animate-mode');
trigger.setAttribute('aria-label', 'Increase the motion and animations on the page');
$('.label', trigger).html('Increase Animations');
PROFILE.SetAnimationBackboneState(false);
PROFILE.SetCookie('reduce-motion', true);
} else {
trigger.classList.remove('reduce-mode');
trigger.classList.add('animate-mode');
trigger.setAttribute('aria-label', 'Reduce the motion and animations on the page');
$('.label', trigger).html('Reduce Animations');
PROFILE.SetAnimationBackboneState(true);
PROFILE.SetCookie('reduce-motion', false);
}
});
/// Cookie check - manages PROFILE state as well
const motionCookieState = PROFILE.GetCookie('reduce-motion');
if (motionCookieState != null && motionCookieState == 'true') {
$('.motion-toggle').trigger('click');
}
},
/// Footer setup
footer () {
//run some basic CSS based animations when it hits a certain point
ScrollTrigger.create({
trigger: $('#main-footer'),
start: "63% bottom",
toggleClass: { targets: $('#main-footer'), className: "in-view" },
// markers: true
});
}
}
PROFILE.init();
Welcome!
Welcome!
With over a decade of experience in web development, I've had the opportunity to work on a variety of projects across different industries and technologies. My focus is on developing web solutions that are not just functional, but also intuitive, inclusive, and genuinely impactful for both clients and users.
These are a few of my favorite projects from the past few years that I've been fortunate enough to help bring to life. Created in partnership with skilled teams and designers, they represent my ongoing commitment to building thoughtful and impactful digital experiences.