Reubencf Claude Sonnet 4.6 (1M context) commited on
Commit
ebb6921
·
1 Parent(s): 280d9f7

redesign about section with image-fill text, simplify contact, overhaul SEO

Browse files

- About: new ImageFillText component using canvas destination-in compositing,
swaps video for static image; corner icons unchanged
- Contact: remove form fields, keep direct email/LinkedIn/Twitter links only
- Projects: update to 5 real names (Konkani AI, Tiny AYA Mobile, PowerPoint AI,
Multi Model Dataset, ISO Language)
- Marquee: replace Gemini and Ollama with Docker and PostgreSQL
- SEO: add Person + WebSite + ProfessionalService JSON-LD, dynamic OG image
via app/opengraph-image.tsx, theme-color (light/dark), viewport metadata,
sitemap section anchors, expanded keywords and googleBot directives
- Switch canonical URL from reubenfernandes.dev to reuben-fernandes.xyz

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

app/layout.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import type { Metadata } from "next";
2
  import { Kanit, JetBrains_Mono } from "next/font/google";
3
  import "./globals.css";
4
 
@@ -15,28 +15,156 @@ const jetbrainsMono = JetBrains_Mono({
15
  display: "swap",
16
  });
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  export const metadata: Metadata = {
19
- title: "Reuben Chagas Fernandes | Full Stack Developer from Goa",
20
- description: "Reuben Chagas Fernandes is a full stack developer from Goa, India, specializing in web development and software engineering.",
21
- keywords: ["Reuben Chagas Fernandes", "Reuben Fernandes", "Reuben Chagas", "Full Stack Developer", "Web Developer", "Goa", "India", "Software Engineering"],
22
- authors: [{ name: "Reuben Chagas Fernandes" }],
23
- creator: "Reuben Chagas Fernandes",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  robots: {
25
  index: true,
26
  follow: true,
 
 
 
 
 
 
 
 
 
27
  },
28
  openGraph: {
29
- title: "Reuben Chagas Fernandes | Full Stack Developer",
30
- description: "Reuben Chagas Fernandes is a full stack developer from Goa, India, specializing in web development and software engineering.",
 
 
31
  type: "website",
32
  locale: "en_US",
33
  },
34
  twitter: {
35
  card: "summary_large_image",
36
- title: "Reuben Chagas Fernandes | Full Stack Developer",
37
- description: "Reuben Chagas Fernandes is a full stack developer from Goa, India",
38
- images: ["https://portfolio-production-70ab.up.railway.app/reuben.png"],
 
39
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  };
41
 
42
  export default function RootLayout({
@@ -49,6 +177,20 @@ export default function RootLayout({
49
  <body
50
  className={`${kanit.variable} ${jetbrainsMono.variable} font-sans antialiased`}
51
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  {children}
53
  </body>
54
  </html>
 
1
+ import type { Metadata, Viewport } from "next";
2
  import { Kanit, JetBrains_Mono } from "next/font/google";
3
  import "./globals.css";
4
 
 
15
  display: "swap",
16
  });
17
 
18
+ const SITE_URL = "https://reuben-fernandes.xyz";
19
+ const SITE_NAME = "Reuben Chagas Fernandes";
20
+ const SITE_TITLE = "Reuben Chagas Fernandes | Full Stack Developer from Goa";
21
+ const SITE_DESCRIPTION =
22
+ "Reuben Chagas Fernandes is a full-stack developer from Goa, India, specializing in React, Next.js, Flutter, Node.js, Python, and AI/LLM research. Available for freelance and full-time roles.";
23
+
24
+ export const viewport: Viewport = {
25
+ themeColor: [
26
+ { media: "(prefers-color-scheme: light)", color: "#FFE0D0" },
27
+ { media: "(prefers-color-scheme: dark)", color: "#0C0C0C" },
28
+ ],
29
+ width: "device-width",
30
+ initialScale: 1,
31
+ maximumScale: 5,
32
+ };
33
+
34
  export const metadata: Metadata = {
35
+ metadataBase: new URL(SITE_URL),
36
+ title: {
37
+ default: SITE_TITLE,
38
+ template: "%s | Reuben Chagas Fernandes",
39
+ },
40
+ description: SITE_DESCRIPTION,
41
+ applicationName: SITE_NAME,
42
+ keywords: [
43
+ "Reuben Chagas Fernandes",
44
+ "Reuben Fernandes",
45
+ "Full Stack Developer",
46
+ "Web Developer",
47
+ "Next.js Developer",
48
+ "React Developer",
49
+ "Flutter Developer",
50
+ "AI Developer",
51
+ "LLM Engineer",
52
+ "Konkani AI",
53
+ "Goa Developer",
54
+ "India Developer",
55
+ "Software Engineer Goa",
56
+ "Hire Full Stack Developer India",
57
+ ],
58
+ authors: [{ name: SITE_NAME, url: SITE_URL }],
59
+ creator: SITE_NAME,
60
+ publisher: SITE_NAME,
61
+ alternates: {
62
+ canonical: "/",
63
+ },
64
+ category: "Technology",
65
+ formatDetection: {
66
+ email: false,
67
+ address: false,
68
+ telephone: false,
69
+ },
70
  robots: {
71
  index: true,
72
  follow: true,
73
+ nocache: false,
74
+ googleBot: {
75
+ index: true,
76
+ follow: true,
77
+ noimageindex: false,
78
+ "max-image-preview": "large",
79
+ "max-snippet": -1,
80
+ "max-video-preview": -1,
81
+ },
82
  },
83
  openGraph: {
84
+ title: SITE_TITLE,
85
+ description: SITE_DESCRIPTION,
86
+ url: SITE_URL,
87
+ siteName: SITE_NAME,
88
  type: "website",
89
  locale: "en_US",
90
  },
91
  twitter: {
92
  card: "summary_large_image",
93
+ title: SITE_TITLE,
94
+ description: SITE_DESCRIPTION,
95
+ creator: "@18reuchagas",
96
+ site: "@18reuchagas",
97
  },
98
+ verification: {
99
+ // Add Google Search Console verification token here when you set it up:
100
+ // google: "your-google-verification-code",
101
+ },
102
+ };
103
+
104
+ const personJsonLd = {
105
+ "@context": "https://schema.org",
106
+ "@type": "Person",
107
+ "@id": `${SITE_URL}/#person`,
108
+ name: SITE_NAME,
109
+ alternateName: "Reuben Fernandes",
110
+ jobTitle: "Full Stack Developer",
111
+ description: SITE_DESCRIPTION,
112
+ url: SITE_URL,
113
+ image: `${SITE_URL}/opengraph-image`,
114
+ email: "mailto:18reuchagasfernandes@gmail.com",
115
+ sameAs: [
116
+ "https://www.linkedin.com/in/reuben-chagas-fernandes/",
117
+ "https://x.com/18reuchagas",
118
+ ],
119
+ address: {
120
+ "@type": "PostalAddress",
121
+ addressLocality: "Goa",
122
+ addressCountry: "IN",
123
+ },
124
+ nationality: {
125
+ "@type": "Country",
126
+ name: "India",
127
+ },
128
+ knowsAbout: [
129
+ "React",
130
+ "Next.js",
131
+ "Flutter",
132
+ "Node.js",
133
+ "Python",
134
+ "TypeScript",
135
+ "Large Language Models",
136
+ "Artificial Intelligence",
137
+ "Konkani Language Technology",
138
+ "Full Stack Development",
139
+ "Web Development",
140
+ "UI/UX Design",
141
+ ],
142
+ knowsLanguage: ["English", "Konkani", "Hindi"],
143
+ };
144
+
145
+ const websiteJsonLd = {
146
+ "@context": "https://schema.org",
147
+ "@type": "WebSite",
148
+ "@id": `${SITE_URL}/#website`,
149
+ url: SITE_URL,
150
+ name: SITE_NAME,
151
+ description: SITE_DESCRIPTION,
152
+ publisher: { "@id": `${SITE_URL}/#person` },
153
+ inLanguage: "en-US",
154
+ };
155
+
156
+ const professionalServiceJsonLd = {
157
+ "@context": "https://schema.org",
158
+ "@type": "ProfessionalService",
159
+ "@id": `${SITE_URL}/#service`,
160
+ name: `${SITE_NAME} — Full Stack Development`,
161
+ url: SITE_URL,
162
+ image: `${SITE_URL}/opengraph-image`,
163
+ description:
164
+ "Full-stack web and mobile development services: React, Next.js, Flutter, Node.js, Python, and AI/LLM integration.",
165
+ provider: { "@id": `${SITE_URL}/#person` },
166
+ areaServed: { "@type": "Country", name: "Worldwide" },
167
+ serviceType: ["Web Development", "Mobile App Development", "AI Integration"],
168
  };
169
 
170
  export default function RootLayout({
 
177
  <body
178
  className={`${kanit.variable} ${jetbrainsMono.variable} font-sans antialiased`}
179
  >
180
+ <script
181
+ type="application/ld+json"
182
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
183
+ />
184
+ <script
185
+ type="application/ld+json"
186
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
187
+ />
188
+ <script
189
+ type="application/ld+json"
190
+ dangerouslySetInnerHTML={{
191
+ __html: JSON.stringify(professionalServiceJsonLd),
192
+ }}
193
+ />
194
  {children}
195
  </body>
196
  </html>
app/opengraph-image.tsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ImageResponse } from "next/og";
2
+
3
+ export const runtime = "edge";
4
+ export const alt = "Reuben Chagas Fernandes — Full Stack Developer";
5
+ export const size = { width: 1200, height: 630 };
6
+ export const contentType = "image/png";
7
+
8
+ export default async function OpengraphImage() {
9
+ return new ImageResponse(
10
+ (
11
+ <div
12
+ style={{
13
+ display: "flex",
14
+ flexDirection: "column",
15
+ width: "100%",
16
+ height: "100%",
17
+ background:
18
+ "linear-gradient(180deg, #FFE0D0 0%, #F15A24 55%, #C42D0B 100%)",
19
+ padding: "80px",
20
+ justifyContent: "space-between",
21
+ fontFamily: "sans-serif",
22
+ }}
23
+ >
24
+ <div
25
+ style={{
26
+ fontSize: 26,
27
+ color: "#1A0F08",
28
+ fontWeight: 600,
29
+ letterSpacing: "0.14em",
30
+ textTransform: "uppercase",
31
+ display: "flex",
32
+ }}
33
+ >
34
+ Hi, I&apos;m Reuben — Full-Stack Developer
35
+ </div>
36
+
37
+ <div
38
+ style={{
39
+ display: "flex",
40
+ flexDirection: "column",
41
+ lineHeight: 0.88,
42
+ }}
43
+ >
44
+ <div
45
+ style={{
46
+ fontSize: 170,
47
+ color: "#FFE0D0",
48
+ fontWeight: 900,
49
+ letterSpacing: "-0.02em",
50
+ }}
51
+ >
52
+ Reuben
53
+ </div>
54
+ <div
55
+ style={{
56
+ fontSize: 170,
57
+ color: "#FFE0D0",
58
+ fontWeight: 900,
59
+ letterSpacing: "-0.02em",
60
+ display: "flex",
61
+ }}
62
+ >
63
+ Fernandes
64
+ <span style={{ color: "#FF8C4C" }}>.</span>
65
+ </div>
66
+ </div>
67
+
68
+ <div
69
+ style={{
70
+ display: "flex",
71
+ justifyContent: "space-between",
72
+ alignItems: "flex-end",
73
+ fontSize: 24,
74
+ color: "#1A0F08",
75
+ fontWeight: 600,
76
+ letterSpacing: "0.14em",
77
+ textTransform: "uppercase",
78
+ }}
79
+ >
80
+ <div style={{ display: "flex" }}>Based in Goa, India</div>
81
+ <div style={{ display: "flex" }}>reuben-fernandes.xyz</div>
82
+ </div>
83
+ </div>
84
+ ),
85
+ { ...size }
86
+ );
87
+ }
app/sitemap.xml CHANGED
@@ -1,9 +1,27 @@
1
  <?xml version="1.0" encoding="UTF-8"?>
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
  <url>
4
- <loc>https://reubenfernandes.dev</loc>
5
- <lastmod>2025-01-27</lastmod>
6
  <changefreq>monthly</changefreq>
7
  <priority>1.0</priority>
8
  </url>
9
- </urlset>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <?xml version="1.0" encoding="UTF-8"?>
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
  <url>
4
+ <loc>https://reuben-fernandes.xyz/</loc>
5
+ <lastmod>2026-04-22</lastmod>
6
  <changefreq>monthly</changefreq>
7
  <priority>1.0</priority>
8
  </url>
9
+ <url>
10
+ <loc>https://reuben-fernandes.xyz/#about</loc>
11
+ <lastmod>2026-04-22</lastmod>
12
+ <changefreq>monthly</changefreq>
13
+ <priority>0.9</priority>
14
+ </url>
15
+ <url>
16
+ <loc>https://reuben-fernandes.xyz/#projects</loc>
17
+ <lastmod>2026-04-22</lastmod>
18
+ <changefreq>monthly</changefreq>
19
+ <priority>0.9</priority>
20
+ </url>
21
+ <url>
22
+ <loc>https://reuben-fernandes.xyz/#contact</loc>
23
+ <lastmod>2026-04-22</lastmod>
24
+ <changefreq>monthly</changefreq>
25
+ <priority>0.7</priority>
26
+ </url>
27
+ </urlset>
components/portfolio/ImageFillText.tsx ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useRef, useEffect, useCallback } from 'react';
4
+ import { useScroll } from 'motion/react';
5
+
6
+ const CANVAS_W = 700;
7
+ const CANVAS_H = Math.round(CANVAS_W * (9 / 16));
8
+ const FONT_SIZE = Math.round(CANVAS_W * 0.024);
9
+ const LINE_HEIGHT = FONT_SIZE * 1.65;
10
+ const PADDING_X = CANVAS_W * 0.08;
11
+ const MAX_TEXT_W = CANVAS_W - PADDING_X * 2;
12
+ const FONT_STR = `500 ${FONT_SIZE}px Kanit, sans-serif`;
13
+ const FALLBACK_FILL = '#FFE0D0';
14
+
15
+ function clamp(v: number, lo: number, hi: number) {
16
+ return Math.min(hi, Math.max(lo, v));
17
+ }
18
+
19
+ interface CharPos {
20
+ char: string;
21
+ x: number;
22
+ y: number;
23
+ index: number;
24
+ }
25
+
26
+ function buildPositions(ctx: CanvasRenderingContext2D, text: string): CharPos[] {
27
+ ctx.font = FONT_STR;
28
+
29
+ const words = text.split(' ');
30
+ const lines: string[] = [];
31
+ let line = '';
32
+
33
+ for (const word of words) {
34
+ const test = line ? `${line} ${word}` : word;
35
+ if (ctx.measureText(test).width > MAX_TEXT_W && line) {
36
+ lines.push(line);
37
+ line = word;
38
+ } else {
39
+ line = test;
40
+ }
41
+ }
42
+ if (line) lines.push(line);
43
+
44
+ const totalH = lines.length * LINE_HEIGHT;
45
+ let y = (CANVAS_H - totalH) / 2;
46
+ const positions: CharPos[] = [];
47
+ let idx = 0;
48
+
49
+ for (const lineText of lines) {
50
+ const lineW = ctx.measureText(lineText).width;
51
+ let x = (CANVAS_W - lineW) / 2;
52
+ for (const char of lineText) {
53
+ positions.push({ char, x, y, index: idx++ });
54
+ x += ctx.measureText(char).width;
55
+ }
56
+ y += LINE_HEIGHT;
57
+ }
58
+
59
+ return positions;
60
+ }
61
+
62
+ export function ImageFillText({ text, imageSrc }: { text: string; imageSrc: string }) {
63
+ const containerRef = useRef<HTMLDivElement>(null);
64
+ const mainCanvasRef = useRef<HTMLCanvasElement>(null);
65
+ const maskCanvasRef = useRef<HTMLCanvasElement>(null);
66
+ const imageRef = useRef<HTMLImageElement | null>(null);
67
+ const scrollRef = useRef(0);
68
+ const charPosRef = useRef<CharPos[]>([]);
69
+
70
+ const { scrollYProgress } = useScroll({
71
+ target: containerRef,
72
+ offset: ['start 0.8', 'end 0.2'],
73
+ });
74
+
75
+ const drawFrame = useCallback(() => {
76
+ const main = mainCanvasRef.current;
77
+ const mask = maskCanvasRef.current;
78
+ if (!main || !mask) return;
79
+
80
+ const mCtx = main.getContext('2d');
81
+ const maskCtx = mask.getContext('2d');
82
+ if (!mCtx || !maskCtx) return;
83
+
84
+ if (charPosRef.current.length === 0) {
85
+ charPosRef.current = buildPositions(maskCtx, text);
86
+ }
87
+
88
+ const chars = charPosRef.current;
89
+ const total = chars.length;
90
+ const progress = scrollRef.current;
91
+
92
+ // Mask canvas: transparent bg, white text at scroll-driven alpha
93
+ maskCtx.clearRect(0, 0, CANVAS_W, CANVAS_H);
94
+ maskCtx.globalCompositeOperation = 'source-over';
95
+ maskCtx.font = FONT_STR;
96
+ maskCtx.textBaseline = 'top';
97
+ maskCtx.fillStyle = 'white';
98
+
99
+ for (const { char, x, y, index } of chars) {
100
+ const start = index / total;
101
+ const end = Math.min(start + 2 / total, 1);
102
+ const range = Math.max(end - start, 0.001);
103
+ maskCtx.globalAlpha = clamp((progress - start) / range, 0.15, 1);
104
+ maskCtx.fillText(char, x, y);
105
+ }
106
+ maskCtx.globalAlpha = 1;
107
+
108
+ // Main canvas: draw image (or fallback fill), clip to mask
109
+ mCtx.clearRect(0, 0, CANVAS_W, CANVAS_H);
110
+ const img = imageRef.current;
111
+ if (img && img.naturalWidth > 0) {
112
+ mCtx.drawImage(img, 0, 0, CANVAS_W, CANVAS_H);
113
+ } else {
114
+ mCtx.fillStyle = FALLBACK_FILL;
115
+ mCtx.fillRect(0, 0, CANVAS_W, CANVAS_H);
116
+ }
117
+ mCtx.globalCompositeOperation = 'destination-in';
118
+ mCtx.drawImage(mask, 0, 0, CANVAS_W, CANVAS_H);
119
+ mCtx.globalCompositeOperation = 'source-over';
120
+ }, [text]);
121
+
122
+ // Preload image and draw once ready
123
+ useEffect(() => {
124
+ const img = new Image();
125
+ const handleLoad = () => {
126
+ imageRef.current = img;
127
+ drawFrame();
128
+ };
129
+ const handleError = () => {
130
+ imageRef.current = null;
131
+ drawFrame();
132
+ };
133
+ img.onload = handleLoad;
134
+ img.onerror = handleError;
135
+ img.src = imageSrc;
136
+ if (img.complete && img.naturalWidth > 0) handleLoad();
137
+ return () => {
138
+ img.onload = null;
139
+ img.onerror = null;
140
+ };
141
+ }, [imageSrc, drawFrame]);
142
+
143
+ // Redraw on scroll change instead of continuous rAF
144
+ useEffect(() => {
145
+ const unsub = scrollYProgress.on('change', (v) => {
146
+ scrollRef.current = v;
147
+ drawFrame();
148
+ });
149
+ drawFrame(); // initial render
150
+ return unsub;
151
+ }, [scrollYProgress, drawFrame]);
152
+
153
+ return (
154
+ <div
155
+ ref={containerRef}
156
+ style={{
157
+ position: 'relative',
158
+ aspectRatio: '16/9',
159
+ maxWidth: CANVAS_W,
160
+ margin: '0 auto',
161
+ width: '100%',
162
+ }}
163
+ >
164
+ <canvas ref={maskCanvasRef} width={CANVAS_W} height={CANVAS_H} style={{ display: 'none' }} />
165
+ <canvas
166
+ ref={mainCanvasRef}
167
+ width={CANVAS_W}
168
+ height={CANVAS_H}
169
+ style={{ width: '100%', height: '100%', display: 'block' }}
170
+ />
171
+ </div>
172
+ );
173
+ }
components/portfolio/sections/AboutSection.tsx CHANGED
@@ -3,9 +3,13 @@
3
  import React from 'react';
4
  import Image from 'next/image';
5
  import { motion } from 'motion/react';
6
- import { AnimatedText } from '../AnimatedText';
7
- import { ContactButton } from '../ContactButton';
8
  import { FadeIn } from '../FadeIn';
 
 
 
 
 
 
9
 
10
  export function AboutSection() {
11
  return (
@@ -46,22 +50,21 @@ export function AboutSection() {
46
  </motion.div>
47
  </FadeIn>
48
 
49
- <FadeIn y={30} duration={0.8}>
50
- <h2 className="font-black uppercase leading-none tracking-tight text-center text-[clamp(3rem,12vw,160px)] inline-block text-transparent bg-clip-text bg-gradient-to-b from-[#FFE0D0] to-[#FF8C4C] mb-10 sm:mb-14 md:mb-16">
51
- About me
52
- </h2>
53
- </FadeIn>
 
54
 
55
- <div className="max-w-[600px] text-center mb-16 sm:mb-20 md:mb-24">
56
- <AnimatedText
57
- text="I'm a Computer Engineering graduate who loves building for the web. My toolkit includes React Next.js Flutter Node.js and Python. Right now I am exploring AI and building things with it. I am also into human languages which is why I built a Konkani LLM to bring my mother tongue into the future. If you have something worth building I'd love to make it with you."
58
- className="text-[#D7E2EA] font-medium leading-relaxed text-[clamp(1rem,2vw,1.35rem)]"
59
- />
60
  </div>
61
 
62
- <FadeIn y={20} delay={0.4} duration={0.8}>
63
- <ContactButton />
64
- </FadeIn>
 
 
65
  </section>
66
  );
67
  }
 
3
  import React from 'react';
4
  import Image from 'next/image';
5
  import { motion } from 'motion/react';
 
 
6
  import { FadeIn } from '../FadeIn';
7
+ import { ContactButton } from '../ContactButton';
8
+ import { ImageFillText } from '../ImageFillText';
9
+
10
+ const ABOUT_IMAGE_URL = '/about.png';
11
+ const ABOUT_TEXT =
12
+ "I'm a Computer Science and Engineering graduate passionate about full-stack development, AI, and UI/UX design. I work across React, Next.js, Flutter, Node.js, and Python, with a growing focus on Large Language Models and AI research. I'm also building a Konkani language project to preserve my native tongue through technology. Let's build something incredible together!";
13
 
14
  export function AboutSection() {
15
  return (
 
50
  </motion.div>
51
  </FadeIn>
52
 
53
+ <div className="flex flex-col items-center w-full gap-10 sm:gap-14 md:gap-16">
54
+ <FadeIn y={40} duration={0.8}>
55
+ <h2 className="font-black uppercase leading-none tracking-tight text-center text-[clamp(3rem,12vw,160px)] inline-block text-transparent bg-clip-text bg-gradient-to-b from-[#FFE0D0] to-[#FF8C4C]">
56
+ About me
57
+ </h2>
58
+ </FadeIn>
59
 
60
+ <ImageFillText text={ABOUT_TEXT} imageSrc={ABOUT_IMAGE_URL} />
 
 
 
 
61
  </div>
62
 
63
+ <div className="mt-16 sm:mt-20 md:mt-24">
64
+ <FadeIn y={20} delay={0.4} duration={0.8}>
65
+ <ContactButton />
66
+ </FadeIn>
67
+ </div>
68
  </section>
69
  );
70
  }
components/portfolio/sections/ContactSection.tsx CHANGED
@@ -4,6 +4,8 @@ import React from 'react';
4
  import { FadeIn } from '../FadeIn';
5
  import { Mail, Linkedin, Twitter } from 'lucide-react';
6
 
 
 
7
  export function ContactSection() {
8
  return (
9
  <section id="contact" className="bg-[#FFE0D0] rounded-t-[40px] sm:rounded-t-[50px] md:rounded-t-[60px] -mt-10 sm:-mt-12 md:-mt-14 relative z-30 px-5 sm:px-8 md:px-10 py-20 sm:py-24 md:py-32">
@@ -13,79 +15,35 @@ export function ContactSection() {
13
  </h2>
14
  </FadeIn>
15
 
16
- <div className="max-w-6xl mx-auto flex flex-col md:flex-row gap-16 md:gap-10">
17
- <div className="flex-1 flex flex-col gap-10">
18
- <FadeIn delay={0.2} duration={0.8}>
19
- <p className="text-[#1A0F08] font-medium text-[clamp(1.5rem,3vw,2.5rem)] leading-tight max-w-md">
20
- Have a project in mind? Let&apos;s build something incredible together.
21
- </p>
22
- </FadeIn>
23
- <FadeIn delay={0.4} duration={0.8}>
24
- <div className="flex flex-col gap-6">
25
- <a href="mailto:18reuchagasfernandes@gmail.com" className="flex items-center gap-4 text-[#1A0F08] hover:text-[#E63F19] transition-colors group">
26
- <div className="w-12 h-12 rounded-full border-2 border-[#1A0F08] flex items-center justify-center group-hover:border-[#E63F19] transition-colors">
27
- <Mail size={20} />
28
- </div>
29
- <span className="font-medium uppercase tracking-wider text-sm sm:text-base">18reuchagasfernandes@gmail.com</span>
30
- </a>
31
- <a href="https://www.linkedin.com/in/reuben-chagas-fernandes/" target="_blank" rel="noreferrer" className="flex items-center gap-4 text-[#1A0F08] hover:text-[#E63F19] transition-colors group">
32
- <div className="w-12 h-12 rounded-full border-2 border-[#1A0F08] flex items-center justify-center group-hover:border-[#E63F19] transition-colors">
33
- <Linkedin size={20} />
34
- </div>
35
- <span className="font-medium uppercase tracking-wider text-sm sm:text-base">LinkedIn</span>
36
- </a>
37
- <a href="https://x.com/18reuchagas" target="_blank" rel="noreferrer" className="flex items-center gap-4 text-[#1A0F08] hover:text-[#E63F19] transition-colors group">
38
- <div className="w-12 h-12 rounded-full border-2 border-[#1A0F08] flex items-center justify-center group-hover:border-[#E63F19] transition-colors">
39
- <Twitter size={20} />
40
- </div>
41
- <span className="font-medium uppercase tracking-wider text-sm sm:text-base">Twitter / X</span>
42
- </a>
43
- </div>
44
- </FadeIn>
45
- </div>
46
 
47
- <div className="flex-1">
48
- <FadeIn delay={0.6} duration={0.8}>
49
- <form className="flex flex-col gap-8 sm:gap-10" onSubmit={(e) => e.preventDefault()}>
50
- <div className="relative">
51
- <input
52
- type="text"
53
- id="name"
54
- placeholder="Your Name"
55
- className="w-full bg-transparent border-b-2 border-[rgba(26,15,8,0.2)] py-4 text-[#1A0F08] placeholder:text-[#1A0F08]/50 outline-none focus:border-[#E63F19] transition-colors peer text-lg font-medium"
56
- required
57
- />
58
  </div>
59
- <div className="relative">
60
- <input
61
- type="email"
62
- id="email"
63
- placeholder="Your Email"
64
- className="w-full bg-transparent border-b-2 border-[rgba(26,15,8,0.2)] py-4 text-[#1A0F08] placeholder:text-[#1A0F08]/50 outline-none focus:border-[#E63F19] transition-colors peer text-lg font-medium"
65
- required
66
- />
67
  </div>
68
- <div className="relative">
69
- <textarea
70
- id="message"
71
- placeholder="Your Message..."
72
- rows={4}
73
- className="w-full bg-transparent border-b-2 border-[rgba(26,15,8,0.2)] py-4 text-[#1A0F08] placeholder:text-[#1A0F08]/50 outline-none focus:border-[#E63F19] transition-colors peer text-lg font-medium resize-none"
74
- required
75
- ></textarea>
76
  </div>
77
- <button
78
- type="submit"
79
- className="self-start relative group overflow-hidden rounded-full border-2 border-[#1A0F08] px-8 py-4 sm:px-10 sm:py-4 transition-colors"
80
- >
81
- <div className="absolute inset-0 bg-[#E63F19] translate-y-full group-hover:translate-y-0 transition-transform duration-300 ease-in-out z-0"></div>
82
- <span className="relative z-10 text-[#1A0F08] font-bold uppercase tracking-widest text-sm sm:text-base group-hover:text-[#FFE0D0] transition-colors duration-300">
83
- Submit Message
84
- </span>
85
- </button>
86
- </form>
87
- </FadeIn>
88
- </div>
89
  </div>
90
  </section>
91
  );
 
4
  import { FadeIn } from '../FadeIn';
5
  import { Mail, Linkedin, Twitter } from 'lucide-react';
6
 
7
+ const CONTACT_EMAIL = '18reuchagasfernandes@gmail.com';
8
+
9
  export function ContactSection() {
10
  return (
11
  <section id="contact" className="bg-[#FFE0D0] rounded-t-[40px] sm:rounded-t-[50px] md:rounded-t-[60px] -mt-10 sm:-mt-12 md:-mt-14 relative z-30 px-5 sm:px-8 md:px-10 py-20 sm:py-24 md:py-32">
 
15
  </h2>
16
  </FadeIn>
17
 
18
+ <div className="max-w-xl mx-auto flex flex-col items-center gap-10">
19
+ <FadeIn delay={0.2} duration={0.8}>
20
+ <p className="text-[#1A0F08] font-medium text-[clamp(1.5rem,3vw,2.5rem)] leading-tight text-center">
21
+ Have a project in mind? Let&apos;s build something incredible together.
22
+ </p>
23
+ </FadeIn>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ <FadeIn delay={0.4} duration={0.8}>
26
+ <div className="flex flex-col gap-6">
27
+ <a href={`mailto:${CONTACT_EMAIL}`} className="flex items-center gap-4 text-[#1A0F08] hover:text-[#E63F19] transition-colors group">
28
+ <div className="w-12 h-12 rounded-full border-2 border-[#1A0F08] flex items-center justify-center group-hover:border-[#E63F19] transition-colors">
29
+ <Mail size={20} />
 
 
 
 
 
 
30
  </div>
31
+ <span className="font-medium uppercase tracking-wider text-sm sm:text-base">{CONTACT_EMAIL}</span>
32
+ </a>
33
+ <a href="https://www.linkedin.com/in/reuben-chagas-fernandes/" target="_blank" rel="noreferrer" className="flex items-center gap-4 text-[#1A0F08] hover:text-[#E63F19] transition-colors group">
34
+ <div className="w-12 h-12 rounded-full border-2 border-[#1A0F08] flex items-center justify-center group-hover:border-[#E63F19] transition-colors">
35
+ <Linkedin size={20} />
 
 
 
36
  </div>
37
+ <span className="font-medium uppercase tracking-wider text-sm sm:text-base">LinkedIn</span>
38
+ </a>
39
+ <a href="https://x.com/18reuchagas" target="_blank" rel="noreferrer" className="flex items-center gap-4 text-[#1A0F08] hover:text-[#E63F19] transition-colors group">
40
+ <div className="w-12 h-12 rounded-full border-2 border-[#1A0F08] flex items-center justify-center group-hover:border-[#E63F19] transition-colors">
41
+ <Twitter size={20} />
 
 
 
42
  </div>
43
+ <span className="font-medium uppercase tracking-wider text-sm sm:text-base">Twitter / X</span>
44
+ </a>
45
+ </div>
46
+ </FadeIn>
 
 
 
 
 
 
 
 
47
  </div>
48
  </section>
49
  );
components/portfolio/sections/HeroSection.tsx CHANGED
@@ -38,9 +38,8 @@ export function HeroSection() {
38
 
39
  <nav className="flex w-full md:w-auto justify-between md:justify-center flex-1 gap-2 sm:gap-6 md:gap-8">
40
  <a href="#about" className="text-[#1A0F08] font-black md:font-medium uppercase tracking-wider text-[11px] sm:text-xs md:text-sm hover:opacity-60 transition-opacity duration-200">About</a>
41
- <a href="#projects" className="text-[#1A0F08] font-black md:font-medium uppercase tracking-wider text-[11px] sm:text-xs md:text-sm hover:opacity-60 transition-opacity duration-200 underline underline-offset-4 decoration-2">Projects</a>
42
- <a href="#resume" className="text-[#1A0F08] font-black md:font-medium uppercase tracking-wider text-[11px] sm:text-xs md:text-sm hover:opacity-60 transition-opacity duration-200">Resume</a>
43
- <a href="#contact" className="text-[#1A0F08] font-black md:font-medium uppercase tracking-wider text-[11px] sm:text-xs md:text-sm hover:opacity-60 transition-opacity duration-200">Contact</a>
44
  </nav>
45
 
46
  <div className="hidden md:flex flex-1 justify-end">
@@ -67,7 +66,6 @@ export function HeroSection() {
67
  <FadeIn delay={0.6} y={20} duration={0.7} className="md:hidden mt-4">
68
  <div className="flex flex-col items-start gap-1">
69
  <p className="text-[#FFE0D0] font-medium text-[10px] uppercase tracking-wider opacity-80 drop-shadow-md">Based in Goa, India</p>
70
- <p className="text-[#FFE0D0] font-medium text-[10px] uppercase tracking-wider opacity-80 drop-shadow-md">Available worldwide</p>
71
  </div>
72
  </FadeIn>
73
  </div>
@@ -93,7 +91,6 @@ export function HeroSection() {
93
  <FadeIn delay={0.6} y={20} duration={0.7} className="hidden md:flex z-10 absolute bottom-20 right-10">
94
  <div className="flex flex-col items-end gap-1">
95
  <p className="text-[#FFE0D0] font-medium text-sm uppercase tracking-wider text-right opacity-90 drop-shadow-md">Based in Goa, India</p>
96
- <p className="text-[#FFE0D0] font-medium text-sm uppercase tracking-wider text-right opacity-90 drop-shadow-md">Available worldwide</p>
97
  </div>
98
  </FadeIn>
99
  </section>
 
38
 
39
  <nav className="flex w-full md:w-auto justify-between md:justify-center flex-1 gap-2 sm:gap-6 md:gap-8">
40
  <a href="#about" className="text-[#1A0F08] font-black md:font-medium uppercase tracking-wider text-[11px] sm:text-xs md:text-sm hover:opacity-60 transition-opacity duration-200">About</a>
41
+ <a href="#projects" className="text-[#1A0F08] font-black md:font-medium uppercase tracking-wider text-[11px] sm:text-xs md:text-sm hover:opacity-60 transition-opacity duration-200">Projects</a>
42
+ <a href="/resume.pdf" target="_blank" rel="noreferrer" className="text-[#1A0F08] font-black md:font-medium uppercase tracking-wider text-[11px] sm:text-xs md:text-sm hover:opacity-60 transition-opacity duration-200">Resume</a>
 
43
  </nav>
44
 
45
  <div className="hidden md:flex flex-1 justify-end">
 
66
  <FadeIn delay={0.6} y={20} duration={0.7} className="md:hidden mt-4">
67
  <div className="flex flex-col items-start gap-1">
68
  <p className="text-[#FFE0D0] font-medium text-[10px] uppercase tracking-wider opacity-80 drop-shadow-md">Based in Goa, India</p>
 
69
  </div>
70
  </FadeIn>
71
  </div>
 
91
  <FadeIn delay={0.6} y={20} duration={0.7} className="hidden md:flex z-10 absolute bottom-20 right-10">
92
  <div className="flex flex-col items-end gap-1">
93
  <p className="text-[#FFE0D0] font-medium text-sm uppercase tracking-wider text-right opacity-90 drop-shadow-md">Based in Goa, India</p>
 
94
  </div>
95
  </FadeIn>
96
  </section>
components/portfolio/sections/MarqueeSection.tsx CHANGED
@@ -2,12 +2,8 @@
2
  /* eslint-disable @next/next/no-img-element */
3
 
4
  import React, { useEffect, useState } from 'react';
5
- import OpenAI from '@lobehub/icons/es/OpenAI/components/Mono';
6
- import Anthropic from '@lobehub/icons/es/Anthropic/components/Mono';
7
  import Claude from '@lobehub/icons/es/Claude/components/Mono';
8
- import Gemini from '@lobehub/icons/es/Gemini/components/Mono';
9
  import HuggingFace from '@lobehub/icons/es/HuggingFace/components/Mono';
10
- import Ollama from '@lobehub/icons/es/Ollama/components/Mono';
11
 
12
  type TechItem = {
13
  name: string;
@@ -28,12 +24,10 @@ const row1: TechItem[] = [
28
  const row2: TechItem[] = [
29
  { name: 'Firebase', icon: <img src="https://cdn.simpleicons.org/firebase/D7E2EA" alt="Firebase" referrerPolicy="no-referrer" className="w-14 h-14" /> },
30
  { name: 'MongoDB', icon: <img src="https://cdn.simpleicons.org/mongodb/D7E2EA" alt="MongoDB" referrerPolicy="no-referrer" className="w-14 h-14" /> },
31
- { name: 'OpenAI', icon: <OpenAI size={56} style={{ color: '#D7E2EA' }} /> },
32
- { name: 'Anthropic', icon: <Anthropic size={56} style={{ color: '#D7E2EA' }} /> },
33
  { name: 'Claude', icon: <Claude size={56} style={{ color: '#D7E2EA' }} /> },
34
- { name: 'Gemini', icon: <Gemini size={56} style={{ color: '#D7E2EA' }} /> },
35
  { name: 'Hugging Face', icon: <HuggingFace size={56} style={{ color: '#D7E2EA' }} /> },
36
- { name: 'Ollama', icon: <Ollama size={56} style={{ color: '#D7E2EA' }} /> },
37
  ];
38
 
39
  export function MarqueeSection() {
 
2
  /* eslint-disable @next/next/no-img-element */
3
 
4
  import React, { useEffect, useState } from 'react';
 
 
5
  import Claude from '@lobehub/icons/es/Claude/components/Mono';
 
6
  import HuggingFace from '@lobehub/icons/es/HuggingFace/components/Mono';
 
7
 
8
  type TechItem = {
9
  name: string;
 
24
  const row2: TechItem[] = [
25
  { name: 'Firebase', icon: <img src="https://cdn.simpleicons.org/firebase/D7E2EA" alt="Firebase" referrerPolicy="no-referrer" className="w-14 h-14" /> },
26
  { name: 'MongoDB', icon: <img src="https://cdn.simpleicons.org/mongodb/D7E2EA" alt="MongoDB" referrerPolicy="no-referrer" className="w-14 h-14" /> },
 
 
27
  { name: 'Claude', icon: <Claude size={56} style={{ color: '#D7E2EA' }} /> },
28
+ { name: 'Docker', icon: <img src="https://cdn.simpleicons.org/docker/D7E2EA" alt="Docker" referrerPolicy="no-referrer" className="w-14 h-14" /> },
29
  { name: 'Hugging Face', icon: <HuggingFace size={56} style={{ color: '#D7E2EA' }} /> },
30
+ { name: 'PostgreSQL', icon: <img src="https://cdn.simpleicons.org/postgresql/D7E2EA" alt="PostgreSQL" referrerPolicy="no-referrer" className="w-14 h-14" /> },
31
  ];
32
 
33
  export function MarqueeSection() {
components/portfolio/sections/ProjectsSection.tsx CHANGED
@@ -14,19 +14,29 @@ type Project = {
14
  const projects: Project[] = [
15
  {
16
  id: '01',
17
- title: 'Konkani Language Project (Personal)',
18
  videoId: 'dQw4w9WgXcQ',
19
  },
20
  {
21
  id: '02',
22
- title: 'AI / LLM Research Project (Personal)',
23
  videoId: 'M7lc1UVf-VE',
24
  },
25
  {
26
  id: '03',
27
- title: 'Android App (Professional)',
28
  videoId: 'jNQXAC9IVRw',
29
  },
 
 
 
 
 
 
 
 
 
 
30
  ];
31
 
32
  export function ProjectsSection() {
 
14
  const projects: Project[] = [
15
  {
16
  id: '01',
17
+ title: 'Konkani AI',
18
  videoId: 'dQw4w9WgXcQ',
19
  },
20
  {
21
  id: '02',
22
+ title: 'Tiny AYA Mobile',
23
  videoId: 'M7lc1UVf-VE',
24
  },
25
  {
26
  id: '03',
27
+ title: 'PowerPoint AI',
28
  videoId: 'jNQXAC9IVRw',
29
  },
30
+ {
31
+ id: '04',
32
+ title: 'Multi Model Dataset',
33
+ videoId: 'dQw4w9WgXcQ',
34
+ },
35
+ {
36
+ id: '05',
37
+ title: 'ISO Language',
38
+ videoId: 'M7lc1UVf-VE',
39
+ },
40
  ];
41
 
42
  export function ProjectsSection() {
public/about.png ADDED

Git LFS Details

  • SHA256: bef3b1a737a0043e3a18ea07744ff58b8f4436381b0c7e9df466888408cbd1bb
  • Pointer size: 132 Bytes
  • Size of remote file: 1.15 MB
public/robots.txt CHANGED
@@ -2,7 +2,7 @@ User-agent: *
2
  Allow: /
3
 
4
  # Sitemap
5
- Sitemap: https://reubenfernandes.dev/sitemap.xml
6
 
7
  # Specific crawl instructions for major search engines
8
  User-agent: Googlebot
 
2
  Allow: /
3
 
4
  # Sitemap
5
+ Sitemap: https://reuben-fernandes.xyz/sitemap.xml
6
 
7
  # Specific crawl instructions for major search engines
8
  User-agent: Googlebot