Skip to content

Commit afbd64e

Browse files
committed
added tech stack
1 parent 2c1ffcf commit afbd64e

8 files changed

Lines changed: 186 additions & 20 deletions

File tree

app/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import TimelineDemo from "@/components/timeline-demo";
22
import PixelatedCanvasDemo from "@/components/pixelated-canvas-demo";
33
import EncryptedTextDemoSecond from "@/components/encrypted-text-demo-2";
4+
import TechStack from "@/components/techstack";
45

56
export default function Home() {
67
return (
78
<main>
8-
<div className="p-8 flex flex-col md:flex-row items-center">
9+
<div className="p-8 flex flex-col lg:flex-row items-center gap-20">
910
<PixelatedCanvasDemo />
10-
<EncryptedTextDemoSecond/>
11+
<div className="p-4 flex-col gap-8">
12+
<EncryptedTextDemoSecond/>
13+
<TechStack/>
14+
</div>
1115
</div>
1216
<TimelineDemo/>
1317
</main>

components/encrypted-text-demo-2.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@ import { EncryptedText } from "@/components/ui/encrypted-text";
44
import React from "react";
55

66
export default function EncryptedTextDemoSecond() {
7+
const statements = [
8+
"Hello I'm Kathrina",
9+
"I'm a developer",
10+
"I build cool things",
11+
"Welcome to my portfolio",
12+
];
13+
714
return (
8-
<p className="mx-auto max-w-lg py-10 text-left text-3xl">
15+
<p className="mx-auto max-w-lg py-8 flex justify-center text-center text-3xl bg-black">
916
<EncryptedText
10-
text="Hello I'm Kathrina"
11-
encryptedClassName="text-neutral-500"
12-
revealedClassName="dark:text-white text-black"
17+
texts={statements}
18+
encryptedClassName="text-neutral-300"
19+
revealedClassName="dark:text-white text-white"
1320
revealDelayMs={50}
21+
cycleDelayMs={3500}
1422
/>
1523
</p>
1624
);

components/pixelated-canvas-demo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default function PixelatedCanvasDemo() {
3434
shape="square"
3535
backgroundColor="#000000"
3636
dropoutStrength={0.1}
37-
interactive
37+
interactive={false}
3838
responsive
3939
distortionStrength={3}
4040
distortionRadius={80}

components/techstack.tsx

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import {
5+
RiNextjsFill,
6+
RiTailwindCssFill,
7+
RiFirebaseFill,
8+
RiFlutterFill,
9+
} from "react-icons/ri";
10+
import {
11+
FaReact,
12+
FaNodeJs,
13+
FaRaspberryPi,
14+
FaDocker,
15+
FaLinux,
16+
FaGitAlt,
17+
FaGithub,
18+
} from "react-icons/fa";
19+
import { BsTypescript } from "react-icons/bs";
20+
import {
21+
SiExpress,
22+
SiPostgresql,
23+
SiMongodb,
24+
SiPython,
25+
SiJupyter,
26+
SiNumpy,
27+
SiPandas,
28+
SiArduino,
29+
SiPostman,
30+
SiGnubash,
31+
} from "react-icons/si";
32+
import { IconType } from "react-icons";
33+
34+
const TechStack = () => {
35+
const techstack = [
36+
"NextJS",
37+
"ReactJS",
38+
"Tailwind CSS",
39+
"Typescript",
40+
"NodeJS",
41+
"ExpressJS",
42+
"PostgreSQL",
43+
"Firebase",
44+
"MongoDB",
45+
"Flutter",
46+
"React Native",
47+
"Python",
48+
"Jupyter Notebook",
49+
"NumPy",
50+
"Pandas",
51+
"Arduino",
52+
"Raspberry Pi",
53+
"Docker",
54+
"Linux",
55+
"Bash",
56+
"Git",
57+
"Github",
58+
"Postman",
59+
];
60+
61+
const icons: Record<string, IconType> = {
62+
NextJS: RiNextjsFill,
63+
ReactJS: FaReact,
64+
"Tailwind CSS": RiTailwindCssFill,
65+
Typescript: BsTypescript,
66+
NodeJS: FaNodeJs,
67+
ExpressJS: SiExpress,
68+
PostgreSQL: SiPostgresql,
69+
Firebase: RiFirebaseFill,
70+
MongoDB: SiMongodb,
71+
Flutter: RiFlutterFill,
72+
"React Native": FaReact,
73+
Python: SiPython,
74+
"Jupyter Notebook": SiJupyter,
75+
NumPy: SiNumpy,
76+
Pandas: SiPandas,
77+
Arduino: SiArduino,
78+
"Raspberry Pi": FaRaspberryPi,
79+
Docker: FaDocker,
80+
Linux: FaLinux,
81+
Bash: SiGnubash,
82+
Git: FaGitAlt,
83+
Github: FaGithub,
84+
Postman: SiPostman,
85+
};
86+
87+
const [isOpen, setIsOpen] = useState(false);
88+
89+
const visibleTech = isOpen ? techstack : techstack.slice(0, 6);
90+
91+
return (
92+
<div className="mt-8 max-w-171">
93+
<div className="flex items-center justify-between mb-4">
94+
<h3 className="text-lg font-semibold text-black">Tech Stack</h3>
95+
<button
96+
aria-label={isOpen ? "Collapse tech list" : "Expand tech list"}
97+
onClick={() => setIsOpen((s) => !s)}
98+
className="ml-3 px-2 py-1 rounded-md text-black hover:bg-black/5"
99+
>
100+
{isOpen ? "▴" : "▾"}
101+
</button>
102+
</div>
103+
104+
<div className="flex flex-wrap gap-2">
105+
{visibleTech.map((t) => {
106+
const Icon = icons[t];
107+
return (
108+
<button
109+
key={t}
110+
className="inline-flex items-center gap-2 px-3 py-2 border-2 border-black text-black rounded-full hover:shadow-md transition"
111+
aria-label={t}
112+
title={t}
113+
>
114+
{Icon && <Icon className="text-lg shrink-0" />}
115+
<span className="text-xs lg:text-sm">{t}</span>
116+
</button>
117+
);
118+
})}
119+
</div>
120+
</div>
121+
);
122+
};
123+
124+
export default TechStack;

components/ui/encrypted-text.tsx

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { motion, useInView } from "motion/react";
44
import { cn } from "@/lib/utils";
55

66
type EncryptedTextProps = {
7-
text: string;
7+
texts: string[];
88
className?: string;
99
/**
1010
* Time in milliseconds between revealing each subsequent real character.
@@ -22,6 +22,8 @@ type EncryptedTextProps = {
2222
encryptedClassName?: string;
2323
/** CSS class for styling the revealed characters */
2424
revealedClassName?: string;
25+
/** When using texts array, duration to display each text before cycling (ms). Defaults to 3000ms. */
26+
cycleDelayMs?: number;
2527
};
2628

2729
const DEFAULT_CHARSET =
@@ -46,36 +48,53 @@ function generateGibberishPreservingSpaces(
4648
}
4749

4850
export const EncryptedText: React.FC<EncryptedTextProps> = ({
49-
text,
51+
texts,
5052
className,
5153
revealDelayMs = 50,
5254
charset = DEFAULT_CHARSET,
5355
flipDelayMs = 50,
5456
encryptedClassName,
5557
revealedClassName,
58+
cycleDelayMs = 3000,
5659
}) => {
5760
const ref = useRef<HTMLSpanElement>(null);
5861
const isInView = useInView(ref, { once: true });
5962
const [isClient, setIsClient] = useState(false);
63+
const [currentTextIndex, setCurrentTextIndex] = useState(0);
6064

6165
const [revealCount, setRevealCount] = useState<number>(0);
6266
const animationFrameRef = useRef<number | null>(null);
6367
const startTimeRef = useRef<number>(0);
6468
const lastFlipTimeRef = useRef<number>(0);
69+
const currentText = texts[currentTextIndex];
6570
const scrambleCharsRef = useRef<string[]>(
66-
text ? generateGibberishPreservingSpaces(text, charset).split("") : [],
71+
currentText ? generateGibberishPreservingSpaces(currentText, charset).split("") : [],
6772
);
6873

6974
useEffect(() => {
7075
setIsClient(true);
7176
}, []);
7277

78+
79+
// Cycle through texts if array is provided
80+
useEffect(() => {
81+
if (!texts || texts.length === 0 || !isClient) return;
82+
83+
const textDuration = cycleDelayMs;
84+
const interval = setInterval(() => {
85+
setCurrentTextIndex((prev) => (prev + 1) % texts.length);
86+
setRevealCount(0);
87+
}, textDuration);
88+
89+
return () => clearInterval(interval);
90+
}, [texts, cycleDelayMs, isClient]);
91+
7392
useEffect(() => {
7493
if (!isInView || !isClient) return;
7594

7695
// Reset state for a fresh animation whenever dependencies change
77-
const initial = text
78-
? generateGibberishPreservingSpaces(text, charset)
96+
const initial = currentText
97+
? generateGibberishPreservingSpaces(currentText, charset)
7998
: "";
8099
scrambleCharsRef.current = initial.split("");
81100
startTimeRef.current = performance.now();
@@ -88,7 +107,7 @@ export const EncryptedText: React.FC<EncryptedTextProps> = ({
88107
if (isCancelled) return;
89108

90109
const elapsedMs = now - startTimeRef.current;
91-
const totalLength = text.length;
110+
const totalLength = currentText.length;
92111
const currentRevealCount = Math.min(
93112
totalLength,
94113
Math.floor(elapsedMs / Math.max(1, revealDelayMs)),
@@ -105,7 +124,7 @@ export const EncryptedText: React.FC<EncryptedTextProps> = ({
105124
if (timeSinceLastFlip >= Math.max(0, flipDelayMs)) {
106125
for (let index = 0; index < totalLength; index += 1) {
107126
if (index >= currentRevealCount) {
108-
if (text[index] !== " ") {
127+
if (currentText[index] !== " ") {
109128
scrambleCharsRef.current[index] =
110129
generateRandomCharacter(charset);
111130
} else {
@@ -127,20 +146,20 @@ export const EncryptedText: React.FC<EncryptedTextProps> = ({
127146
cancelAnimationFrame(animationFrameRef.current);
128147
}
129148
};
130-
}, [isInView, isClient, text, revealDelayMs, charset, flipDelayMs]);
149+
}, [isInView, isClient, currentText, revealDelayMs, charset, flipDelayMs]);
131150

132-
if (!text) return null;
151+
if (!currentText) return null;
133152

134153
// Only render the animated version on client to prevent hydration mismatch
135154
if (!isClient) {
136155
return (
137156
<motion.span
138157
ref={ref}
139158
className={cn(className)}
140-
aria-label={text}
159+
aria-label={currentText}
141160
role="text"
142161
>
143-
{text}
162+
{currentText}
144163
</motion.span>
145164
);
146165
}
@@ -149,10 +168,10 @@ export const EncryptedText: React.FC<EncryptedTextProps> = ({
149168
<motion.span
150169
ref={ref}
151170
className={cn(className)}
152-
aria-label={text}
171+
aria-label={currentText}
153172
role="text"
154173
>
155-
{text.split("").map((char, index) => {
174+
{currentText.split("").map((char, index) => {
156175
const isRevealed = index < revealCount;
157176
const displayChar = isRevealed
158177
? char

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"radix-ui": "^1.4.3",
1818
"react": "19.2.4",
1919
"react-dom": "19.2.4",
20+
"react-icons": "^5.6.0",
2021
"shadcn": "^4.7.0",
2122
"tailwind-merge": "^3.5.0",
2223
"tw-animate-css": "^1.4.0"

public/avatar.jpeg

-160 KB
Binary file not shown.

0 commit comments

Comments
 (0)