├── .eslintrc.json
├── public
├── favicon.ico
├── profile.jpg
├── project-1.png
├── solana-ads.jpg
├── gradient-lg.webp
├── gradient-sm.webp
├── decentralized-stackoverflow.webp
└── vercel.svg
├── utils.ts
├── next.config.js
├── theme
├── components
│ ├── heading.tsx
│ ├── stack.tsx
│ ├── hstack.tsx
│ ├── text.tsx
│ ├── index.tsx
│ ├── input.tsx
│ └── button.tsx
├── colors.tsx
├── styles.tsx
├── index.tsx
├── breakpoints.tsx
├── text.tsx
└── tokens.tsx
├── next-env.d.ts
├── pages
├── _app.tsx
└── index.tsx
├── styles
├── globals.css
└── Home.module.css
├── .gitignore
├── components
├── InlineList.tsx
├── LinkButton.tsx
├── ColorModeButton.tsx
├── SparkleIcon.tsx
├── TextReveal.tsx
├── sections
│ ├── HeadingSection.tsx
│ ├── ProjectSection.tsx
│ ├── AboutSection.tsx
│ └── IntroSection.tsx
├── Ticker.tsx
└── MotionText.tsx
├── tsconfig.json
├── package.json
├── README.md
├── hooks.ts
└── data.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/HEAD/public/profile.jpg
--------------------------------------------------------------------------------
/public/project-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/HEAD/public/project-1.png
--------------------------------------------------------------------------------
/public/solana-ads.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/HEAD/public/solana-ads.jpg
--------------------------------------------------------------------------------
/public/gradient-lg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/HEAD/public/gradient-lg.webp
--------------------------------------------------------------------------------
/public/gradient-sm.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/HEAD/public/gradient-sm.webp
--------------------------------------------------------------------------------
/utils.ts:
--------------------------------------------------------------------------------
1 | export function clamp(number: number, min: number, max: number) {
2 | return Math.max(min, Math.min(number, max));
3 | }
4 |
--------------------------------------------------------------------------------
/public/decentralized-stackoverflow.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pointer-gg/web3-portfolio-site/HEAD/public/decentralized-stackoverflow.webp
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | }
6 |
7 | module.exports = nextConfig
8 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
22 | );
23 | }
24 |
25 | SparkleIcon.defaultProps = {
26 | h: 4,
27 | w: 4,
28 | };
29 |
30 | export default SparkleIcon;
31 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------