Skip to content

Commit d039876

Browse files
committed
feat: Revamp contact page and enhance resume handling; removed redundant form, added availability status, response info, collaboration interests, and improved resume URL management
Signed-off-by: VKrishna04 <75069043+VKrishna04@users.noreply.github.com>
1 parent c36b3db commit d039876

File tree

8 files changed

+155
-42
lines changed

8 files changed

+155
-42
lines changed
File renamed without changes.

docs/favicon-navbar-config-examples.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Example configuration for dynamic favicon and navbar logo
3-
*
3+
*
44
* This file shows how to configure the new dynamic favicon and navbar features
55
*/
66

@@ -10,14 +10,14 @@
1010
"type": "github",
1111
"customUrl": "https://github.com/VKrishna04.png",
1212
"githubUsername": "VKrishna04",
13-
"sizes": ["16x16", "32x32", "96x96", "192x192", "512x512"],
13+
"sizes": ["16x16", "32x32", "96x96", "192x192", "512x512"](),
1414
"appleTouchIcon": true
1515
},
1616
"navbar": {
1717
"logo": {
1818
"type": "github",
1919
"text": "VK",
20-
"name": "Krishna GSVV",
20+
"name": "Krishna GSVV",
2121
"showName": true,
2222
"showNameOnMobile": false,
2323
"gradient": "from-purple-500 to-pink-500",
@@ -34,7 +34,7 @@
3434
"favicon": {
3535
"type": "custom",
3636
"customUrl": "/custom-favicon.png",
37-
"sizes": ["16x16", "32x32", "96x96"],
37+
"sizes": ["16x16", "32x32", "96x96"](),
3838
"appleTouchIcon": true
3939
},
4040
"navbar": {
@@ -73,4 +73,4 @@
7373
"gradient": "from-purple-500 to-pink-500"
7474
}
7575
}
76-
}
76+
}

