├── .eslintrc.json ├── .gitignore ├── README.md ├── animations ├── classes │ ├── animation.ts │ ├── component.ts │ └── page.ts ├── components │ └── preloader.ts ├── elements │ └── Paragraphs.ts ├── index.ts └── pages │ └── home.ts ├── hooks ├── index.ts ├── useFoucFix.tsx └── useIsomorphicLayoutEffect.tsx ├── layout ├── Transition │ ├── Transition.module.scss │ └── index.tsx ├── frontLayout │ ├── Layout.module.scss │ └── index.tsx └── preloader │ ├── Preloader.module.scss │ └── index.tsx ├── module.d.ts ├── next.config.js ├── package.json ├── pages ├── 404.tsx ├── 500.tsx ├── _app.tsx ├── _document.tsx ├── about.tsx ├── careers.tsx ├── contact.tsx └── index.tsx ├── public ├── favicon.ico ├── icon-512.svg ├── icon-black.svg ├── icon.png ├── icon.svg ├── manifest.json ├── sw.js ├── sw.js.map ├── vercel.svg ├── workbox-eac1af49.js └── workbox-eac1af49.js.map ├── scss ├── base │ ├── _base.scss │ └── _reset.scss ├── import.scss ├── index.scss ├── shared │ ├── _links.scss │ └── _typography.scss └── utils │ ├── _functions.scss │ ├── _mixins.scss │ ├── _utils.scss │ └── _variables.scss ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env.development 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /animations/classes/animation.ts: -------------------------------------------------------------------------------- 1 | import { Component } from './component' 2 | import gsap from 'gsap' 3 | 4 | export class Animation extends Component { 5 | observer!: IntersectionObserver 6 | observerOptions: { 7 | root?: Element | null 8 | rootMargin?: string 9 | threshold?: number 10 | } 11 | 12 | rect: { 13 | distanceY: number 14 | targetDistanceY: number 15 | currentDistanceY: number 16 | } 17 | 18 | frame!: number 19 | frameII!: number 20 | 21 | constructor({ element, elements }: { element: string; elements: {} }) { 22 | // call methods here 23 | super({ element, elements }) 24 | 25 | this.observerOptions = { 26 | // rootMargin: "0px 0px -20% 0px", 27 | threshold: 1, 28 | } 29 | 30 | this.rect = { 31 | distanceY: 0, 32 | targetDistanceY: 0, 33 | currentDistanceY: 0, 34 | } 35 | 36 | this.createObserver() 37 | this.calculateDistanceY() 38 | this.updateCurrentDistanceY() 39 | } 40 | 41 | calculateDistanceY(): void { 42 | const { y, height }: DOMRect = this.element.getBoundingClientRect() 43 | this.rect.distanceY = y + height / 2 - window.innerHeight / 2 44 | 45 | this.updateTargetDistanceY() 46 | } 47 | 48 | updateTargetDistanceY(): void { 49 | const endPoint: number = window.innerHeight / 3 50 | 51 | const { y, height }: DOMRect = this.element.getBoundingClientRect() 52 | const newValue: number = y + height / 2 - endPoint 53 | 54 | const clamped: number = gsap.utils.clamp(0, this.rect.distanceY + 50, newValue) 55 | this.rect.targetDistanceY = clamped 56 | 57 | this.frameII = window.requestAnimationFrame(this.updateTargetDistanceY.bind(this)) 58 | } 59 | 60 | updateCurrentDistanceY(): void { 61 | this.rect.currentDistanceY = gsap.utils.interpolate( 62 | this.rect.currentDistanceY, 63 | this.rect.targetDistanceY, 64 | 0.1 65 | ) 66 | 67 | this.frame = window.requestAnimationFrame(this.updateCurrentDistanceY.bind(this)) 68 | } 69 | 70 | // create intersection observer method to animate in and out 71 | createObserver(): void { 72 | this.observer = new window.IntersectionObserver((entries) => { 73 | entries.forEach((entry) => { 74 | if (entry.isIntersecting) { 75 | this.animateIn() 76 | } else { 77 | this.animateOut() 78 | } 79 | }) 80 | }, this.observerOptions) 81 | 82 | this.element && this.observer.observe(this.element) 83 | } 84 | 85 | animateIn(): void {} 86 | animateOut(): void {} 87 | } 88 | -------------------------------------------------------------------------------- /animations/classes/component.ts: -------------------------------------------------------------------------------- 1 | import each from 'lodash/each' 2 | import EventEmitter from 'events' 3 | import AutoBind from 'auto-bind' 4 | 5 | export class Component extends EventEmitter { 6 | element!: HTMLElement 7 | elements: any 8 | selector: any 9 | selectorChildren: {} 10 | 11 | constructor({ element, elements }: { element: string; elements: {} }) { 12 | super() 13 | AutoBind(this) 14 | 15 | this.selector = element 16 | this.selectorChildren = { 17 | ...elements, 18 | } 19 | 20 | this.create() 21 | this.addEventListeners() 22 | } 23 | 24 | create() { 25 | if (this.selector instanceof window.HTMLElement) { 26 | this.element = this.selector 27 | } else { 28 | this.element = document.querySelector(this.selector) 29 | } 30 | this.elements = {} 31 | 32 | each(this.selectorChildren, (selector: any, key) => { 33 | if (selector instanceof window.HTMLElement || selector instanceof window.NodeList) { 34 | this.elements[key] = selector 35 | } else if (Array.isArray(selector)) { 36 | this.elements[key] = selector 37 | } else { 38 | this.elements[key] = this.element.querySelectorAll(selector) 39 | 40 | if (this.elements[key].length === 0) { 41 | this.elements[key] = null 42 | } else if (this.elements[key].length === 1) { 43 | this.elements[key] = this.element.querySelector(selector) 44 | } 45 | } 46 | }) 47 | } 48 | 49 | addEventListeners() {} 50 | 51 | removeEventListeners() {} 52 | } 53 | -------------------------------------------------------------------------------- /animations/classes/page.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { EventEmitter } from 'events' 3 | import AutoBind from 'auto-bind' 4 | import { each } from 'lodash' 5 | 6 | export class Page extends EventEmitter { 7 | components: any 8 | elements: any 9 | selectors: { element: any; elements: any } 10 | element: any 11 | 12 | constructor({ element, elements }: any) { 13 | super() 14 | AutoBind(this) 15 | 16 | this.selectors = { 17 | element, 18 | elements: { 19 | // animationsParagraphs: '[data-animation="paragraphs"]', 20 | preloaders: '[src]', 21 | ...elements, 22 | }, 23 | } 24 | } 25 | 26 | create(): void { 27 | this.element = document.querySelector(this.selectors.element) 28 | this.elements = {} 29 | 30 | each(this.selectors.elements, (selector, key) => { 31 | if (selector instanceof window.HTMLElement || selector instanceof window.NodeList) { 32 | this.elements[key] = selector 33 | } else if (Array.isArray(selector)) { 34 | this.elements[key] = selector 35 | } else { 36 | this.elements[key] = this.element.querySelectorAll(selector) 37 | 38 | if (this.elements[key].length === 0) { 39 | this.elements[key] = null 40 | } else if (this.elements[key].length === 1) { 41 | this.elements[key] = this.element.querySelector(selector) 42 | } 43 | } 44 | }) 45 | } 46 | 47 | show() {} 48 | 49 | reset() {} 50 | 51 | /** 52 | * Events 53 | */ 54 | onResize() {} 55 | 56 | onWheel(event: any) {} 57 | 58 | onTouchDown(event: any) {} 59 | 60 | onTouchMove(event: any) {} 61 | 62 | onTouchUp(event: any) {} 63 | 64 | update() {} 65 | } 66 | -------------------------------------------------------------------------------- /animations/components/preloader.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '../classes/component' 2 | import GSAP from 'gsap' 3 | import { each } from 'lodash' 4 | 5 | export class Preloader extends Component { 6 | images: any[] 7 | length: number 8 | interval!: ReturnType | undefined 9 | constructor() { 10 | super({ 11 | element: '[data-animation="preloader"]', 12 | elements: { 13 | text: '[data-animation="preloader__text"] span', 14 | h1: '[data-animation="preloader__text"]', 15 | }, 16 | }) 17 | 18 | this.images = [...document.querySelectorAll('[data-src]')] 19 | 20 | this.length = 0 21 | this.interval 22 | this.createLoader() 23 | } 24 | 25 | createLoader() { 26 | if (this.images.length > 0) { 27 | this.images.forEach((image) => { 28 | const media = new window.Image() 29 | const src = image.getAttribute('data-src') 30 | media.crossOrigin = 'anonymous' 31 | media.src = src 32 | 33 | media.onload = (_) => { 34 | image.setAttribute('src', src) 35 | this.onAssetLoaded() 36 | } 37 | }) 38 | } else this.onLoaded() 39 | } 40 | 41 | onAssetLoaded(image?: any) { 42 | this.length += 1 43 | 44 | const percent = this.length / this.images.length 45 | // this.elements.number.innerHTML = `${Math.round(percent * 100)}%` 46 | // this.elements.path.style.strokeDasharray = `${Math.round(percent * 100)} 100` 47 | 48 | if (percent === 1) { 49 | this.onLoaded() 50 | } 51 | } 52 | 53 | create() { 54 | super.create() 55 | } 56 | 57 | onLoaded() { 58 | this.interval = setInterval(() => { 59 | this.length < 99 ? (this.length += 1) : (this.length = 100) 60 | // this.elements.number.innerHTML = `${this.length}%` 61 | }, 27) 62 | 63 | // GSAP.timeline() 64 | // .to(this.element, { autoAlpha: 0 }) 65 | // .call(() => { 66 | // this.emit('completed') 67 | // }) 68 | GSAP.timeline() 69 | .to(this.elements.text, { 70 | y: '0', 71 | duration: 1.5, 72 | ease: 'expo.inOut', 73 | }) 74 | .to(this.elements.text, { 75 | width: 'auto', 76 | delay: 0.5, 77 | }) 78 | .to( 79 | this.elements.text, 80 | { 81 | delay: 1, 82 | width: '0', 83 | overflow: 'hidden', 84 | }, 85 | 'text' 86 | ) 87 | .to( 88 | [this.elements.text[0], this.elements.text[4]], 89 | { 90 | delay: 1, 91 | width: 'auto', 92 | }, 93 | 'text' 94 | ) 95 | .to(this.elements.h1, { 96 | backgroundColor: 'white', 97 | }) 98 | .to([this.element], { 99 | delay: 0.5, 100 | autoAlpha: 0, 101 | }) 102 | .call(() => { 103 | this.emit('completed') 104 | }) 105 | } 106 | 107 | destroy() { 108 | // this.element.parentNode.removeChild(this.element) 109 | setTimeout(() => { 110 | clearInterval(this.interval) 111 | }, 4000) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /animations/elements/Paragraphs.ts: -------------------------------------------------------------------------------- 1 | import each from 'lodash/each' 2 | import gsap from 'gsap' 3 | import Splitting from 'splitting' 4 | 5 | import { Animation } from '../classes/animation' 6 | 7 | export default class Titles extends Animation { 8 | elementLinesSpan 9 | constructor({ element, elements }: { element: string; elements: {} }) { 10 | super({ 11 | element, 12 | elements, 13 | }) 14 | 15 | this.splitText() 16 | this.elementLinesSpan = this.element.querySelectorAll('span') 17 | } 18 | 19 | splitText() { 20 | Splitting({ target: this.element, by: 'lines' }) 21 | } 22 | 23 | animateIn() { 24 | each(this.elementLinesSpan, (line) => { 25 | const lineIndex: number = Number(line.style.getPropertyValue('--line-index')) 26 | gsap 27 | .timeline({ delay: 0.5 }) 28 | .set(this.element, { 29 | autoAlpha: 1, 30 | }) 31 | .set(line, { 32 | y: '100%', 33 | autoAlpha: 0, 34 | }) 35 | .to(line, { 36 | y: 0, 37 | autoAlpha: 1, 38 | delay: 0.15 * lineIndex, 39 | duration: 0.75, 40 | ease: 'back.out(1)', 41 | }) 42 | }) 43 | } 44 | 45 | animateOut() { 46 | gsap.set(this.element, { 47 | autoAlpha: 0, 48 | }) 49 | } 50 | 51 | onResize() { 52 | this.splitText() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /animations/index.ts: -------------------------------------------------------------------------------- 1 | type Props = { 2 | page: string 3 | } 4 | import Lenis from '@studio-freight/lenis' 5 | import { each, map } from 'lodash' 6 | 7 | import { Home } from './pages/home' 8 | import { Preloader } from './components/preloader' 9 | 10 | import gsap from 'gsap' 11 | import ScrollTrigger from 'gsap/ScrollTrigger' 12 | gsap.registerPlugin(ScrollTrigger) 13 | ScrollTrigger.normalizeScroll(true) 14 | export class App { 15 | currentpage: any 16 | preloader: any 17 | pages: any 18 | pagetitle: string 19 | frame: any 20 | 21 | animations: any[] 22 | animationsParagraphs: any 23 | animationsParallax: any 24 | 25 | lenis: any 26 | 27 | // constructor receiving props from react 28 | constructor({ page }: Props) { 29 | this.pagetitle = page 30 | this.animations = [] 31 | this.createPages() 32 | this.onResize() 33 | this.update() 34 | this.reset() 35 | this.initLenis() 36 | } 37 | 38 | createPreloader() { 39 | if (!sessionStorage.getItem('preloader')) { 40 | this.preloader = new Preloader() 41 | this.preloader.once('completed', this.onPreloaded.bind(this)) 42 | } 43 | } 44 | 45 | async reset() { 46 | if (sessionStorage.getItem('preloader')) { 47 | await this.currentpage.reset() 48 | setTimeout(() => { 49 | this.currentpage.show() 50 | }, 1000) 51 | } else { 52 | this.createPreloader() 53 | } 54 | } 55 | 56 | initLenis() { 57 | this.lenis = new Lenis({ 58 | duration: 1.25, 59 | easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), 60 | direction: 'vertical', 61 | gestureDirection: 'vertical', 62 | smooth: true, 63 | mouseMultiplier: 0.8, 64 | smoothTouch: false, 65 | infinite: false, 66 | }) 67 | 68 | this.lenis.on('scroll', ({ scroll }) => { 69 | ScrollTrigger.update() 70 | each(this.animations, (animation) => { 71 | animation.update && animation.update({ current: scroll }) 72 | }) 73 | }) 74 | 75 | const raf = (time: any) => { 76 | this.lenis.raf(time) 77 | requestAnimationFrame(raf) 78 | } 79 | 80 | requestAnimationFrame(raf) 81 | } 82 | 83 | createComponents(): void { 84 | this.createAnimations() 85 | } 86 | 87 | createAnimations(): void {} 88 | 89 | createPages() { 90 | this.pages = { 91 | home: new Home(), 92 | } 93 | 94 | this.currentpage = this.pages[this.pagetitle] 95 | this.currentpage.create() 96 | this.currentpage.reset() 97 | this.createComponents() 98 | this.addEventListeners() 99 | } 100 | 101 | onPreloaded() { 102 | this.preloader.destroy() 103 | this.currentpage.show() 104 | } 105 | 106 | /** 107 | * Loop 108 | **/ 109 | update() { 110 | if (this.currentpage && this.currentpage.update) { 111 | this.currentpage.update() 112 | } 113 | 114 | this.frame = window.requestAnimationFrame(this.update.bind(this)) 115 | } 116 | 117 | onResize() { 118 | if (this.currentpage && this.currentpage.onResize) { 119 | this.currentpage.onResize() 120 | } 121 | } 122 | 123 | addEventListeners() {} 124 | } 125 | 126 | console.log( 127 | '%c 💛 & 💡 Josiah — https://jobenetuk.dev/', 128 | 'background: #000; color: #ff873c;' 129 | ) 130 | -------------------------------------------------------------------------------- /animations/pages/home.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '../classes/page' 2 | 3 | export class Home extends Page { 4 | constructor() { 5 | super({ 6 | element: '[data-animation="home"]', 7 | elements: {}, 8 | }) 9 | } 10 | 11 | resetElements(): void {} 12 | 13 | create() { 14 | super.create() 15 | // this.resetElements() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' 2 | export { useFoucFix } from './useFoucFix' 3 | -------------------------------------------------------------------------------- /hooks/useFoucFix.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | // Temporary fix to avoid flash of unstyled content (FOUC) during route transitions. 4 | // Keep an eye on this issue and remove this code when resolved: https://github.com/vercel/next.js/issues/17464 5 | export const useFoucFix = () => 6 | useEffect(() => { 7 | // Gather all server-side rendered stylesheet entries. 8 | let stylesheets = Array.from( 9 | document.querySelectorAll('link[rel="stylesheet"][data-n-p]') 10 | ).map((element) => ({ 11 | element, 12 | href: element.getAttribute('href'), 13 | })) 14 | 15 | // Remove the `data-n-p` attribute to prevent Next.js from removing it early. 16 | stylesheets.forEach(({ element }) => element.removeAttribute('data-n-p')) 17 | 18 | const hrefs: (string | null)[] = [] 19 | 20 | const mutationHandler = (mutations: any[]) => { 21 | // Gather all