@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/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,10 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App.tsx';
4
+ import './index.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
@@ -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,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -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
+ });