├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── ColorModeButton.tsx ├── InlineList.tsx ├── LinkButton.tsx ├── MotionText.tsx ├── SparkleIcon.tsx ├── TextReveal.tsx ├── Ticker.tsx └── sections │ ├── AboutSection.tsx │ ├── HeadingSection.tsx │ ├── IntroSection.tsx │ └── ProjectSection.tsx ├── data.ts ├── hooks.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx └── index.tsx ├── public ├── decentralized-stackoverflow.webp ├── favicon.ico ├── gradient-lg.webp ├── gradient-sm.webp ├── profile.jpg ├── project-1.png ├── solana-ads.jpg └── vercel.svg ├── styles ├── Home.module.css └── globals.css ├── theme ├── breakpoints.tsx ├── colors.tsx ├── components │ ├── button.tsx │ ├── heading.tsx │ ├── hstack.tsx │ ├── index.tsx │ ├── input.tsx │ ├── stack.tsx │ └── text.tsx ├── index.tsx ├── styles.tsx ├── text.tsx └── tokens.tsx ├── tsconfig.json └── utils.ts /.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 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ColorModeButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, useColorMode } from "@chakra-ui/react"; 2 | import { DarkModeSwitch } from "react-toggle-dark-mode"; 3 | 4 | const ColorModeButton = () => { 5 | const colorMode = useColorMode(); 6 | return ( 7 | { 11 | if (checked) { 12 | colorMode.setColorMode("dark"); 13 | } else { 14 | colorMode.setColorMode("light"); 15 | } 16 | }} 17 | size="md" 18 | p="sm" 19 | rounded={"full"} 20 | aria-label="Switch app theme" 21 | /> 22 | ); 23 | }; 24 | export default ColorModeButton; 25 | -------------------------------------------------------------------------------- /components/InlineList.tsx: -------------------------------------------------------------------------------- 1 | import SparkleIcon from "./SparkleIcon"; 2 | 3 | interface InlineListProps { 4 | items: string[]; 5 | } 6 | 7 | const InlineList = ({ items }: InlineListProps) => { 8 | return ( 9 | 10 | {items.map((item, i) => ( 11 | 12 | {item} 13 | 14 |   15 | {i < items.length - 1 && } 16 |   17 | 18 | 19 | ))} 20 | 21 | ); 22 | }; 23 | 24 | export default InlineList; 25 | -------------------------------------------------------------------------------- /components/LinkButton.tsx: -------------------------------------------------------------------------------- 1 | import Link, { LinkProps } from "next/link"; 2 | import { Button, ButtonProps } from "@chakra-ui/react"; 3 | 4 | interface LinkButtonProps extends ButtonProps { 5 | href: LinkProps["href"]; 6 | prefetch?: LinkProps["prefetch"]; 7 | newTab?: boolean; 8 | } 9 | 10 | const LinkButton = ({ 11 | href, 12 | children, 13 | prefetch = true, 14 | newTab = false, 15 | ...otherProps 16 | }: LinkButtonProps) => { 17 | return ( 18 | 19 | 22 | 23 | ); 24 | }; 25 | export default LinkButton; 26 | -------------------------------------------------------------------------------- /components/MotionText.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | HTMLMotionProps, 4 | motion, 5 | Target, 6 | TargetAndTransition, 7 | } from "framer-motion"; 8 | 9 | const DEFAULT_RENDERERS = { 10 | char: ({ children }: any) => { 11 | return ( 12 |
20 | {children} 21 |
22 | ); 23 | }, 24 | word: ({ children }: any) => { 25 | return ( 26 |
33 | {children} 34 |
35 | ); 36 | }, 37 | line: ({ children }: any) => { 38 | return ( 39 |
45 | {children} 46 |
47 | ); 48 | }, 49 | }; 50 | 51 | export type MotionTextVariants = { 52 | char?: { 53 | [key: string]: 54 | | TargetAndTransition 55 | | (( 56 | custom: CharContext, 57 | current: Target, 58 | velocity: Target 59 | ) => TargetAndTransition | string); 60 | }; 61 | word?: { 62 | [key: string]: 63 | | TargetAndTransition 64 | | (( 65 | custom: WordContext, 66 | current: Target, 67 | velocity: Target 68 | ) => TargetAndTransition | string); 69 | }; 70 | line?: { 71 | [key: string]: 72 | | TargetAndTransition 73 | | (( 74 | custom: LineContext, 75 | current: Target, 76 | velocity: Target 77 | ) => TargetAndTransition | string); 78 | }; 79 | }; 80 | 81 | export type MotionTextRenderers = { 82 | char?: any; 83 | word?: any; 84 | line?: any; 85 | }; 86 | 87 | export type MotionTextStyles = { 88 | char?: React.CSSProperties; 89 | word?: React.CSSProperties; 90 | line?: React.CSSProperties; 91 | }; 92 | 93 | export type MotionTextClasses = { 94 | char?: string; 95 | word?: string; 96 | line?: string; 97 | }; 98 | 99 | export interface MotionTextProps 100 | extends Omit, "variants"> { 101 | text: string; 102 | variants?: MotionTextVariants; 103 | renderers?: MotionTextRenderers; 104 | styles?: MotionTextStyles; 105 | classes?: MotionTextClasses; 106 | data?: any; 107 | } 108 | 109 | export type IndexValuePair = { 110 | index: number; 111 | value: string; 112 | }; 113 | 114 | export type LineContext = { 115 | // Current line 116 | line: IndexValuePair; 117 | // First char in line 118 | startChar: IndexValuePair; 119 | // Last char in line 120 | endChar: IndexValuePair; 121 | // First word in line 122 | startWord: IndexValuePair; 123 | // Last word in line 124 | endWord: IndexValuePair; 125 | // Total number of chars 126 | charCount: number; 127 | // Total number of words 128 | wordCount: number; 129 | // Total number of lines 130 | lineCount: number; 131 | // custom data 132 | data: any; 133 | }; 134 | 135 | export type WordContext = { 136 | // Line word belongs to 137 | line: IndexValuePair; 138 | // Current word 139 | word: IndexValuePair; 140 | // First char in word 141 | startChar: IndexValuePair; 142 | // Last char in word 143 | endChar: IndexValuePair; 144 | // Total number of chars 145 | charCount: number; 146 | // Total number of words 147 | wordCount: number; 148 | // Total number of lines 149 | lineCount: number; 150 | // custom data 151 | data: any; 152 | }; 153 | 154 | export type CharContext = { 155 | // Line char belongs to 156 | line: IndexValuePair; 157 | // Word char belongs to 158 | word: IndexValuePair; 159 | // Current char 160 | char: IndexValuePair; 161 | // Total number of chars 162 | charCount: number; 163 | // Total number of words 164 | wordCount: number; 165 | // Total number of lines 166 | lineCount: number; 167 | // custom data 168 | data: any; 169 | }; 170 | 171 | const WHITESPACE_REGEX = /\S/; 172 | 173 | const getContexts = (text: string, data: any) => { 174 | let lineCounter = 0; 175 | let wordCounter = 0; 176 | let charCounter = 0; 177 | 178 | const lineContexts: Record = {}; 179 | const wordContexts: Record = {}; 180 | const charContexts: Record = {}; 181 | 182 | text 183 | .split(/\r?\n/) 184 | .filter((x) => WHITESPACE_REGEX.test(x)) 185 | .forEach((line, i) => { 186 | // line = line.trim(); 187 | const lineIndex = lineCounter++; 188 | const lineContext: any = { 189 | line: { 190 | value: line, 191 | index: lineIndex, 192 | }, 193 | data, 194 | }; 195 | lineContexts[`${i}`] = lineContext; 196 | line 197 | .split(" ") 198 | .filter((x) => WHITESPACE_REGEX.test(x)) 199 | .forEach((word, j, wordArr) => { 200 | const wordIndex = wordCounter++; 201 | const wordContext: any = { 202 | word: { 203 | value: word, 204 | index: wordIndex, 205 | }, 206 | line: lineContext.line, 207 | data, 208 | }; 209 | wordContexts[`${i}#${j}`] = wordContext; 210 | if (j === 0) { 211 | lineContext.startWord = { 212 | value: word, 213 | index: wordIndex, 214 | }; 215 | } 216 | if (j === wordArr.length - 1) { 217 | lineContext.endWord = { 218 | value: word, 219 | index: wordIndex, 220 | }; 221 | } 222 | word.split("").forEach((char, k, charArr) => { 223 | const charIndex = charCounter++; 224 | const charContext: any = { 225 | char: { 226 | value: char, 227 | index: charIndex, 228 | }, 229 | word: wordContext.word, 230 | line: lineContext.line, 231 | data, 232 | }; 233 | charContexts[`${i}#${j}#${k}`] = charContext; 234 | if (j === 0 && k === 0) { 235 | lineContext.startChar = { 236 | value: char, 237 | index: charIndex, 238 | }; 239 | } 240 | if (j === wordArr.length - 1 && k === charArr.length - 1) { 241 | lineContext.endChar = { 242 | value: char, 243 | index: charIndex, 244 | }; 245 | } 246 | if (k === 0) { 247 | wordContext.startChar = { 248 | value: char, 249 | index: charIndex, 250 | }; 251 | } 252 | if (k === charArr.length - 1) { 253 | wordContext.endChar = { 254 | value: char, 255 | index: charIndex, 256 | }; 257 | } 258 | }); 259 | }); 260 | }); 261 | 262 | Object.values(lineContexts).forEach((context) => { 263 | context.charCount = charCounter; 264 | context.wordCount = wordCounter; 265 | context.lineCount = lineCounter; 266 | }); 267 | 268 | Object.values(wordContexts).forEach((context) => { 269 | context.charCount = charCounter; 270 | context.wordCount = wordCounter; 271 | context.lineCount = lineCounter; 272 | }); 273 | 274 | Object.values(charContexts).forEach((context) => { 275 | context.charCount = charCounter; 276 | context.wordCount = wordCounter; 277 | context.lineCount = lineCounter; 278 | }); 279 | 280 | return { 281 | lineContexts, 282 | wordContexts, 283 | charContexts, 284 | }; 285 | }; 286 | 287 | const MotionText = ({ 288 | text, 289 | variants = {}, 290 | renderers = {}, 291 | styles = {}, 292 | classes = {}, 293 | data, 294 | ...otherProps 295 | }: MotionTextProps) => { 296 | const { lineContexts, wordContexts, charContexts } = React.useMemo( 297 | () => getContexts(text, data), 298 | [text, JSON.stringify(data)] 299 | ); 300 | 301 | return ( 302 | 303 | {text 304 | .split(/\r?\n/) 305 | .filter((x) => WHITESPACE_REGEX.test(x)) 306 | .map((line, i) => { 307 | const LineRenderer = renderers?.line ?? DEFAULT_RENDERERS.line; 308 | 309 | return ( 310 | 311 | 317 | {line 318 | .split(" ") 319 | .filter((x) => WHITESPACE_REGEX.test(x)) 320 | .map((word, j, wordArr) => { 321 | const WordRenderer = 322 | renderers?.word ?? DEFAULT_RENDERERS.word; 323 | 324 | return ( 325 | 326 | 336 | {word.split("").map((char, k) => { 337 | const CharRenderer = 338 | renderers?.char ?? DEFAULT_RENDERERS.char; 339 | return ( 340 | 341 | 351 | {char} 352 | 353 | 354 | ); 355 | })} 356 | 357 | {j !== wordArr.length - 1 &&  } 358 | 359 | ); 360 | })} 361 | 362 | 363 | ); 364 | })} 365 | 366 | ); 367 | }; 368 | 369 | export default MotionText; 370 | -------------------------------------------------------------------------------- /components/SparkleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { chakra, ChakraComponent } from "@chakra-ui/react"; 3 | 4 | const Svg = chakra("svg"); 5 | 6 | function SparkleIcon(props: ChakraComponent<"svg">) { 7 | return ( 8 | 17 | 21 | 22 | ); 23 | } 24 | 25 | SparkleIcon.defaultProps = { 26 | h: 4, 27 | w: 4, 28 | }; 29 | 30 | export default SparkleIcon; 31 | -------------------------------------------------------------------------------- /components/TextReveal.tsx: -------------------------------------------------------------------------------- 1 | import MotionText, { MotionTextVariants } from "./MotionText"; 2 | 3 | const spring = { 4 | type: "spring", 5 | mass: 0.5, 6 | damping: 35, 7 | stiffness: 200, 8 | restDelta: 0.000001, 9 | }; 10 | 11 | const CHAR_DELAY = 0.05; 12 | 13 | interface TextRevealProps { 14 | text: string; 15 | delay?: number; 16 | } 17 | 18 | const variants: MotionTextVariants = { 19 | char: { 20 | hidden: (context) => ({ 21 | y: "100%", 22 | }), 23 | visible: (context) => ({ 24 | y: 0, 25 | transition: { 26 | ...spring, 27 | delay: context.char.index * CHAR_DELAY + (context.data?.delay || 0), 28 | }, 29 | }), 30 | }, 31 | }; 32 | 33 | const TextReveal = ({ text, delay = 0 }: TextRevealProps) => { 34 | return ( 35 | 42 | ); 43 | }; 44 | 45 | export default TextReveal; 46 | -------------------------------------------------------------------------------- /components/Ticker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | motion, 4 | MotionValue, 5 | useMotionTemplate, 6 | useSpring, 7 | useTransform, 8 | } from "framer-motion"; 9 | import { useScrollClock } from "../hooks"; 10 | 11 | interface TickerProps extends React.HTMLProps { 12 | loopDuration?: number; 13 | direction?: "x" | "y"; 14 | children: React.ReactNode; 15 | } 16 | 17 | const Ticker = ({ 18 | loopDuration = 12000, 19 | direction = "x", 20 | children, 21 | ...otherProps 22 | }: TickerProps) => { 23 | const clock = useScrollClock({ scrollAccelerationFactor: 15 }); 24 | const progress = useTransform( 25 | clock, 26 | (time) => (time % loopDuration) / loopDuration 27 | ); 28 | const percentage = useTransform(progress, (t) => t * 100); 29 | const translation = useMotionTemplate`-${percentage}%`; 30 | const styleAttr = direction === "y" ? "translateY" : "translateX"; 31 | const leftOffset = direction === "y" ? 0 : "100%"; 32 | const topOffset = direction === "y" ? "100%" : 0; 33 | 34 | return ( 35 |
44 | 45 |
{children}
46 |
55 | {children} 56 |
57 |
58 |
59 | ); 60 | }; 61 | 62 | export default Ticker; 63 | -------------------------------------------------------------------------------- /components/sections/AboutSection.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Center, Flex, Heading, Stack, Text } from "@chakra-ui/layout"; 2 | import { chakra } from "@chakra-ui/system"; 3 | import Image from "next/image"; 4 | import { Scroll } from "scrollex"; 5 | import Ticker from "../Ticker"; 6 | import InlineList from "../InlineList"; 7 | import { portfolio } from "../../data"; 8 | 9 | const ChakraTicker = chakra(Ticker); 10 | const ScrollSection = chakra(Scroll.Section); 11 | 12 | const AboutSection = () => { 13 | return ( 14 | 15 | 16 | 17 | 18 | {portfolio.about.bio} 19 | 20 | 21 | 22 | 28 | 35 | 36 | 37 | 38 | 39 | 47 |
48 | 49 | 59 | 65 | 66 | 67 | {portfolio.about.firstName} {portfolio.about.lastName} 68 | 69 | 70 |
71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default AboutSection; 78 | -------------------------------------------------------------------------------- /components/sections/HeadingSection.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Heading } from "@chakra-ui/layout"; 2 | import { chakra } from "@chakra-ui/system"; 3 | import { Keyframes, Scroll } from "scrollex"; 4 | 5 | const ScrollSection = chakra(Scroll.Section); 6 | const ScrollItem = chakra(Scroll.Item); 7 | 8 | const keyframes: Record = { 9 | heading: ({ section, container }) => ({ 10 | [section.topAt("container-top")]: { 11 | translateX: "0%", 12 | }, 13 | [section.bottomAt("container-bottom") - container.height / 3]: { 14 | translateX: "-100%", 15 | }, 16 | }), 17 | }; 18 | 19 | const HeadingSection = ({ heading }: any) => { 20 | return ( 21 | 22 | 23 | 24 | 25 | {heading} 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default HeadingSection; 34 | -------------------------------------------------------------------------------- /components/sections/IntroSection.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Center, Heading, HStack, Stack } from "@chakra-ui/layout"; 2 | import { chakra } from "@chakra-ui/system"; 3 | import Image from "next/image"; 4 | import { Scroll } from "scrollex"; 5 | import { motion, useSpring, useTransform } from "framer-motion"; 6 | import TextReveal from "../TextReveal"; 7 | import gradientImg from "../../public/gradient-sm.webp"; 8 | import { portfolio } from "../../data"; 9 | import { useScrollClock } from "../../hooks"; 10 | import ColorModeButton from "../ColorModeButton"; 11 | 12 | const MotionHStack = motion(HStack); 13 | const MotionBox = motion(Box); 14 | const ScrollSection = chakra(Scroll.Section); 15 | 16 | const GradientImg = () => { 17 | const clock = useScrollClock({ scrollAccelerationFactor: 20 }); 18 | const rotate = useTransform(clock, (time) => time / 100); 19 | return ( 20 | 29 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | const IntroSection = () => { 44 | return ( 45 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | 53 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 74 | Web3 75 | 76 | Developer 77 | 78 | 79 |
80 |
81 | ); 82 | }; 83 | 84 | export default IntroSection; 85 | -------------------------------------------------------------------------------- /components/sections/ProjectSection.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Center, Flex, Heading, Text } from "@chakra-ui/layout"; 2 | import { chakra } from "@chakra-ui/system"; 3 | import Image from "next/image"; 4 | import { Scroll } from "scrollex"; 5 | import Ticker from "../Ticker"; 6 | import InlineList from "../InlineList"; 7 | import LinkButton from "../LinkButton"; 8 | 9 | const ChakraTicker = chakra(Ticker); 10 | const ScrollSection = chakra(Scroll.Section); 11 | 12 | const ProjectSection = ({ project }: any) => { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | Built With 22 | 23 |
24 |
25 | 31 | 32 | 33 | 34 | 35 |
36 | 43 | View Project 44 | 45 |
46 | 47 | 48 | 49 | {project.name} 50 | 51 | 52 | {project.description} 53 | 54 | 55 | 56 |
57 | ); 58 | }; 59 | 60 | export default ProjectSection; 61 | -------------------------------------------------------------------------------- /data.ts: -------------------------------------------------------------------------------- 1 | import profileImg from "./public/profile.jpg"; 2 | import stackOverflowImg from "./public/decentralized-stackoverflow.webp"; 3 | import solanaAdsImg from "./public/solana-ads.jpg"; 4 | 5 | export const portfolio = { 6 | about: { 7 | firstName: "Kamila", 8 | lastName: "Mendoza", 9 | img: profileImg, 10 | bio: `Hi, I'm Kamila! 11 | 12 | I began my web development journey 8 years ago and for the last 2 have committed myself to learning web3 technologies. 13 | 14 | I have considerable experience with solidity and rust and have developed dozens of smart contracts for ethereum/solana. 15 | 16 | I also have considerable experience with nextjs, typescript, and postgres, which has become my stack of choice for most dapps. 17 | 18 | Through my career I have dabbled with several avenues of programming including security, data science, and computer vision, though nothing has satisfied me quite like web development. 19 | 20 | I was born in Mexico City and moved to the US when I was was 15. Because of this I am fluent in both Spanish and English. 21 | 22 | Shortly after moving to the US, I studied computer science at the Rochester Institiute of Technology where I graduated with a 3.95 GPA 23 | 24 | After school, I worked as a fullstack engineer at Target for 3 years where I primarily worked on supply chain API's and UI's. 25 | 26 | I then decided that enterprise life is not for me and jumped into the startup world where I've been enjoying my time working with small teams!`, 27 | skills: ["TypeScript", "NextJS", "Rust", "Solidity", "Solana", "Polygon"], 28 | }, 29 | projects: [ 30 | { 31 | name: "Decentralized Stack Overflow", 32 | img: stackOverflowImg, 33 | tools: ["NextJS", "TypeScript", "Solidity", "Polygon"], 34 | url: "https://pointer.gg", 35 | description: `In this project, I built a decentralized forum via Polygon that allows users to post and answer programming related questions. 36 | 37 | Users can post a question for a small fee at which time they also offer a reward amount to incentivize answers. 38 | 39 | Other users can pay a small fee to answer the question and make them eligible for the reward. Those answering must pay a small fee to prevent them from spamming questions with poor quality answers. 40 | 41 | For 1 month answers to a question will be hidden, but interested parties can pay a small fee to see the answers and upvote them. 42 | 43 | After this month has passed, the user with the most upvoted answer will receive the allocated reward.`, 44 | }, 45 | { 46 | name: "Ad Slots Via Solana", 47 | img: solanaAdsImg, 48 | tools: ["NextJS", "TypeScript", "Rust", "Solana"], 49 | url: "https://pointer.gg", 50 | description: `In this project I built a smart contract and UI to allow users to purchase ad slots on my blog. 51 | 52 | My dev/design blog brings in 1k unique hits per day and provides unique exposure to top developers and designers. 53 | 54 | This gives users the opportunity to advertise to a highly educated and passionate community. 55 | 56 | Ads can be scheduled through a calendar and purchased in the app, though must be approved before they will display, therefore it's advisable not to schedule an ad to run immediately after purchase.`, 57 | }, 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /hooks.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useScrollState, useScrollValue } from "scrollex"; 3 | import { 4 | useAnimationFrame, 5 | useMotionValue, 6 | useSpring, 7 | useTransform, 8 | } from "framer-motion"; 9 | 10 | type ScrollStatus = "up" | "down" | "static"; 11 | type ScrollDirection = "up" | "down"; 12 | 13 | // Derive current scroll status from velocity 14 | const useScrollStatus = (): ScrollStatus => { 15 | const status = useScrollState(({ velocity }) => { 16 | if (velocity > 0) { 17 | return "down"; 18 | } else if (velocity < 0) { 19 | return "up"; 20 | } else { 21 | return "static"; 22 | } 23 | }); 24 | return status || "static"; 25 | }; 26 | 27 | // This will never return to static, it will remember the last scroll direction 28 | const useLastScrollDirection = (): ScrollDirection => { 29 | const [lastDirection, setLastDirection] = 30 | React.useState("down"); 31 | const scrollStatus = useScrollStatus(); 32 | React.useEffect(() => { 33 | if (scrollStatus === "up" || scrollStatus === "down") { 34 | setLastDirection(scrollStatus); 35 | } 36 | }, [scrollStatus]); 37 | return lastDirection; 38 | }; 39 | 40 | // Get scroll position as MotionValue 41 | const useScrollPosition = () => { 42 | return useScrollValue(({ position }) => position); 43 | }; 44 | 45 | // Reversible clock as MotionValue 46 | const useClock = ({ defaultValue = 0, reverse = false } = {}) => { 47 | const rawClock = useMotionValue(0); 48 | const clock = useMotionValue(defaultValue); 49 | useAnimationFrame((t) => { 50 | const dt = t - rawClock.get(); 51 | rawClock.set(rawClock.get() + dt); 52 | if (reverse) { 53 | clock.set(clock.get() - dt); 54 | } else { 55 | clock.set(clock.get() + dt); 56 | } 57 | }); 58 | return clock; 59 | }; 60 | 61 | // Compose all of our helper hooks into a clock 62 | // that depends on scroll direction/position 63 | export const useScrollClock = ({ scrollAccelerationFactor = 10 } = {}) => { 64 | const scrollPosition = useScrollPosition(); 65 | const lastScrollDirection = useLastScrollDirection(); 66 | const clock = useClock({ 67 | defaultValue: Date.now(), 68 | reverse: lastScrollDirection === "up", 69 | }); 70 | 71 | const scrollClock = useTransform( 72 | [clock, scrollPosition as any], 73 | ([time, pos]: number[]) => time + (pos || 0) * scrollAccelerationFactor 74 | ); 75 | 76 | // Smooth out motion with a spring 77 | return useSpring(scrollClock, { mass: 0.05, stiffness: 100, damping: 10 }); 78 | }; 79 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webb3-portfolio", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "predev": "npx @chakra-ui/cli tokens ./theme/index.tsx", 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/react": "^2.2.4", 14 | "@emotion/react": "^11.9.3", 15 | "@emotion/styled": "^11.9.3", 16 | "framer-motion": "^6.5.1", 17 | "next": "12.2.2", 18 | "react": "18.2.0", 19 | "react-dom": "18.2.0", 20 | "react-toggle-dark-mode": "^1.1.0", 21 | "scrollex": "^2.0.0" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "18.0.6", 25 | "@types/react": "18.0.15", 26 | "@types/react-dom": "18.0.6", 27 | "eslint": "8.20.0", 28 | "eslint-config-next": "12.2.2", 29 | "typescript": "4.7.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { ChakraProvider } from "@chakra-ui/react"; 4 | import { theme } from "../theme"; 5 | 6 | function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { chakra } from "@chakra-ui/system"; 2 | import type { NextPage } from "next"; 3 | import { Scroll } from "scrollex"; 4 | import HeadingSection from "../components/sections/HeadingSection"; 5 | import AboutSection from "../components/sections/AboutSection"; 6 | import IntroSection from "../components/sections/IntroSection"; 7 | import ProjectSection from "../components/sections/ProjectSection"; 8 | import { portfolio } from "../data"; 9 | 10 | const ScrollContainer = chakra(Scroll.Container); 11 | 12 | const Home: NextPage = () => { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | {portfolio.projects.map((project) => ( 20 | 21 | ))} 22 | 23 | ); 24 | }; 25 | 26 | export default Home; 27 | -------------------------------------------------------------------------------- /public/decentralized-stackoverflow.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/1fb0388dee8e837a440eb565f481ec2c77988e8b/public/decentralized-stackoverflow.webp -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/1fb0388dee8e837a440eb565f481ec2c77988e8b/public/favicon.ico -------------------------------------------------------------------------------- /public/gradient-lg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/1fb0388dee8e837a440eb565f481ec2c77988e8b/public/gradient-lg.webp -------------------------------------------------------------------------------- /public/gradient-sm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/1fb0388dee8e837a440eb565f481ec2c77988e8b/public/gradient-sm.webp -------------------------------------------------------------------------------- /public/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/1fb0388dee8e837a440eb565f481ec2c77988e8b/public/profile.jpg -------------------------------------------------------------------------------- /public/project-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/1fb0388dee8e837a440eb565f481ec2c77988e8b/public/project-1.png -------------------------------------------------------------------------------- /public/solana-ads.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/1fb0388dee8e837a440eb565f481ec2c77988e8b/public/solana-ads.jpg -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=block"); 2 | 3 | html, 4 | body { 5 | padding: 0; 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 8 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 9 | } 10 | 11 | a { 12 | color: inherit; 13 | text-decoration: none; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | -------------------------------------------------------------------------------- /theme/breakpoints.tsx: -------------------------------------------------------------------------------- 1 | import * as tools from "@chakra-ui/theme-tools"; 2 | 3 | export const breakpoints: tools.BaseBreakpointConfig = { 4 | sm: "30em", 5 | md: "48em", 6 | lg: "62em", 7 | xl: "80em", 8 | "2xl": "96em" 9 | }; 10 | 11 | export const mediaQueries = { 12 | base: `@media screen and (min-width: 0em)`, 13 | sm: `@media screen and (min-width: ${breakpoints.sm})`, 14 | md: `@media screen and (min-width: ${breakpoints.md})`, 15 | lg: `@media screen and (min-width: ${breakpoints.lg})`, 16 | xl: `@media screen and (min-width: ${breakpoints.xl})`, 17 | "2xl": `@media screen and (min-width: ${breakpoints["2xl"]})` 18 | }; 19 | -------------------------------------------------------------------------------- /theme/colors.tsx: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | gray: { 3 | 50: "#fafafa", 4 | 100: "#f5f5f5", 5 | 200: "#e5e5e5", 6 | 300: "#d4d4d4", 7 | 400: "#a3a3a3", 8 | 500: "#737373", 9 | 600: "#525252", 10 | 700: "#404040", 11 | 800: "#262626", 12 | 900: "#171717", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /theme/components/button.tsx: -------------------------------------------------------------------------------- 1 | import { mode } from "@chakra-ui/theme-tools"; 2 | import type { SystemStyleFunction } from "@chakra-ui/theme-tools"; 3 | import { Button as ChakraComponent } from "@chakra-ui/react"; 4 | 5 | export const Button = { 6 | baseStyle: { 7 | border: "sm", 8 | rounded: "none", 9 | }, 10 | variants: { 11 | outline: { 12 | borderColor: "border-contrast-xl", 13 | _hover: { 14 | bg: "bg-contrast-md", 15 | }, 16 | _active: { 17 | bg: "bg-contrast-xl", 18 | }, 19 | _disabled: { 20 | bg: "bg-contrast-md", 21 | }, 22 | }, 23 | }, 24 | }; 25 | 26 | ChakraComponent.defaultProps = { 27 | ...ChakraComponent.defaultProps, 28 | fontSize: "lg", 29 | variant: "outline", 30 | }; 31 | -------------------------------------------------------------------------------- /theme/components/heading.tsx: -------------------------------------------------------------------------------- 1 | import { fonts, textStyles } from "../text"; 2 | 3 | export const Heading = { 4 | baseStyle: { 5 | fontFamily: fonts.heading, 6 | }, 7 | sizes: textStyles, 8 | }; 9 | -------------------------------------------------------------------------------- /theme/components/hstack.tsx: -------------------------------------------------------------------------------- 1 | import { HStack as ChakraComponent } from "@chakra-ui/react"; 2 | 3 | ChakraComponent.defaultProps = { 4 | ...ChakraComponent.defaultProps, 5 | spacing: "md" 6 | }; 7 | 8 | export const HStack = {}; 9 | -------------------------------------------------------------------------------- /theme/components/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "./button"; 2 | import { Heading } from "./heading"; 3 | import { Text } from "./text"; 4 | import { HStack } from "./hstack"; 5 | import { Stack } from "./stack"; 6 | import { Input } from "./input"; 7 | 8 | export const components = { 9 | Button, 10 | Heading, 11 | HStack, 12 | Stack, 13 | Text, 14 | Input, 15 | }; 16 | -------------------------------------------------------------------------------- /theme/components/input.tsx: -------------------------------------------------------------------------------- 1 | import { Input as ChakraComponent } from "@chakra-ui/react"; 2 | 3 | ChakraComponent.defaultProps = { 4 | ...ChakraComponent.defaultProps, 5 | focusBorderColor: "white", 6 | variant: "outline", 7 | rounded: "none", 8 | }; 9 | 10 | export const Input = { 11 | variants: { 12 | outline: { 13 | field: { 14 | bg: "transparent", 15 | border: "sm", 16 | color: "text-contrast-md", 17 | 18 | _hover: { 19 | border: "sm", 20 | }, 21 | _disabled: { 22 | border: "sm", 23 | }, 24 | _placeholder: { 25 | color: "text-contrast-sm", 26 | }, 27 | }, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /theme/components/stack.tsx: -------------------------------------------------------------------------------- 1 | import { Stack as ChakraComponent } from "@chakra-ui/react"; 2 | 3 | ChakraComponent.defaultProps = { 4 | ...ChakraComponent.defaultProps, 5 | spacing: "md" 6 | }; 7 | 8 | export const Stack = {}; 9 | -------------------------------------------------------------------------------- /theme/components/text.tsx: -------------------------------------------------------------------------------- 1 | import { fonts, textStyles } from "../text"; 2 | import { theme } from "@chakra-ui/react"; 3 | 4 | export const Text = { 5 | baseStyle: { 6 | fontFamily: fonts.body, 7 | }, 8 | sizes: textStyles, 9 | }; 10 | -------------------------------------------------------------------------------- /theme/index.tsx: -------------------------------------------------------------------------------- 1 | import { extendTheme } from "@chakra-ui/react"; 2 | import { styles } from "./styles"; 3 | import { textStyles, fonts } from "./text"; 4 | import { semanticTokens } from "./tokens"; 5 | import { colors } from "./colors"; 6 | import { components } from "./components"; 7 | 8 | const config = { 9 | initialColorMode: "dark", 10 | useSystemColorMode: false, 11 | }; 12 | 13 | export const theme = extendTheme({ 14 | fonts, 15 | config, 16 | styles, 17 | colors, 18 | textStyles, 19 | semanticTokens, 20 | shadows: { outline: "0 0 0 3px var(--chakra-colors-focus-ring)" }, 21 | components, 22 | }); 23 | -------------------------------------------------------------------------------- /theme/styles.tsx: -------------------------------------------------------------------------------- 1 | import { StyleFunctionProps } from "@chakra-ui/theme-tools"; 2 | 3 | export const styles = { 4 | global: (props: StyleFunctionProps) => ({ 5 | html: { 6 | fontSize: { 7 | base: "70%", 8 | md: "100%", 9 | }, 10 | }, 11 | body: { 12 | bgColor: "bg-body", 13 | }, 14 | "::selection": { 15 | background: "white", 16 | color: "black", 17 | }, 18 | }), 19 | }; 20 | -------------------------------------------------------------------------------- /theme/text.tsx: -------------------------------------------------------------------------------- 1 | const FONT_SCALE_BASE = 1; 2 | const FONT_SCALE_MULTIPLIER = 1.5; 3 | 4 | export const fonts = { 5 | heading: `'Major Mono Display', sans-serif`, 6 | body: `'Space Mono', sans-serif`, 7 | }; 8 | 9 | export const textStyles = { 10 | xs: { 11 | fontSize: FONT_SCALE_BASE * FONT_SCALE_MULTIPLIER ** -1 + "rem", 12 | fontWeight: 400, 13 | lineHeight: "150%", 14 | letterSpacing: "0", 15 | }, 16 | sm: { 17 | fontSize: FONT_SCALE_BASE * FONT_SCALE_MULTIPLIER ** 0 + "rem", 18 | fontWeight: 400, 19 | lineHeight: "150%", 20 | letterSpacing: "0", 21 | }, 22 | md: { 23 | fontSize: FONT_SCALE_BASE * FONT_SCALE_MULTIPLIER ** 1 + "rem", 24 | fontWeight: 400, 25 | lineHeight: "150%", 26 | letterSpacing: "0", 27 | }, 28 | lg: { 29 | fontSize: FONT_SCALE_BASE * FONT_SCALE_MULTIPLIER ** 2 + "rem", 30 | fontWeight: 400, 31 | lineHeight: "150%", 32 | letterSpacing: "0", 33 | }, 34 | xl: { 35 | fontSize: FONT_SCALE_BASE * FONT_SCALE_MULTIPLIER ** 3 + "rem", 36 | fontWeight: 600, 37 | lineHeight: "120%", 38 | letterSpacing: "0", 39 | }, 40 | "2xl": { 41 | fontSize: FONT_SCALE_BASE * FONT_SCALE_MULTIPLIER ** 4 + "rem", 42 | fontWeight: 600, 43 | lineHeight: "120%", 44 | letterSpacing: "0", 45 | }, 46 | "3xl": { 47 | fontSize: FONT_SCALE_BASE * FONT_SCALE_MULTIPLIER ** 5 + "rem", 48 | fontWeight: 600, 49 | lineHeight: "110%", 50 | letterSpacing: "0", 51 | }, 52 | "4xl": { 53 | fontSize: FONT_SCALE_BASE * FONT_SCALE_MULTIPLIER ** 6 + "rem", 54 | fontWeight: 600, 55 | lineHeight: "110%", 56 | letterSpacing: "0", 57 | }, 58 | "5xl": { 59 | fontSize: FONT_SCALE_BASE * FONT_SCALE_MULTIPLIER ** 7 + "rem", 60 | fontWeight: 600, 61 | lineHeight: "100%", 62 | letterSpacing: "0", 63 | }, 64 | "6xl": { 65 | fontSize: FONT_SCALE_BASE * FONT_SCALE_MULTIPLIER ** 8 + "rem", 66 | fontWeight: 600, 67 | lineHeight: "100%", 68 | letterSpacing: "0", 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /theme/tokens.tsx: -------------------------------------------------------------------------------- 1 | import { mediaQueries } from "./breakpoints"; 2 | 3 | const MD_SPACE_UNIT = 0.25; 4 | const BASE_SPACE_UNIT = MD_SPACE_UNIT * 0.8; 5 | 6 | export const semanticTokens = { 7 | colors: { 8 | "bg-body": { 9 | default: "gray.50", 10 | _dark: "gray.900", 11 | }, 12 | "bg-body-inverse": { 13 | default: "gray.900", 14 | _dark: "gray.50", 15 | }, 16 | "bg-contrast-xs": { 17 | default: "rgba(0, 0, 0, 0.0125)", 18 | _dark: "rgba(255, 255, 255, 0.0125)", 19 | }, 20 | "bg-contrast-sm": { 21 | default: "rgba(0, 0, 0, 0.025)", 22 | _dark: "rgba(255, 255, 255, 0.025)", 23 | }, 24 | "bg-contrast-md": { 25 | default: "rgba(0, 0, 0, 0.05)", 26 | _dark: "rgba(255, 255, 255, 0.05)", 27 | }, 28 | "bg-contrast-lg": { 29 | default: "rgba(0, 0, 0, 0.075)", 30 | _dark: "rgba(255, 255, 255, 0.075)", 31 | }, 32 | "bg-contrast-xl": { 33 | default: "rgba(0, 0, 0, 0.1)", 34 | _dark: "rgba(255, 255, 255, 0.1)", 35 | }, 36 | "text-contrast-xs": { 37 | default: "blackAlpha.500", 38 | _dark: "whiteAlpha.500", 39 | }, 40 | "text-contrast-sm": { 41 | default: "blackAlpha.600", 42 | _dark: "whiteAlpha.600", 43 | }, 44 | "text-contrast-md": { 45 | default: "blackAlpha.700", 46 | _dark: "whiteAlpha.700", 47 | }, 48 | "text-contrast-lg": { 49 | default: "blackAlpha.800", 50 | _dark: "whiteAlpha.800", 51 | }, 52 | "text-contrast-xl": { 53 | default: "blackAlpha.900", 54 | _dark: "whiteAlpha.900", 55 | }, 56 | "border-contrast-xs": { 57 | default: "rgba(0, 0, 0, 0.1)", 58 | _dark: "rgba(255, 255, 255, 0.1)", 59 | }, 60 | "border-contrast-sm": { 61 | default: "rgba(0, 0, 0, 0.2)", 62 | _dark: "rgba(255, 255, 255, 0.2)", 63 | }, 64 | "border-contrast-md": { 65 | default: "rgba(0, 0, 0, 0.3)", 66 | _dark: "rgba(255, 255, 255, 0.3)", 67 | }, 68 | "border-contrast-lg": { 69 | default: "rgba(0, 0, 0, 0.4)", 70 | _dark: "rgba(255, 255, 255, 0.4)", 71 | }, 72 | "border-contrast-xl": { 73 | default: "rgba(0, 0, 0, 0.5)", 74 | _dark: "rgba(255, 255, 255, 0.5)", 75 | }, 76 | "focus-ring": { 77 | default: "black", 78 | _dark: "white", 79 | }, 80 | }, 81 | space: { 82 | xs: { 83 | [mediaQueries.base]: 1 * BASE_SPACE_UNIT + "rem", 84 | [mediaQueries.md]: 1 * MD_SPACE_UNIT + "rem", 85 | }, 86 | sm: { 87 | [mediaQueries.base]: 2 * BASE_SPACE_UNIT + "rem", 88 | [mediaQueries.md]: 2 * MD_SPACE_UNIT + "rem", 89 | }, 90 | md: { 91 | [mediaQueries.base]: 4 * BASE_SPACE_UNIT + "rem", 92 | [mediaQueries.md]: 4 * MD_SPACE_UNIT + "rem", 93 | }, 94 | lg: { 95 | [mediaQueries.base]: 6 * BASE_SPACE_UNIT + "rem", 96 | [mediaQueries.md]: 6 * MD_SPACE_UNIT + "rem", 97 | }, 98 | xl: { 99 | [mediaQueries.base]: 8 * BASE_SPACE_UNIT + "rem", 100 | [mediaQueries.md]: 8 * MD_SPACE_UNIT + "rem", 101 | }, 102 | "2xl": { 103 | [mediaQueries.base]: 12 * BASE_SPACE_UNIT + "rem", 104 | [mediaQueries.md]: 12 * MD_SPACE_UNIT + "rem", 105 | }, 106 | "3xl": { 107 | [mediaQueries.base]: 16 * BASE_SPACE_UNIT + "rem", 108 | [mediaQueries.md]: 16 * MD_SPACE_UNIT + "rem", 109 | }, 110 | "4xl": { 111 | [mediaQueries.base]: 24 * BASE_SPACE_UNIT + "rem", 112 | [mediaQueries.md]: 24 * MD_SPACE_UNIT + "rem", 113 | }, 114 | "5xl": { 115 | [mediaQueries.base]: 32 * BASE_SPACE_UNIT + "rem", 116 | [mediaQueries.md]: 32 * MD_SPACE_UNIT + "rem", 117 | }, 118 | }, 119 | borders: { 120 | sm: `1px solid var(--chakra-colors-border-contrast-xl)`, 121 | md: `2px solid var(--chakra-colors-border-contrast-xl)`, 122 | lg: `3px solid var(--chakra-colors-border-contrast-xl)`, 123 | }, 124 | sizes: { 125 | "h-screen": "calc(var(--vh) * 100)", 126 | "h-screen-2": "calc(var(--vh) * 200)", 127 | "h-screen-3": "calc(var(--vh) * 300)", 128 | "h-screen-4": "calc(var(--vh) * 400)", 129 | "h-screen-5": "calc(var(--vh) * 500)", 130 | "w-screen": "calc(var(--vw) * 100)", 131 | "w-screen-2": "calc(var(--vw) * 200)", 132 | "w-screen-3": "calc(var(--vw) * 300)", 133 | "w-screen-4": "calc(var(--vw) * 400)", 134 | "w-screen-5": "calc(var(--vw) * 500)", 135 | }, 136 | }; 137 | 138 | if (typeof window !== "undefined") { 139 | const updateViewportUnits = () => { 140 | let vh = window.innerHeight * 0.01; 141 | let vw = window.innerWidth * 0.01; 142 | document.documentElement.style.setProperty("--vh", `${vh}px`); 143 | document.documentElement.style.setProperty("--vw", `${vw}px`); 144 | }; 145 | updateViewportUnits(); 146 | window.addEventListener("resize", updateViewportUnits); 147 | } 148 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | export function clamp(number: number, min: number, max: number) { 2 | return Math.max(min, Math.min(number, max)); 3 | } 4 | --------------------------------------------------------------------------------