├── .gitignore ├── .npmrc ├── .stackblitzrc ├── README.md ├── astro.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public ├── assets │ ├── avatar-1.jpeg │ ├── avatar-2.jpeg │ ├── avatar-3.jpeg │ ├── avatar-4.jpeg │ ├── avatar-5.jpeg │ └── logo.svg └── favicon.ico ├── sandbox.config.json ├── src ├── components │ ├── Button.astro │ ├── CTA.astro │ ├── Clients.astro │ ├── Hero.astro │ ├── Navbar.astro │ ├── Pricing.astro │ ├── Reviews.astro │ ├── Services.astro │ └── Tour.astro ├── data │ ├── pricing.js │ ├── reviews.json │ └── services.json ├── designFile │ └── Landing Page.fig ├── icons │ ├── logo.svg │ └── programmer.svg ├── js │ └── reviews.js ├── layouts │ └── BaseLayout.astro ├── pages │ └── index.astro └── styles │ ├── global.css │ └── home.css └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # logs 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # environment variables 13 | .env 14 | .env.production 15 | 16 | # macOS-specific files 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ## force pnpm to hoist 2 | shamefully-hoist = true -------------------------------------------------------------------------------- /.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "startCommand": "npm start", 3 | "env": { 4 | "ENABLE_CJS_IMPORTS": true 5 | } 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Desgn Landing Page (Figma-to-Code) 2 | During this video series, we’re going to do a full design-to-code landing page. 3 | 4 | We’ll start by hand-designing everything in Figma and then code out the page using a new static site generator called Astro. To style the site, we’ll write modern CSS and tack on a few superpowers using Open Props, a brand new framework made entirely from css variables. And for a bit of extra fun, we’ll hook our styling up with Post CSS and Open Props’ just-in-time engine to keep our compiled stylesheet lightweight and speedy, only including props we use and our manually-typed CSS. 5 | 6 | 🔗 Key Links 🔗 7 | - Live code: https://codinginpublic.dev/projects/desgn-landing-page/ 8 | - YouTube series: https://www.youtube.com/watch?v=jyjScZWgzIg&list=PL4cUxeGkcC9hZm9NYpd4G-jhoeEk0ls-- 9 | 10 | --------------------------------------- 11 | 12 | 🔗 Additional Links 🔗 13 | - [Figma](https://figma.com) 14 | - [NodeJS](https://nodejs.org/en/) 15 | - [Astro Docs](https://docs.astro.build/en/getting-started/) 16 | - [Astro Icon](https://www.npmjs.com/package/astro-icon) 17 | - [PostCSS Docs](https://github.com/postcss/postcss#usage) 18 | - [PostCSS autoprefixer Plugin](https://github.com/postcss/autoprefixer) 19 | - [Open Props](https://open-props.style/) 20 | - [PostCSS Plugin for JIT Open Props](https://github.com/GoogleChromeLabs/postcss-jit-props) 21 | 22 | --------------------------------------- 23 | 24 | 🌐 Connect With Me 🌐 25 | - Website: https://www.codinginpublic.dev 26 | - Blog: https://www.chrispennington.blog 27 | - Twitter: https://twitter.com/cpenned 28 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | // projectRoot: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project. 3 | // pages: './src/pages', // Path to Astro components, pages, and data 4 | // dist: './dist', // When running `astro build`, path to final static output 5 | // public: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing. 6 | buildOptions: { 7 | // site: 'http://example.com', // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. 8 | sitemap: true, // Generate sitemap (set to "false" to disable) 9 | }, 10 | devOptions: { 11 | // hostname: 'localhost', // The hostname to run the dev server on. 12 | // port: 3000, // The port to run the dev server on. 13 | }, 14 | renderers: [], 15 | vite: { 16 | ssr: { 17 | external: ["svgo"], 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/starter", 3 | "version": "0.0.1", 4 | "private": true, 5 | "browserslist": [ 6 | "defaults" 7 | ], 8 | "scripts": { 9 | "dev": "astro dev", 10 | "start": "astro dev", 11 | "build": "astro build", 12 | "preview": "astro preview" 13 | }, 14 | "devDependencies": { 15 | "astro": "^0.23.0", 16 | "autoprefixer": "^10.4.2", 17 | "open-props": "^1.3.8", 18 | "postcss": "^8.4.6", 19 | "postcss-jit-props": "^1.0.4" 20 | }, 21 | "dependencies": { 22 | "astro-icon": "^0.6.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const postcssjitprops = require("postcss-jit-props"); 2 | const OpenProps = require("open-props"); 3 | 4 | 5 | module.exports = { 6 | plugins: [ 7 | postcssjitprops(OpenProps), 8 | require('autoprefixer') 9 | ] 10 | }; -------------------------------------------------------------------------------- /public/assets/avatar-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-in-public/desgn-landing-page/31789e02b0e655797a210a1c9033f343ab7abd86/public/assets/avatar-1.jpeg -------------------------------------------------------------------------------- /public/assets/avatar-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-in-public/desgn-landing-page/31789e02b0e655797a210a1c9033f343ab7abd86/public/assets/avatar-2.jpeg -------------------------------------------------------------------------------- /public/assets/avatar-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-in-public/desgn-landing-page/31789e02b0e655797a210a1c9033f343ab7abd86/public/assets/avatar-3.jpeg -------------------------------------------------------------------------------- /public/assets/avatar-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-in-public/desgn-landing-page/31789e02b0e655797a210a1c9033f343ab7abd86/public/assets/avatar-4.jpeg -------------------------------------------------------------------------------- /public/assets/avatar-5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-in-public/desgn-landing-page/31789e02b0e655797a210a1c9033f343ab7abd86/public/assets/avatar-5.jpeg -------------------------------------------------------------------------------- /public/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | https://rawcdn.githack.com/withastro/astro/main/examples/starter/public/favicon.ico -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "infiniteLoopProtection": true, 3 | "hardReloadOnChange": false, 4 | "view": "browser", 5 | "template": "node", 6 | "container": { 7 | "port": 3000, 8 | "startScript": "start", 9 | "node": "14" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Button.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { text, link, style } = Astro.props; 3 | --- 4 | 5 | {text} -------------------------------------------------------------------------------- /src/components/CTA.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Button from './Button.astro'; 3 | --- 4 |
5 |
6 |

