@dytsou/resume-converter 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -0
- package/.github/dependabot.yml +44 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/deploy.yml +56 -0
- package/.github/workflows/publish.yml +241 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/eslint.config.js +28 -0
- package/index.html +15 -0
- package/latex/resume.tex +273 -0
- package/package.json +39 -0
- package/scripts/convert-latex.mjs +774 -0
- package/src/App.tsx +47 -0
- package/src/components/DownloadFromDriveButton.tsx +104 -0
- package/src/index.css +122 -0
- package/src/main.tsx +10 -0
- package/src/utils/googleDriveUtils.ts +64 -0
- package/src/utils/resumeUtils.ts +26 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +24 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +22 -0
- package/vite.config.ts +10 -0
package/src/App.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { DownloadFromDriveButton } from './components/DownloadFromDriveButton';
|
|
3
|
+
import { getResumeUrl, getGoogleDriveResumeLink } from './utils/resumeUtils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Main application component
|
|
7
|
+
* Displays the resume in an iframe and optionally provides a download button
|
|
8
|
+
*/
|
|
9
|
+
function App() {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
document.title = 'Resume';
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
const resumeUrl = getResumeUrl();
|
|
15
|
+
const googleDriveLink = getGoogleDriveResumeLink();
|
|
16
|
+
|
|
17
|
+
const handleIframeLoad = () => {
|
|
18
|
+
console.log('Iframe loaded successfully');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleIframeError = (e: React.SyntheticEvent<HTMLIFrameElement, Event>) => {
|
|
22
|
+
console.error('Iframe error:', e);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
{googleDriveLink && (
|
|
28
|
+
<div className="download-button-wrapper">
|
|
29
|
+
<DownloadFromDriveButton
|
|
30
|
+
driveLink={googleDriveLink}
|
|
31
|
+
filename="resume.pdf"
|
|
32
|
+
buttonText="Download Resume"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
<iframe
|
|
37
|
+
src={resumeUrl}
|
|
38
|
+
title="Resume"
|
|
39
|
+
style={{ border: 'none', width: '100vw', height: '100vh' }}
|
|
40
|
+
onLoad={handleIframeLoad}
|
|
41
|
+
onError={handleIframeError}
|
|
42
|
+
/>
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default App;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { convertToDirectDownloadLink, downloadFile } from '../utils/googleDriveUtils';
|
|
3
|
+
|
|
4
|
+
interface DownloadFromDriveButtonProps {
|
|
5
|
+
/** Google Drive share link */
|
|
6
|
+
driveLink: string;
|
|
7
|
+
/** Optional filename for the downloaded file */
|
|
8
|
+
filename?: string;
|
|
9
|
+
/** Optional custom button text */
|
|
10
|
+
buttonText?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DownloadFromDriveButton({
|
|
14
|
+
driveLink,
|
|
15
|
+
filename,
|
|
16
|
+
buttonText = 'Download Resume',
|
|
17
|
+
}: DownloadFromDriveButtonProps) {
|
|
18
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
|
|
21
|
+
const handleDownload = async () => {
|
|
22
|
+
if (!driveLink) {
|
|
23
|
+
setError('No Google Drive link provided');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setIsLoading(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const directLink = convertToDirectDownloadLink(driveLink);
|
|
32
|
+
|
|
33
|
+
if (!directLink) {
|
|
34
|
+
throw new Error('Invalid Google Drive link format');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Trigger download
|
|
38
|
+
downloadFile(directLink, filename);
|
|
39
|
+
|
|
40
|
+
// Reset loading state after a short delay
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
setIsLoading(false);
|
|
43
|
+
}, 500);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to download file';
|
|
46
|
+
setError(errorMessage);
|
|
47
|
+
setIsLoading(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="download-drive-button-container">
|
|
53
|
+
<button
|
|
54
|
+
onClick={handleDownload}
|
|
55
|
+
disabled={isLoading || !driveLink}
|
|
56
|
+
className="download-drive-button"
|
|
57
|
+
aria-label={buttonText}
|
|
58
|
+
title={buttonText}
|
|
59
|
+
>
|
|
60
|
+
{isLoading ? (
|
|
61
|
+
<span className="button-spinner" aria-hidden="true">
|
|
62
|
+
<svg
|
|
63
|
+
width="24"
|
|
64
|
+
height="24"
|
|
65
|
+
viewBox="0 0 24 24"
|
|
66
|
+
fill="none"
|
|
67
|
+
stroke="currentColor"
|
|
68
|
+
strokeWidth="2"
|
|
69
|
+
strokeLinecap="round"
|
|
70
|
+
strokeLinejoin="round"
|
|
71
|
+
>
|
|
72
|
+
<circle cx="12" cy="12" r="10" opacity="0.3" />
|
|
73
|
+
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
|
|
74
|
+
</svg>
|
|
75
|
+
</span>
|
|
76
|
+
) : (
|
|
77
|
+
<span className="button-icon" aria-hidden="true">
|
|
78
|
+
<svg
|
|
79
|
+
width="24"
|
|
80
|
+
height="24"
|
|
81
|
+
viewBox="0 0 24 24"
|
|
82
|
+
fill="none"
|
|
83
|
+
stroke="currentColor"
|
|
84
|
+
strokeWidth="2"
|
|
85
|
+
strokeLinecap="round"
|
|
86
|
+
strokeLinejoin="round"
|
|
87
|
+
>
|
|
88
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
89
|
+
<polyline points="7 10 12 15 17 10" />
|
|
90
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
91
|
+
</svg>
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
</button>
|
|
95
|
+
{error && (
|
|
96
|
+
<div className="download-error" role="alert">
|
|
97
|
+
{error}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
package/src/index.css
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/* Basic CSS reset and styles */
|
|
2
|
+
* {
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Download button wrapper - positioned bottom-right */
|
|
13
|
+
.download-button-wrapper {
|
|
14
|
+
position: fixed;
|
|
15
|
+
bottom: 20px;
|
|
16
|
+
right: 20px;
|
|
17
|
+
z-index: 1000;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Download from Drive button styles */
|
|
21
|
+
.download-drive-button-container {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
align-items: flex-end;
|
|
25
|
+
gap: 8px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.download-drive-button {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
width: 56px;
|
|
33
|
+
height: 56px;
|
|
34
|
+
padding: 0;
|
|
35
|
+
background: rgba(255, 255, 255, 0.1);
|
|
36
|
+
backdrop-filter: blur(10px);
|
|
37
|
+
-webkit-backdrop-filter: blur(10px);
|
|
38
|
+
color: #333;
|
|
39
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
transition: all 0.3s ease;
|
|
43
|
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
|
44
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.download-drive-button:hover:not(:disabled) {
|
|
48
|
+
background: rgba(255, 255, 255, 0.2);
|
|
49
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
50
|
+
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.15);
|
|
51
|
+
transform: translateY(-2px) scale(1.05);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.download-drive-button:active:not(:disabled) {
|
|
55
|
+
transform: translateY(0) scale(1);
|
|
56
|
+
background: rgba(255, 255, 255, 0.15);
|
|
57
|
+
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.download-drive-button:disabled {
|
|
61
|
+
opacity: 0.6;
|
|
62
|
+
cursor: not-allowed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.button-icon,
|
|
66
|
+
.button-spinner {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
width: 24px;
|
|
71
|
+
height: 24px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.button-icon svg,
|
|
75
|
+
.button-spinner svg {
|
|
76
|
+
width: 100%;
|
|
77
|
+
height: 100%;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.button-spinner svg {
|
|
81
|
+
animation: spin 1s linear infinite;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@keyframes spin {
|
|
85
|
+
from {
|
|
86
|
+
transform: rotate(0deg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
to {
|
|
90
|
+
transform: rotate(360deg);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.download-error {
|
|
95
|
+
padding: 8px 12px;
|
|
96
|
+
background-color: #fee;
|
|
97
|
+
color: #c33;
|
|
98
|
+
border: 1px solid #fcc;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
font-size: 12px;
|
|
101
|
+
max-width: 300px;
|
|
102
|
+
text-align: center;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Responsive design */
|
|
106
|
+
@media (max-width: 768px) {
|
|
107
|
+
.download-button-wrapper {
|
|
108
|
+
bottom: 16px;
|
|
109
|
+
right: 16px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.download-drive-button {
|
|
113
|
+
width: 48px;
|
|
114
|
+
height: 48px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.button-icon,
|
|
118
|
+
.button-spinner {
|
|
119
|
+
width: 20px;
|
|
120
|
+
height: 20px;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for working with Google Drive links
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extracts the file ID from various Google Drive link formats
|
|
7
|
+
* Supports:
|
|
8
|
+
* - https://drive.google.com/file/d/{FILE_ID}/view?usp=sharing
|
|
9
|
+
* - https://drive.google.com/file/d/{FILE_ID}/view
|
|
10
|
+
* - https://drive.google.com/open?id={FILE_ID}
|
|
11
|
+
* - https://drive.google.com/uc?id={FILE_ID}
|
|
12
|
+
* - Direct file ID
|
|
13
|
+
*/
|
|
14
|
+
export function extractGoogleDriveFileId(link: string): string | null {
|
|
15
|
+
if (!link) return null;
|
|
16
|
+
|
|
17
|
+
// Try to extract from /file/d/{FILE_ID}/ format
|
|
18
|
+
const fileIdMatch = link.match(/\/file\/d\/([a-zA-Z0-9_-]+)/);
|
|
19
|
+
if (fileIdMatch) {
|
|
20
|
+
return fileIdMatch[1];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Try to extract from ?id={FILE_ID} format
|
|
24
|
+
const idParamMatch = link.match(/[?&]id=([a-zA-Z0-9_-]+)/);
|
|
25
|
+
if (idParamMatch) {
|
|
26
|
+
return idParamMatch[1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// If it's already just a file ID (no URL structure)
|
|
30
|
+
if (/^[a-zA-Z0-9_-]+$/.test(link.trim())) {
|
|
31
|
+
return link.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Converts a Google Drive share link to a direct download link
|
|
39
|
+
* @param shareLink - The Google Drive share link
|
|
40
|
+
* @returns Direct download link or null if conversion fails
|
|
41
|
+
*/
|
|
42
|
+
export function convertToDirectDownloadLink(shareLink: string): string | null {
|
|
43
|
+
const fileId = extractGoogleDriveFileId(shareLink);
|
|
44
|
+
if (!fileId) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return `https://drive.google.com/uc?export=download&id=${fileId}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Triggers a download of a file from a URL
|
|
53
|
+
* @param url - The URL to download from
|
|
54
|
+
* @param filename - Optional filename for the downloaded file
|
|
55
|
+
*/
|
|
56
|
+
export function downloadFile(url: string, filename?: string): void {
|
|
57
|
+
const link = document.createElement('a');
|
|
58
|
+
link.href = url;
|
|
59
|
+
link.download = filename || '';
|
|
60
|
+
link.target = '_blank';
|
|
61
|
+
document.body.appendChild(link);
|
|
62
|
+
link.click();
|
|
63
|
+
document.body.removeChild(link);
|
|
64
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for resume-related operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets the resume HTML file URL based on the current environment
|
|
7
|
+
* Handles both GitHub Pages deployment and local development
|
|
8
|
+
*/
|
|
9
|
+
export function getResumeUrl(): string {
|
|
10
|
+
const baseUrl = import.meta.env.BASE_URL;
|
|
11
|
+
|
|
12
|
+
// For GitHub Pages, use absolute path
|
|
13
|
+
if (baseUrl === '/resume/') {
|
|
14
|
+
return '/resume/converted-docs/resume.html';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// For local development
|
|
18
|
+
return `${baseUrl}converted-docs/resume.html`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Gets the Google Drive resume link from environment variables
|
|
23
|
+
*/
|
|
24
|
+
export function getGoogleDriveResumeLink(): string {
|
|
25
|
+
return import.meta.env.VITE_GOOGLE_DRIVE_RESUME_LINK || '';
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"]
|
|
24
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2023"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
|
|
8
|
+
/* Bundler mode */
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"moduleDetection": "force",
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
|
|
15
|
+
/* Linting */
|
|
16
|
+
"strict": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["vite.config.ts"]
|
|
22
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
// https://vitejs.dev/config/
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
base: process.env.GITHUB_PAGES === 'true' ? '/resume/' : '/',
|
|
8
|
+
optimizeDeps: {
|
|
9
|
+
},
|
|
10
|
+
});
|