public/settings.json

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -290,10 +290,10 @@
290290
},
291291
"resume": {
292292
"type": "file",
293-
"url": "https://drive.google.com/file/d/1ZlGEECwaC-q7mGAhXqF6nLgpgYYgUroQ/view?usp=sharing",
293+
"url": "https://drive.google.com/file/d/1a7av243LfnlHaVpSRzXciy0Kv1FfYPVP/view?usp=sharing",
294294
"filename": "Krishna_GSVV_Resume.pdf",
295-
"alternativeUrl": "",
296-
"note": "Set type to 'file' to serve from public folder, or 'external' to use alternativeUrl (Google Drive, Dropbox, etc.)",
295+
"alternativeUrl": "https://drive.google.com/file/d/1a7av243LfnlHaVpSRzXciy0Kv1FfYPVP/view?usp=sharing",
296+
"note": "Resume URL - can be a relative path (for local files) or a full URL (for external). If the RESUME_LINK environment variable is set, it will override this value.",
297297
"sectionOrder": [
298298
"experiences",
299299
"education",
@@ -468,6 +468,14 @@
468468
"skillsHeading": "Technical Skills",
469469
"skillsIcon": "CpuChipIcon",
470470
"certifications": [
471+
{
472+
"name": "AWS Certified Cloud Practitioner",
473+
"certificateUrl": "https://drive.google.com/file/d/1RoyflDxiOjvilKKqSdrkdduH85YeMIs1/view?usp=sharing",
474+
"credentialId": "a6d782dae67d4865980a627800cd6a14",
475+
"date": "August 27, 2025 - 2028",
476+
"issuer": "Amazon Web Services",
477+
"verificationUrl": "https://cp.certmetrics.com/amazon/en/public/verify/credential/a6d782dae67d4865980a627800cd6a14"
478+
},
471479
{
472480
"name": "Oracle Cloud Infrastructure 2025 Certified DevOps Professional",
473481
"certificateUrl": "https://drive.google.com/file/d/1agdxuPZMtTZKG2EATzEp_WKyLCkbx4uL/view?usp=drive_link",
@@ -484,12 +492,6 @@
484492
"issuer": "Oracle University",
485493
"verificationUrl": "https://catalog-education.oracle.com/ords/certview/sharebadge?id=D7A5B1169CBBF5293E9E216E889A16D9E28346FC8CC006E736CAE94691F767F4"
486494
},
487-
{
488-
"name": "AWS Cloud Practitioner",
489-
"certificateUrl": "https://drive.google.com/file/d/1S0YHD-RkG5w2r-iZwEOiq4EkrJIqDnfy/view?usp=sharing",
490-
"date": "March 20, 2025",
491-
"issuer": "Corporate and Industry Relations (CIR), Amrita Vishwa Vidyapeetham, Coimbatore."
492-
},
493495
{
494496
"name": "Oracle Cloud Infrastructure 2025 Certified Foundations Associate",
495497
"certificateUrl": "https://drive.google.com/file/d/1g03w4xyfmwqlIzUT2JDPrZgkHbgGM7AK/view?usp=sharing",

public/settings.schema.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,7 +1313,7 @@
13131313
},
13141314
"resume": {
13151315
"type": "object",
1316-
"description": "Resume configuration and content management",
1316+
"description": "Resume configuration and content management. The resume URL can be set via the 'url' property, 'alternativeUrl', or by setting the RESUME_LINK environment variable. If RESUME_LINK is set, it takes priority.",
13171317
"additionalProperties": true,
13181318
"properties": {
13191319
"type": {
@@ -1322,11 +1322,11 @@
13221322
"file",
13231323
"external"
13241324
],
1325-
"description": "Resume source type - 'file' for local PDF, 'external' for external URL"
1325+
"description": "Resume source type - 'file' for local PDF, 'external' for external URL."
13261326
},
13271327
"url": {
13281328
"type": "string",
1329-
"description": "Resume URL - can be relative path (for local files) or full URL (for external)",
1329+
"description": "Resume URL - can be a relative path (for local files) or a full URL (for external). If the RESUME_LINK environment variable is set, it will override this value.",
13301330
"examples": [
13311331
"https://example.com/resume.pdf",
13321332
"/resume.pdf",
@@ -1336,7 +1336,7 @@
13361336
"filename": {
13371337
"type": "string",
13381338
"pattern": "^[a-zA-Z0-9._-]+\\.(pdf|docx)$",
1339-
"description": "Local resume filename (required when type is 'file')",
1339+
"description": "Local resume filename (required when type is 'file').",
13401340
"examples": [
13411341
"resume.pdf",
13421342
"john-doe-resume.pdf"

src/components/Footer.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ const Footer = () => {
238238
<a
239239
key={link.path}
240240
href={link.path}
241+
target="_blank"
242+
rel="noopener noreferrer"
241243
className={`block text-sm transition-colors duration-300 ${
242244
styling.textColor || "text-gray-400"
243245
} hover:text-purple-400 hover:translate-x-1 transform transition-transform`}

src/pages/Contact.jsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
SiVercel,
5454
SiHeroku,
5555
} from "react-icons/si";
56+
import { getResumeUrl } from "../utils/resume";
5657

5758
const Contact = () => {
5859
const [settings, setSettings] = useState({});
@@ -155,6 +156,8 @@ const Contact = () => {
155156
{info.href ? (
156157
<a
157158
href={sanitizeUrl(info.href)}
159+
target="_blank"
160+
rel="noopener noreferrer"
158161
className="text-white hover:text-purple-400 transition-colors"
159162
>
160163
{info.value}
@@ -232,27 +235,37 @@ const Contact = () => {
232235
const IconComponent = getHeroIcon(action.icon);
233236
let url = action.url;
234237

235-
// Replace placeholders
238+
// If this is the Resume quick action and mode is 'auto', use shared resume URL
239+
const isResumeAction =
240+
action.key === "resume" ||
241+
action.label?.toLowerCase().includes("resume");
236242
if (
237-
url.includes("{email}") &&
238-
settings.social?.contact?.email
243+
isResumeAction &&
244+
settings.contact?.quickActions?.resumeMode === "auto"
239245
) {
240-
url = url.replace("{email}", settings.social.contact.email);
241-
}
242-
if (url.includes("{calendly}") && contactConfig.calendly) {
243-
url = url.replace("{calendly}", contactConfig.calendly);
246+
url = resumeUrl;
247+
} else {
248+
// Replace placeholders as before
249+
if (
250+
url.includes("{email}") &&
251+
settings.social?.contact?.email
252+
) {
253+
url = url.replace(
254+
"{email}",
255+
settings.social.contact.email
256+
);
257+
}
258+
if (url.includes("{calendly}") && contactConfig.calendly) {
259+
url = url.replace("{calendly}", contactConfig.calendly);
260+
}
244261
}
245262

246263
return (
247264
<a
248265
key={index}
249266
href={sanitizeUrl(url)}
250-
target={url.startsWith("http") ? "_blank" : undefined}
251-
rel={
252-
url.startsWith("http")
253-
? "noopener noreferrer"
254-
: undefined
255-
}
267+
target="_blank"
268+
rel="noopener noreferrer"
256269
className={`flex items-center p-2 ${
257270
colorMap[action.colorTheme] ||
258271
"text-gray-400 hover:bg-gray-900/20"
@@ -660,6 +673,7 @@ const Contact = () => {
660673
const collaborationInterests = getCollaborationInterests();
661674
const faqItems = getFAQItems();
662675
const contactInfo = getContactInfo();
676+
const resumeUrl = getResumeUrl(settings);
663677

664678
return (
665679
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 py-20 px-4">

src/pages/Resume.jsx

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,26 +74,48 @@ const Resume = () => {
7474
}, []);
7575

7676
const getResumeUrl = () => {
77+
// 1. Check for environment variable (RESUME_LINK)
78+
const envResumeUrl = import.meta.env.RESUME_LINK;
79+
if (
80+
envResumeUrl &&
81+
typeof envResumeUrl === "string" &&
82+
envResumeUrl.trim() !== ""
83+
) {
84+
try {
85+
if (
86+
envResumeUrl.startsWith("http://") ||
87+
envResumeUrl.startsWith("https://")
88+
) {
89+
return new URL(envResumeUrl).href;
90+
}
91+
if (envResumeUrl.startsWith("/") || !envResumeUrl.includes(":")) {
92+
if (
93+
envResumeUrl.includes("javascript:") ||
94+
envResumeUrl.includes("data:") ||
95+
envResumeUrl.includes("vbscript:")
96+
) {
97+
return "/resume.pdf";
98+
}
99+
return envResumeUrl;
100+
}
101+
return "/resume.pdf";
102+
} catch {
103+
return "/resume.pdf";
104+
}
105+
}
106+
// 2. Fallback to settings.json logic
77107
const resumeConfig = settings.resume || {};
78108
let url;
79-
80109
if (resumeConfig.type === "external" && resumeConfig.alternativeUrl) {
81110
url = resumeConfig.alternativeUrl;
82111
} else {
83112
url = resumeConfig.url || "/resume.pdf";
84113
}
85-
86-
// Sanitize URL to prevent XSS
87114
try {
88-
// For external URLs, validate they start with http/https
89115
if (url.startsWith("http://") || url.startsWith("https://")) {
90-
// Additional validation for external URLs
91-
const urlObj = new URL(url);
92-
return urlObj.href;
116+
return new URL(url).href;
93117
}
94-
// For relative URLs, ensure they don't contain dangerous protocols
95118
if (url.startsWith("/") || !url.includes(":")) {
96-
// Ensure no script injection in relative URLs
97119
if (
98120
url.includes("javascript:") ||
99121
url.includes("data:") ||
@@ -103,10 +125,8 @@ const Resume = () => {
103125
}
104126
return url;
105127
}
106-
// Fallback to default if URL seems suspicious
107128
return "/resume.pdf";
108129
} catch {
109-
console.warn("Invalid resume URL:", url);
110130
return "/resume.pdf";
111131
}
112132
};

src/utils/resume.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Shared utility for getting the resume URL and filename
2+
3+
/**
4+
* Returns a safe, absolute URL for the resume PDF or external link.
5+
* Priority: env var (VITE_RESUME_LINK or RESUME_LINK) > settings.resume.alternativeUrl > settings.resume.url > /resume.pdf
6+
* @param {object} settings - The settings object (from settings.json)
7+
* @returns {string} - A safe URL for the resume
8+
*/
9+
export function getResumeUrl(settings) {
10+
// 1. Check for environment variable (VITE_RESUME_LINK or RESUME_LINK)
11+
const envResumeUrl = import.meta.env.VITE_RESUME_LINK || import.meta.env.RESUME_LINK;
12+
if (typeof envResumeUrl === "string" && envResumeUrl.trim() !== "") {
13+
return sanitizeResumeUrl(envResumeUrl);
14+
}
15+
// 2. Fallback to settings.json logic
16+
const resumeConfig = settings?.resume || {};
17+
let url =
18+
(resumeConfig.type === "external" && resumeConfig.alternativeUrl) ? resumeConfig.alternativeUrl : resumeConfig.url;
19+
if (typeof url === "string" && url.trim() !== "") {
20+
return sanitizeResumeUrl(url);
21+
}
22+
// 3. Default fallback
23+
return "/resume.pdf";
24+
}
25+
26+
/**
27+
* Returns a safe filename for the resume download, or undefined to let the browser decide.
28+
* @param {object} settings - The settings object (from settings.json)
29+
* @returns {string|undefined} - A safe filename or undefined
30+
*/
31+
export function getResumeFilename(settings) {
32+
const resumeConfig = settings?.resume || {};
33+
const filename = resumeConfig.filename;
34+
if (!filename || typeof filename !== "string") {
35+
return undefined;
36+
}
37+
const sanitizedFilename = filename
38+
.replace(/[<>:"/\\|?*]/g, "")
39+
.replace(/\.\./g, "")
40+
.trim();
41+
if (sanitizedFilename.length === 0) {
42+
return undefined;
43+
}
44+
return sanitizedFilename;
45+
}
46+
47+
/**
48+
* Sanitizes a resume URL to prevent XSS and invalid protocols.
49+
* Allows only http, https, and relative URLs. Falls back to /resume.pdf if unsafe.
50+
* @param {string} url - The URL to sanitize
51+
* @returns {string} - A safe URL
52+
*/
53+
function sanitizeResumeUrl(url) {
54+
if (!url || typeof url !== "string") return "/resume.pdf";
55+
const trimmed = url.trim();
56+
// Disallow javascript:, data:, vbscript:
57+
if (/^(javascript:|data:|vbscript:)/i.test(trimmed)) return "/resume.pdf";
58+
// Allow only http, https, or relative URLs
59+
try {
60+
const parsed = new URL(trimmed, window.location.origin);
61+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
62+
return parsed.href;
63+
}
64+
// If it's a relative URL, allow it
65+
if (trimmed.startsWith("/")) {
66+
return trimmed;
67+
}
68+
} catch {
69+
// If URL constructor fails, treat as relative if it doesn't contain a colon
70+
if (!trimmed.includes(":")) {
71+
return "/" + trimmed.replace(/^\/+/, "");
72+
}
73+
}
74+
return "/resume.pdf";
75+
}

0 commit comments

Comments
 (0)