Let Us Tell Your Story

7 |

Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde, fugit a. Nulla numquam beatae, amet doloremque quod sunt molestias accusamus!

8 |
9 |
10 |
13 |
-------------------------------------------------------------------------------- /src/components/Clients.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import {Icon} from 'astro-icon'; 3 | const data = [ 4 | { 5 | "icon" :"simple-icons:amd", 6 | "alt" :"AMD Logo", 7 | }, 8 | { 9 | "icon" :"simple-icons:500px", 10 | "alt" :"500 Logo", 11 | }, 12 | { 13 | "icon" :"simple-icons:abbvie", 14 | "alt" :"Abbvie Logo", 15 | }, 16 | { 17 | "icon" :"simple-icons:bbc", 18 | "alt" :"BBC Logo", 19 | }, 20 | { 21 | "icon" :"simple-icons:astonmartin", 22 | "alt" :"Aston Martin Logo", 23 | }, 24 | { 25 | "icon" :"simple-icons:asda", 26 | "alt" :"Asada Logo", 27 | }, 28 | ] 29 | --- 30 | 31 |
32 |

Happy Clients

33 |
34 | { 35 | data.map(i => ( 36 |
40 |
-------------------------------------------------------------------------------- /src/components/Hero.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Button from './Button.astro'; 3 | --- 4 |
5 |
6 |

Your Kind of Designer

7 |

Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde, fugit a. Nulla numquam beatae, amet doloremque quod sunt molestias accusamus! Assumenda aspernatur fuga expedita accusamus, corporis in optio porro ratione libero. Voluptatem in deleniti id!

8 |
9 |
10 |
13 |
-------------------------------------------------------------------------------- /src/components/Navbar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon' 3 | const navLinks = [ 4 | { 5 | name: 'Home', 6 | url: '/', 7 | style: 'transparent', 8 | }, 9 | { 10 | name: 'Pricing', 11 | url: '#', 12 | style: 'transparent', 13 | }, 14 | { 15 | name: 'About', 16 | url: '#', 17 | style: 'transparent', 18 | }, 19 | { 20 | name: 'Contact Me', 21 | url: '#', 22 | style: 'primary', 23 | }, 24 | ]; 25 | 26 | --- 27 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/components/Pricing.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import {Icon} from 'astro-icon'; 3 | import Button from './Button.astro' 4 | import data from '../data/pricing.js'; 5 | --- 6 | 7 |
8 |
9 |

A Price for Everyone

10 |

Three tiers to meet your needs.

11 |
12 |
13 | { 14 | data.map(card => ( 15 |
16 |
17 |

{card.price}

18 |

{card.plan}

19 |
20 |

{card.description}

21 |
    22 | { 23 | card.details.map(feature => ( 24 |
  • 25 | 26 | {feature} 27 |
  • 28 | ) 29 | ) 30 | } 31 |
32 |
35 | )) 36 | } 37 |
38 |
-------------------------------------------------------------------------------- /src/components/Reviews.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import {Icon} from 'astro-icon'; 3 | import data from '../data/reviews.json'; 4 | --- 5 | 6 |
7 |

Reviews

8 |
9 | 12 |
13 | { 14 | data.slice(data.length - 1).map((review) => ( 15 |
16 | {review.name} 17 |

{review.content}

18 |

{review.name}

19 |
20 | )) 21 | } 22 | { 23 | data.map((review) => ( 24 |
25 | {review.name} 26 |

{review.content}

27 |

{review.name}

28 |
29 | )) 30 | } 31 | { 32 | data.slice(0,1).map((review) => ( 33 |
34 | {review.name} 35 |

{review.content}

36 |

{review.name}

37 |
38 | )) 39 | } 40 |
41 |
42 | { 43 | data.map((review) => ( 44 | 45 | )) 46 | } 47 |
48 | 51 |
52 |
53 | 54 | -------------------------------------------------------------------------------- /src/components/Services.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import {Icon} from 'astro-icon'; 3 | import data from '../data/services.json'; 4 | --- 5 | 6 |
7 |

Services

8 | { 9 | data.map(service => ( 10 |
11 |
12 | 13 |
14 |

{service.name}

15 |

{service.content}

16 |
17 | )) 18 | } 19 |
-------------------------------------------------------------------------------- /src/components/Tour.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Markdown } from 'astro/components'; 3 | --- 4 | 5 |
6 | 9 | 10 |
11 | 12 | ## 🚀 Project Structure 13 | 14 | Inside of your Astro project, you'll see the following folders and files: 15 | 16 | ``` 17 | / 18 | ├── public/ 19 | │ └── favicon.ico 20 | ├── src/ 21 | │ ├── components/ 22 | │ │ └── Tour.astro 23 | │ └── pages/ 24 | │ └── index.astro 25 | └── package.json 26 | ``` 27 | 28 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. 29 | Each page is exposed as a route based on its file name. 30 | 31 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 32 | 33 | Any static assets, like images, can be placed in the `public/` directory. 34 | 35 |
36 | 37 |
38 |

👀 Want to learn more?

39 |

Feel free to check our documentation or jump into our Discord server.

40 |
41 |
42 | 43 | 85 | -------------------------------------------------------------------------------- /src/data/pricing.js: -------------------------------------------------------------------------------- 1 | const pricingData = [ 2 | { 3 | price: "$1000", 4 | plan: "Starter Plan", 5 | description: "Landing page to maximize conversion rate", 6 | details: [ 7 | "Custom Branding", 8 | "Single Landing page", 9 | "SEO-optimized", 10 | "One Email inbox", 11 | "Contact Form", 12 | ], 13 | button: { 14 | text: "Learn more", 15 | url: "#", 16 | style: "muted", 17 | }, 18 | subtext: "$500 up front + $500 upon completion", 19 | }, 20 | { 21 | price: "$1400", 22 | plan: "Pro Plan", 23 | description: "Personalized site to showcase your brand", 24 | details: [ 25 | "Starter Plan+", 26 | "Up to 5 pages", 27 | "Social feed integrations", 28 | "CMS for custom content", 29 | "Custom analytics", 30 | ], 31 | button: { 32 | text: "Sign up", 33 | url: "#", 34 | style: "secondary", 35 | }, 36 | subtext: "$700 up front + $700 upon completion", 37 | featured: true, 38 | }, 39 | { 40 | price: "$2000", 41 | plan: "Writer’s Plan", 42 | description: "Personalized site with accompanying blog", 43 | details: [ 44 | "Pro Plan+", 45 | "Full-featured blog", 46 | "Search functionality", 47 | "Two email inboxes", 48 | "Free domain name (1 year)", 49 | ], 50 | button: { 51 | text: "Learn more", 52 | url: "#", 53 | style: "muted", 54 | }, 55 | subtext: "$1000 up front + $1000 upon completion", 56 | }, 57 | ]; 58 | 59 | export default pricingData; 60 | -------------------------------------------------------------------------------- /src/data/reviews.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "John Doe", 4 | "content": "iquam vel nibh amet, vitae at purus dui semper vel facilisis enim", 5 | "avatar": "/assets/avatar-1.jpeg", 6 | "active": true 7 | }, 8 | { 9 | "name": "Christian Jones", 10 | "content": "iquam vel nibh amet, vitae at purus dui semper vel facilisis enim", 11 | "avatar": "/assets/avatar-2.jpeg" 12 | }, 13 | { 14 | "name": "Clarence Robinson", 15 | "content": "iquam vel nibh amet, vitae at purus dui semper vel facilisis enim vel facilisis enim", 16 | "avatar": "/assets/avatar-3.jpeg" 17 | }, 18 | { 19 | "name": "Kathleen Alday", 20 | "content": "iquam amet, vitae at purus dui semper vel facilisis enim", 21 | "avatar": "/assets/avatar-4.jpeg" 22 | }, 23 | { 24 | "name": "Tina Givens", 25 | "content": "iquam vel nibh amet, vitae at purus enim enim dui semper vel facilisis enim", 26 | "avatar": "/assets/avatar-5.jpeg" 27 | } 28 | ] -------------------------------------------------------------------------------- /src/data/services.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "icon": "feather:code", 4 | "name": "Custom Coding", 5 | "content" : "Vel eget eu vestibulum mauris et ut hendrerit aliquam vel nibh amet, vitae at purus dui semper vel facilisis enim" 6 | }, 7 | { 8 | "icon": "feather:users", 9 | "name": "User Friendly", 10 | "content" : "Vel eget eu vestibulum mauris et ut hendrerit aliquam iquam vel nibh amet, vitae at purus dui semper vel facilisis enim vel nibh amet, vitae at purus dui semper vel facilisis enim" 11 | }, 12 | { 13 | "icon": "feather:search", 14 | "name": "SEO Optimized", 15 | "content" : "Vel eget eu vestibulum mauris et ut hendrerit aliquam vel nibh amet, vitae at purus dui semper vel facilisis enim" 16 | }, 17 | { 18 | "icon": "feather:trending-up", 19 | "name": "Built for Growth", 20 | "content" : "Vel eget eu vestibulum mauris et ut hendrerit aliquam vel nibh amet, vitae at vitae a vitae a purus dui semper vel facilisis enim" 21 | }, 22 | { 23 | "icon": "feather:smartphone", 24 | "name": "Ready for Mobile", 25 | "content" : "Vel eget eu vestibulum mauris et ut hendrerit aliquam vel nibh amet, vitae at purus dui semper vel facilisis enim" 26 | }, 27 | { 28 | "icon": "feather:home", 29 | "name": "Feels like you", 30 | "content" : "Vel eget eu vestibulum mauris et ut hendrerit aliquam vel nibh amet, vitae at purus dui semper vel facilisis enim" 31 | } 32 | ] -------------------------------------------------------------------------------- /src/designFile/Landing Page.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding-in-public/desgn-landing-page/31789e02b0e655797a210a1c9033f343ab7abd86/src/designFile/Landing Page.fig -------------------------------------------------------------------------------- /src/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/icons/programmer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/js/reviews.js: -------------------------------------------------------------------------------- 1 | const reviewsSlider = document.querySelector('.reviews'); 2 | const reviewBtns = document.querySelectorAll('.review-btn'); 3 | const reviews = [...document.querySelectorAll('.review')]; 4 | const indicators = [...document.querySelectorAll('.indicator')]; 5 | let isMoving; 6 | let currentIndex = 1; 7 | 8 | function showActiveIndicator(){ 9 | indicators.forEach(ind => ind.classList.remove('active')); 10 | let activeIndicator; 11 | if(currentIndex === 0 || currentIndex === reviews.length - 2){ 12 | activeIndicator = indicators.length - 1; 13 | } else if (currentIndex === reviews.length - 1 || currentIndex === 1){ 14 | activeIndicator = 0; 15 | } else { 16 | activeIndicator = currentIndex - 1; 17 | } 18 | indicators[activeIndicator].classList.add('active'); 19 | } 20 | 21 | function moveSlider(){ 22 | reviewsSlider.style.transform = `translateX(-${currentIndex * 100}%)`; 23 | showActiveIndicator(); 24 | }; 25 | 26 | function handleReviewBtnClick(e){ 27 | if(isMoving){ return }; 28 | isMoving = true; 29 | e.currentTarget.id === 'next' 30 | ? currentIndex++ 31 | : currentIndex--; 32 | moveSlider(); 33 | } 34 | 35 | function handleIndicatorClick(e){ 36 | if(isMoving){ return }; 37 | isMoving = true; 38 | currentIndex = indicators.indexOf(e.target) + 1; 39 | moveSlider(); 40 | } 41 | 42 | // Event Listeners 43 | reviewBtns.forEach(btn => { 44 | btn.addEventListener('click', handleReviewBtnClick); 45 | }) 46 | 47 | indicators.forEach(ind => { 48 | ind.addEventListener('click', handleIndicatorClick); 49 | }) 50 | 51 | reviewsSlider.addEventListener('transitionend', () => { 52 | isMoving = false; 53 | if(currentIndex === 0){ 54 | currentIndex = reviews.length - 2; 55 | reviewsSlider.style.transitionDuration = '1ms'; 56 | return moveSlider(); 57 | } 58 | if(currentIndex === reviews.length - 1){ 59 | currentIndex = 1; 60 | reviewsSlider.style.transitionDuration = '1ms'; 61 | return moveSlider(); 62 | } 63 | reviewsSlider.style.transitionDuration = '300ms'; 64 | }) 65 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Navbar from '../components/Navbar.astro' 3 | const {title} = Astro.props; 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | {title} 11 | 12 | 16 | 17 | 18 |
19 | 20 |
21 | 22 |
23 | 26 |
27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import {Icon} from 'astro-icon'; 3 | import BaseLayout from '../layouts/BaseLayout.astro'; 4 | import Hero from '../components/Hero.astro'; 5 | import Services from '../components/Services.astro'; 6 | import Reviews from '../components/Reviews.astro'; 7 | import Pricing from '../components/Pricing.astro'; 8 | import Clients from '../components/Clients.astro'; 9 | import CTA from '../components/CTA.astro'; 10 | --- 11 | 12 | 13 | 14 | 15 | 16 |
17 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Inter:400,600"); 2 | 3 | *, 4 | *::before, 5 | *::after { 6 | box-sizing: border-box; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | img, 12 | picture, 13 | svg { 14 | max-width: 100%; 15 | display: block; 16 | } 17 | 18 | :root { 19 | /* Colors */ 20 | --bkg: 222 47.4% 11.2%; 21 | --bkg-alt: 226 58.3% 18.8%; 22 | --text: 226 100% 93.9%; 23 | --text-alt: 226 22.1% 73.3%; 24 | --muted: 230 39% 67.8%; 25 | --white: 255 100% 100%; 26 | --accent1: 241 76.8% 62.7%; 27 | --accent2: 330 85.2% 60.4%; 28 | --accent3: 0 90.6% 70.8%; 29 | --gradient: linear-gradient( 30 | 94.55deg, 31 | hsl(var(--accent2)) -4.6%, 32 | hsl(var(--accent3)) 99.9% 33 | ); 34 | } 35 | 36 | @media (min-width: 768px) { 37 | html { 38 | font-size: 130%; 39 | } 40 | } 41 | 42 | body { 43 | font-family: 'Inter', sans-serif; 44 | font-weight: 400; 45 | line-height: 1.55; 46 | max-width: 2000px; 47 | margin: auto; 48 | background-color: hsl(var(--bkg)); 49 | color: hsl(var(--text)); 50 | } 51 | 52 | /* LAYOUT */ 53 | .wrapper { 54 | overflow: hidden; 55 | display: grid; 56 | grid-template-rows: auto 1fr auto; 57 | min-height: 100vh; 58 | } 59 | 60 | main { 61 | display: grid; 62 | gap: var(--size-fluid-6); 63 | padding: var(--size-fluid-5) var(--size-fluid-2); 64 | } 65 | 66 | .container { 67 | width: 100%; 68 | max-width: 1500px; 69 | margin: auto; 70 | } 71 | 72 | .container-sm { 73 | width: 100%; 74 | max-width: 1200px; 75 | margin: auto; 76 | } 77 | 78 | .container-xs { 79 | width: 100%; 80 | max-width: 900px; 81 | margin: auto; 82 | } 83 | 84 | /* UTILS */ 85 | 86 | .text-bkg { 87 | color: hsl(var(--bkg)); 88 | } 89 | .text-bkg-alt { 90 | color: hsl(var(--bkg-alt)); 91 | } 92 | .text-text { 93 | color: hsl(var(--text)); 94 | } 95 | .text-text-alt { 96 | color: hsl(var(--text-alt)); 97 | } 98 | .text-muted { 99 | color: hsl(var(--muted)); 100 | } 101 | .text-white { 102 | color: hsl(var(--white)); 103 | } 104 | .text-accent1 { 105 | color: hsl(var(--accent1)); 106 | } 107 | .text-accent2 { 108 | color: hsl(var(--accent2)); 109 | } 110 | .text-accent3 { 111 | color: hsl(var(--accent3)); 112 | } 113 | .text-gradient { 114 | color: transparent; 115 | background: var(--gradient); 116 | background-clip: text; 117 | } 118 | 119 | .h1 { 120 | font-size: var(--font-size-fluid-3); 121 | font-weight: 600; 122 | line-height: 1.1; 123 | } 124 | .h2 { 125 | font-size: var(--font-size-fluid-2); 126 | font-weight: 600; 127 | line-height: 1.1; 128 | } 129 | .h3 { 130 | font-size: var(--font-size-fluid-1); 131 | font-weight: 600; 132 | line-height: 1.1; 133 | } 134 | 135 | small { 136 | font-size: var(--font-size-00); 137 | } 138 | 139 | .grid-sm { 140 | display: grid; 141 | place-items: center; 142 | gap: var(--size-fluid-1); 143 | } 144 | 145 | .grid-md { 146 | display: grid; 147 | place-items: center; 148 | gap: var(--size-fluid-3); 149 | } 150 | 151 | .grid-lg { 152 | display: grid; 153 | place-items: center; 154 | align-content: center; 155 | gap: var(--size-fluid-3); 156 | } 157 | 158 | .sr-only { 159 | position: absolute; 160 | width: 1px; 161 | height: 1px; 162 | margin: -1px; 163 | overflow: hidden; 164 | clip: rect(0, 0, 0, 0); 165 | white-space: nowrap; 166 | border-width: 0; 167 | } 168 | 169 | .narrow { 170 | max-width: 80ch; 171 | } 172 | 173 | /* COMPONENTS */ 174 | .btn { 175 | color: hsl(var(--white)); 176 | text-decoration: none; 177 | padding: var(--size-2) var(--size-fluid-4); 178 | border-radius: var(--radius-1); 179 | cursor: pointer; 180 | } 181 | 182 | .btn--primary { 183 | background-color: hsl(var(--accent1)); 184 | } 185 | 186 | .btn--secondary { 187 | background: var(--gradient); 188 | } 189 | 190 | .btn--muted { 191 | background-color: hsl(var(--muted)); 192 | } 193 | 194 | .btn--menu { 195 | background-color: transparent; 196 | border: none; 197 | display: grid; 198 | place-items: center; 199 | padding-inline: var(--size-2); 200 | } 201 | 202 | .btn--menu[aria-expanded="true"] + .nav-links { 203 | transform: translateY(0); 204 | } 205 | 206 | .blur { 207 | position: relative; 208 | } 209 | 210 | .blur::after, 211 | .blur::before { 212 | content: ''; 213 | position: absolute; 214 | inset: 0; 215 | z-index: -1; 216 | filter: blur(35px); 217 | border-radius: 50% 50% 50%; 218 | } 219 | 220 | .blur::after { 221 | background-color: hsl(var(--accent1) / .2); 222 | transform: rotate(-20deg); 223 | } 224 | 225 | .blur::before { 226 | background-color: hsl(var(--accent2) / .2); 227 | transform: rotate(20deg); 228 | } 229 | 230 | @media screen (min-width: 768px) { 231 | .blur::after, 232 | .blur::before { 233 | filter: blur(120px); 234 | } 235 | } 236 | 237 | /* NAV */ 238 | .nav-container { 239 | display: flex; 240 | justify-content: space-between; 241 | align-items: center; 242 | padding: var(--size-fluid-2); 243 | } 244 | 245 | .nav-links, 246 | .nav-wrapper { 247 | display: flex; 248 | align-items: center; 249 | gap: var(--size-3); 250 | } 251 | 252 | .nav-links { 253 | flex-direction: column; 254 | transform: translateY(-200%); 255 | position: absolute; 256 | background-color: hsl(var(--bkg)); 257 | top: var(--size-fluid-5); 258 | left: 0; 259 | right: 0; 260 | text-align: center; 261 | padding: var(--size-3); 262 | border-bottom: 2px solid hsl(var(--muted)); 263 | } 264 | 265 | li[role="none"], 266 | .nav-link { 267 | width: 100%; 268 | display: block; 269 | font-size: var(--font-size-1); 270 | } 271 | 272 | .logo { 273 | width: calc(var(--size-fluid-8) * 0.75); 274 | } 275 | 276 | @media (min-width: 900px) { 277 | .nav-wrapper { 278 | gap: var(--size-4); 279 | } 280 | .btn--menu { 281 | display: none; 282 | } 283 | .nav-links { 284 | flex-direction: row; 285 | position: static; 286 | transform: translateY(0); 287 | border: none; 288 | padding: 0; 289 | inset: initial; 290 | background-color: transparent; 291 | } 292 | li[role="none"], 293 | .nav-link { 294 | width: initial; 295 | font-size: var(--font-size-0); 296 | } 297 | } 298 | 299 | /* HERO */ 300 | header { 301 | text-align: center; 302 | } 303 | 304 | 305 | /* PROGRAMMER SECTION */ 306 | .programmer::after, 307 | .programmer::before { 308 | opacity: 0.8; 309 | } 310 | 311 | .programmer-icon { 312 | width: var(--size-fluid-9); 313 | filter: drop-shadow(10px 10px 25px hsl(var(--accent2) / 0.2)); 314 | } 315 | 316 | /* SERVICES */ 317 | .services { 318 | display: flex; 319 | flex-wrap: wrap; 320 | align-items: start; 321 | gap: var(--size-fluid-3); 322 | } 323 | 324 | .service { 325 | flex: 1 1 300px; 326 | display: grid; 327 | gap: var(--size-2); 328 | } 329 | 330 | .service--icon { 331 | background-color: hsl(var(--muted)); 332 | justify-self: start; 333 | padding: clamp(0.6rem, 3vw, .8rem); 334 | border-radius: 50%; 335 | } 336 | 337 | .service--icon svg { 338 | width: var(--size-fluid-2); 339 | } 340 | 341 | /* REVIEWS */ 342 | .reviews-wrapper { 343 | background-color: hsl(var(--bkg)); 344 | margin: 0 calc(var(--size-fluid-2) * -1); 345 | } 346 | 347 | .reviews-wrapper::after, 348 | .reviews-wrapper::before { 349 | inset-inline: 20%; 350 | } 351 | 352 | .reviews-container { 353 | overflow: hidden; 354 | position: relative; 355 | width: calc(100vw - var(--size-fluid-2)); 356 | background-color: hsl(var(--bkg)); 357 | } 358 | 359 | .reviews { 360 | display: flex; 361 | margin: var(--size-fluid-4) 0 var(--size-fluid-5); 362 | text-align: center; 363 | transform: translateX(-100%); 364 | transition: transform 300ms ease-in-out; 365 | } 366 | 367 | .review { 368 | flex: 1 0 100%; 369 | } 370 | 371 | .review-avatar { 372 | max-width: var(--size-fluid-5); 373 | border-radius: 50%; 374 | } 375 | 376 | .review-content { 377 | max-width: 80%; 378 | } 379 | 380 | .review-btn { 381 | position: absolute; 382 | z-index: 10; 383 | top: 0; 384 | bottom: 0; 385 | background: none; 386 | border: none; 387 | background-color: hsl(var(--bkg)); 388 | color: hsl(var(--muted)); 389 | padding: var(--size-1); 390 | width: var(--size-fluid-4); 391 | cursor: pointer; 392 | transition: all 300ms var(--ease-squish-2); 393 | } 394 | 395 | .review-btn--prev { 396 | left: 0; 397 | } 398 | 399 | .review-btn--prev:is(:hover, :focus) { 400 | left: calc(var(--size-1) * -1); 401 | color: hsl(var(--text)); 402 | } 403 | .review-btn--next { 404 | right: 0; 405 | } 406 | 407 | .review-btn--next:is(:hover, :focus) { 408 | right: calc(var(--size-1) * -1); 409 | color: hsl(var(--text)); 410 | } 411 | 412 | .indicator-container { 413 | position: absolute; 414 | left: 50%; 415 | bottom: var(--size-5); 416 | display: flex; 417 | justify-content: center; 418 | gap: var(--size-3); 419 | transform: translateX(-50%); 420 | } 421 | 422 | .indicator { 423 | background: transparent; 424 | border: 0.15em solid hsl(var(--text-alt)); 425 | border-radius: 50%; 426 | padding: 0.3rem; 427 | height: var(--size-fluid-1); 428 | cursor: pointer; 429 | } 430 | 431 | .indicator.active { 432 | background: hsl(var(--text-alt)); 433 | } 434 | 435 | /* PRICING */ 436 | .pricing-wrapper { 437 | display: grid; 438 | gap: var(--size-fluid-5); 439 | } 440 | 441 | .pricing-container { 442 | display: flex; 443 | flex-wrap: wrap; 444 | gap: var(--size-3); 445 | align-items: center; 446 | justify-content: center; 447 | } 448 | 449 | .pricing-container::before, 450 | .pricing-container::after { 451 | inset: 15%; 452 | } 453 | 454 | .pricing-card { 455 | padding: var(--size-3) var(--size-5); 456 | border: 1px solid hsl(var(--text-alt)); 457 | background-color: hsl(var(--bkg)); 458 | } 459 | 460 | .pricing-card.featured { 461 | border-color: hsl(var(--accent2)); 462 | position: relative; 463 | } 464 | 465 | .pricing-card.featured::before { 466 | content: "Most Popular"; 467 | position: absolute; 468 | top: calc(var(--size-fluid-1) * -.15); 469 | transform: translateY(-50%); 470 | background: var(--gradient); 471 | font-size: var(--font-size-00); 472 | text-transform: uppercase; 473 | text-align: center; 474 | border-radius: var(--size-2); 475 | padding: 0 var(--size-2); 476 | } 477 | 478 | @media screen and (min-width: 1075px) { 479 | .pricing-card.featured { 480 | transform: scale(1.15); 481 | border: 4px solid hsl(var(--accent2)); 482 | padding: var(--size-5) var(--size-6) var(--size-3); 483 | margin: 2rem 0; 484 | } 485 | } 486 | 487 | .pricing-card-price { 488 | font-size: var(--size-fluid-2); 489 | font-weight: bold; 490 | text-align: center; 491 | } 492 | 493 | .pricing-card-pill { 494 | background-color: hsl(var(--text-alt)); 495 | color: hsl(var(--bkg)); 496 | text-transform: uppercase; 497 | text-align: center; 498 | font-size: var(--font-size-00); 499 | border-radius: var(--size-2); 500 | padding: 0 var(--size-2); 501 | } 502 | 503 | .pricing-card-description { 504 | font-size: var(--font-size-0); 505 | text-align: center; 506 | } 507 | 508 | .pricing-card-feature-container { 509 | list-style: none; 510 | font-size: var(--font-size-0); 511 | display: grid; 512 | gap: var(--size-2); 513 | margin-bottom: var(--size-2); 514 | } 515 | 516 | .pricing-card-feature { 517 | display: flex; 518 | gap: var(--size-2); 519 | } 520 | 521 | /* CLIENTS */ 522 | .clients--heading { 523 | font-weight: normal; 524 | } 525 | 526 | .client-logo-container { 527 | display: flex; 528 | flex-wrap: wrap; 529 | justify-content: center; 530 | gap: 0 var(--size-fluid-3); 531 | } 532 | 533 | .client-logo { 534 | flex: 0 1 var(--size-fluid-5); 535 | } 536 | 537 | /* CTA */ 538 | .cta { 539 | text-align: center; 540 | } 541 | 542 | .cta .narrow { 543 | max-width: var(--size-content-3); 544 | } 545 | 546 | /* FOOTER */ 547 | 548 | footer { 549 | display: grid; 550 | place-items: center; 551 | padding: var(--size-2); 552 | background-color: hsl(var(--bkg)); 553 | color: hsl(var(--text-alt)); 554 | } 555 | 556 | footer::after, 557 | footer::before { 558 | height: 100px; 559 | } 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | -------------------------------------------------------------------------------- /src/styles/home.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-mono: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 3 | 'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace; 4 | --color-light: #f3f4f6; 5 | } 6 | 7 | @media (prefers-color-scheme: dark) { 8 | :root { 9 | --color-light: #1f2937; 10 | } 11 | } 12 | 13 | a { 14 | color: inherit; 15 | } 16 | 17 | header > div { 18 | font-size: clamp(2rem, -0.4742rem + 6.1856vw, 2.75rem); 19 | } 20 | 21 | header > div { 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | } 26 | 27 | header h1 { 28 | font-size: 1em; 29 | font-weight: 500; 30 | } 31 | header img { 32 | width: 2em; 33 | height: 2.667em; 34 | } 35 | 36 | h2 { 37 | font-weight: 500; 38 | font-size: clamp(1.5rem, 1rem + 1.25vw, 2rem); 39 | } 40 | 41 | .counter { 42 | display: grid; 43 | grid-auto-flow: column; 44 | gap: 1em; 45 | font-size: 2rem; 46 | justify-content: center; 47 | padding: 2rem 1rem; 48 | } 49 | 50 | .counter > pre { 51 | text-align: center; 52 | min-width: 3ch; 53 | } 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node" 4 | } 5 | } 6 | --------------------------------------------------------------------------------