├── utils
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── duck.png
├── github.png
├── logo.png
├── bug-icon.png
├── linkedin.png
├── copy-icon.png
└── logo-store.png
├── .gitignore
├── manifest.json
├── LICENSE
├── popup.html
├── README.md
├── styles.css
└── popup.js
/utils/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/1.png
--------------------------------------------------------------------------------
/utils/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/2.png
--------------------------------------------------------------------------------
/utils/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/3.png
--------------------------------------------------------------------------------
/utils/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/4.png
--------------------------------------------------------------------------------
/utils/duck.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/duck.png
--------------------------------------------------------------------------------
/utils/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/github.png
--------------------------------------------------------------------------------
/utils/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/logo.png
--------------------------------------------------------------------------------
/utils/bug-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/bug-icon.png
--------------------------------------------------------------------------------
/utils/linkedin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/linkedin.png
--------------------------------------------------------------------------------
/utils/copy-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/copy-icon.png
--------------------------------------------------------------------------------
/utils/logo-store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khanhdo05/quack/HEAD/utils/logo-store.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node.js dependencies
2 | node_modules/
3 | .idea/
4 | # Logs
5 | logs/
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | # OS generated files
12 | .DS_Store
13 | Thumbs.db
14 |
15 | # IDE specific files
16 | .idea/
17 | .vscode/
18 | *.sublime-project
19 | *.sublime-workspace
20 |
21 | # Build output
22 | dist/
23 | build/
24 |
25 | # Chrome extension specific files
26 | *.pem
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "quack",
4 | "description": "Quickly copy-paste GitHub, LinkedIn, and portfolio links for job applications using Quack.",
5 | "version": "0.0.0.2",
6 | "icons": {
7 | "16": "utils/duck.png",
8 | "48": "utils/duck.png",
9 | "128": "utils/duck.png"
10 | },
11 | "action": {
12 | "default_popup": "popup.html",
13 | "default_icon": "utils/duck.png"
14 | },
15 | "permissions": [
16 | "clipboardWrite",
17 | "storage"
18 | ]
19 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Khanh Do
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Quack
8 |
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Quack
21 |
27 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Quack
2 |
3 | Quickly copy-paste GitHub, LinkedIn, and portfolio links for job applications using Quack.
4 |
5 | ## Installation
6 |
7 | To install the extension, visit the [Chrome Web Store](https://chromewebstore.google.com/detail/quack/kbbkbaoiaeccjdbkcjngdfgphfeolcfj) and click "Add to Chrome".
8 |
9 | 
10 |
11 | ## Usage
12 |
13 | 
14 | 
15 |
16 | ## Contributing
17 |
18 | We welcome contributions from the community! Here are some guidelines to help you get started:
19 |
20 | ### Reporting Bugs
21 |
22 | If you find a bug, please report it by opening an issue on the [GitHub repository](https://github.com/khanhdo05/quack/issues). Include as much detail as possible, such as steps to reproduce the bug, your operating system, and browser version.
23 |
24 | ### Feature Requests
25 |
26 | If you have an idea for a new feature, please open an issue on the [GitHub repository](https://github.com/khanhdo05/quack/issues) and describe your idea in detail. We appreciate all suggestions and feedback!
27 |
28 | ### Code Contributions
29 |
30 | 1. **Fork the repository**: Click the "Fork" button at the top right corner of the repository page.
31 | 2. **Clone your fork**:
32 | ```sh
33 | git clone git@github.com:khanhdo05/quack.git
34 | cd quack
35 | ```
36 | 3. **Create a new branch**:
37 | ```sh
38 | git checkout -b feature-or-bugfix-name
39 | ```
40 | 4. **Make your changes**: Implement your feature or bug fix.
41 | 5. **Commit your changes**:
42 | ```sh
43 | git add .
44 | git commit -m "Description of your changes"
45 | ```
46 | 6. **Push to your fork**:
47 | ```sh
48 | git push origin feature-or-bugfix-name
49 | ```
50 | 7. **Create a pull request**: Go to the original repository and click the "New pull request" button. Provide a detailed description of your changes.
51 |
52 | ### Code Style
53 |
54 | Please follow these guidelines to maintain a consistent code style:
55 |
56 | - Use 2 spaces for indentation.
57 | - Use camelCase for variable and function names.
58 | - Use descriptive names for variables and functions.
59 | - Add comments to explain complex logic.
60 |
61 | ### Testing
62 |
63 | Before submitting your changes, make sure to test your code thoroughly. You can try out your local version by following these steps:
64 |
65 | 1. Open Chrome and navigate to `chrome://extensions/`.
66 | 2. Enable "Developer mode" by toggling the switch in the top right corner.
67 | 3. Click on "Load unpacked" and select the repository folder.
68 |
69 | ## License
70 |
71 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
72 |
73 | ## Contact
74 |
75 | If you have any questions or need further assistance, feel free to open an issue on the [GitHub repository](https://github.com/khanhdo05/quack/issues).
76 |
77 | ---
78 |
79 | Thank you for contributing to Quack!
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial, sans-serif;
3 | padding: 10px;
4 | background-color: #f7cc6f;
5 | position: relative;
6 | }
7 | .title {
8 | text-align: center;
9 | font-family: "Bagel Fat One", system-ui;
10 | font-weight: 400;
11 | font-style: normal;
12 | font-size: 9vw;
13 | }
14 | .input-container {
15 | display: flex;
16 | align-items: center;
17 | margin-bottom: 10px;
18 | width: 100%; /* Ensure full width */
19 | }
20 | input {
21 | flex: 1;
22 | padding: 5px;
23 | border: 2px solid #000;
24 | border-radius: 5px;
25 | line-height: normal;
26 | color: #282828;
27 | box-sizing: border-box;
28 | }
29 |
30 | .icon {
31 | width: 20px;
32 | height: 20px;
33 | margin-right: 5px;
34 | border-radius: 50%;
35 | }
36 | .label{
37 | min-width: 80px;
38 | max-width: 80px; /* Set a maximum width */
39 | margin-right: 5px;
40 | margin-left: 5px;
41 | font-family: "Comfortaa", sans-serif;
42 | font-optical-sizing: auto;
43 | font-weight: 700;
44 | font-style: normal;
45 | white-space: nowrap;
46 | overflow: hidden;
47 | text-overflow: ellipsis; /* Add ellipsis for overflowing text */
48 | }
49 |
50 | .custom-name {
51 | /* Reset input-specific styles */
52 | outline: none;
53 | border: 0;
54 | background: none;
55 | padding: 2px;
56 | border-bottom: 1px solid #ccc;
57 |
58 | /* Label-like styling */
59 | font-family: "Comfortaa", sans-serif;
60 | font-weight: 700;
61 | font-style: normal;
62 | color: #282828;
63 | white-space: nowrap;
64 | overflow: hidden;
65 | text-overflow: ellipsis;
66 | font-optical-sizing: auto;
67 | min-width: 80px;
68 | max-width: 80px;
69 | margin-right: 5px;
70 | margin-left: 5px;
71 | }
72 |
73 | .custom-link-url {
74 | flex: 1;
75 | padding: 5px;
76 | border: 2px solid #000;
77 | border-radius: 5px;
78 | line-height: normal;
79 | color: #282828;
80 | box-sizing: border-box;
81 | min-width: 0; /* Allow input to shrink */
82 | }
83 |
84 | #settingsButton {
85 | margin-top: 10px;
86 | width: 100%;
87 | }
88 |
89 | @keyframes fadein {
90 | from {bottom: 0; opacity: 0;}
91 | to {bottom: 30px; opacity: 1;}
92 | }
93 | @keyframes fadeout {
94 | from {bottom: 30px; opacity: 1;}
95 | to {bottom: 0; opacity: 0;}
96 | }
97 | .short-input {
98 | width: 50px;
99 | margin-right: 5px;
100 | }
101 | .bug-report {
102 | position: absolute;
103 | top: 10px;
104 | right: 10px;
105 | text-decoration: none;
106 | }
107 | .bug-icon {
108 | width: 20px;
109 | height: 20px;
110 | }
111 | .button-35 {
112 | align-items: center;
113 | background-color: #fff;
114 | border-radius: 3px;
115 | box-shadow: transparent 0 0 0 3px,rgba(18, 18, 18, .1) 0 3px 6px;
116 | box-sizing: border-box;
117 | color: #121212;
118 | cursor: pointer;
119 | display: inline-flex;
120 | flex: 1 1 auto;
121 | font-family: Inter,sans-serif;
122 | font-size: 0.8rem;
123 | font-weight: 700;
124 | justify-content: center;
125 | line-height: 1;
126 | margin: 0;
127 | outline: none;
128 | padding: 0.2rem 0.5rem;
129 | text-align: center;
130 | text-decoration: none;
131 | transition: box-shadow .2s,-webkit-box-shadow .2s;
132 | white-space: nowrap;
133 | border: 0;
134 | user-select: none;
135 | -webkit-user-select: none;
136 | touch-action: manipulation;
137 | }
138 |
139 | .button-container .button-35 {
140 | margin: 0 5px; /* Add horizontal margin to buttons inside the container */
141 | }
142 |
143 | .button-container .button-35:first-child {
144 | margin-left: 0; /* Remove left margin from the first button */
145 | }
146 |
147 | .button-container .button-35:last-child {
148 | margin-right: 0; /* Remove right margin from the last button */
149 | }
150 |
151 | .button-35:hover {
152 | box-shadow: #121212 0 0 0 2px, transparent 0 0 0 0;
153 | }
154 |
155 | .snackbar {
156 | font-family: "Poppins", sans-serif;
157 | font-weight: 500;
158 | font-style: normal;
159 | margin-top: 2px;
160 | }
161 |
162 | .custom-link {
163 | display: flex;
164 | align-items: center;
165 | margin-bottom: 10px;
166 | }
167 |
168 | .edit-icon, .button-35.delete-button, .button-35.plus-button, .copy-icon {
169 | flex: none;
170 | width: 20px;
171 | height: 20px;
172 | cursor: pointer;
173 | margin-left: 5px;
174 | }
175 |
176 | .button-35.save-button, .button-35.auto-save-enabled, .button-35.auto-save-disabled {
177 | height: 20px;
178 | flex: none;
179 | width: min-content;
180 | }
181 |
182 | .button-container {
183 | display: flex;
184 | justify-content: left;
185 | margin-top: 10px;
186 | }
187 |
188 | .auto-save-enabled {
189 | background-color: #4CAF50;
190 | color: white;
191 | }
192 |
193 | .auto-save-disabled {
194 | background-color: #f44336;
195 | color: white;
196 | }
197 |
--------------------------------------------------------------------------------
/popup.js:
--------------------------------------------------------------------------------
1 | document.addEventListener('DOMContentLoaded', () => {
2 | const githubInput = document.getElementById('github');
3 | const linkedinInput = document.getElementById('linkedin');
4 | const portfolioInput = document.getElementById('portfolio');
5 | const customLinksContainer = document.getElementById('custom-links');
6 | const autoSaveButton = document.getElementById('autoSave');
7 | const autoSaveDisabledString = 'auto-save-disabled';
8 | const autoSaveEnabledString = 'auto-save-enabled';
9 |
10 | // Check if auto-save is enabled
11 | function isAutoSaveEnabled() {
12 | return autoSaveButton.classList.contains(autoSaveEnabledString);
13 | }
14 |
15 | // Conditional saving
16 | function conditionalSave() {
17 | console.log("Auto-save condition checked.");
18 | if (isAutoSaveEnabled()) saveAllLinks();
19 | }
20 |
21 | // Initialize auto-save functionality on inputs
22 | function initializeAutoSave(inputElement) {
23 | inputElement.addEventListener('input', () => {
24 | conditionalSave();
25 | });
26 | }
27 |
28 | function saveIfEnter(inputElement) {
29 | inputElement.addEventListener('keypress', (event) => {
30 | if (event.key === 'Enter') {
31 | event.preventDefault(); // Prevents any default form submission
32 | saveAllLinks();
33 | showSnackbar('Saved!');
34 | }
35 | });
36 | }
37 |
38 | // Load saved links from Chrome storage
39 | chrome.storage.sync.get(['github', 'linkedin', 'portfolio', 'customLinks'], (result) => {
40 | if (result.github) githubInput.value = result.github;
41 | if (result.linkedin) linkedinInput.value = result.linkedin;
42 | if (result.portfolio) portfolioInput.value = result.portfolio;
43 |
44 | // Load custom links if available
45 | if (result.customLinks) {
46 | result.customLinks.forEach(link => {
47 | addCustomLink(link.name, link.url);
48 | });
49 | }
50 | });
51 |
52 | // Load auto-save preference
53 | chrome.storage.sync.get(['autoSave'], (result) => {
54 | const isAutoSaveEnabled = result.autoSave !== undefined ? result.autoSave : true;
55 | updateAutoSaveButton(isAutoSaveEnabled);
56 | });
57 |
58 | autoSaveButton.addEventListener('click', toggleAutoSave);
59 |
60 | function toggleAutoSave() {
61 | const isCurrentlyEnabled = isAutoSaveEnabled();
62 | if (isCurrentlyEnabled) {
63 | showSnackbar('Auto-save disabled');
64 | } else {
65 | showSnackbar('Auto-save enabled');
66 | }
67 | const newAutoSaveState = !isCurrentlyEnabled;
68 | chrome.storage.sync.set({ autoSave: newAutoSaveState });
69 | updateAutoSaveButton(newAutoSaveState);
70 | }
71 |
72 | // Handle auto-save toggle
73 | function updateAutoSaveButton(isEnabled) {
74 | if (isEnabled) {
75 | autoSaveButton.classList.remove('auto-save-disabled');
76 | autoSaveButton.classList.add('auto-save-enabled');
77 | } else {
78 | autoSaveButton.classList.remove('auto-save-enabled');
79 | autoSaveButton.classList.add('auto-save-disabled');
80 | }
81 | }
82 |
83 | // Set up auto-save for main inputs
84 | [githubInput, linkedinInput, portfolioInput].forEach(initializeAutoSave);
85 |
86 | // Set up save if enter
87 | [githubInput, linkedinInput, portfolioInput].forEach(saveIfEnter);
88 |
89 | // Function to save all links
90 | function saveAllLinks() {
91 | console.log("Saving all links...");
92 |
93 | const github = githubInput.value;
94 | const linkedin = linkedinInput.value;
95 | const portfolio = portfolioInput.value;
96 |
97 | const customLinks = Array.from(customLinksContainer.children).map(linkContainer => {
98 | const inputs = linkContainer.querySelectorAll('input');
99 | return {
100 | name: inputs[0].value, // Link Name
101 | url: inputs[1].value // Link URL
102 | };
103 | });
104 |
105 | chrome.storage.sync.set({ github, linkedin, portfolio, customLinks })
106 | .then(() => {
107 | console.log("Links saved successfully.");
108 | if (!isAutoSaveEnabled()) {
109 | showSnackbar('Saved!');
110 | }
111 | })
112 | .catch(error => {
113 | showSnackbar('Error saving links');
114 | console.error("Error saving links:", error);
115 | });
116 | }
117 |
118 | // Modify existing save button click event
119 | document.getElementById('save').addEventListener('click', () => {
120 | saveAllLinks()
121 | });
122 |
123 | // Copy GitHub link
124 | document.getElementById('copyGithub').addEventListener('click', () => {
125 | copyToClipboard(githubInput.value);
126 | });
127 |
128 | // Copy LinkedIn link
129 | document.getElementById('copyLinkedin').addEventListener('click', () => {
130 | copyToClipboard(linkedinInput.value);
131 | });
132 |
133 | // Copy Portfolio link
134 | document.getElementById('copyPortfolio').addEventListener('click', () => {
135 | copyToClipboard(portfolioInput.value);
136 | });
137 |
138 | function copyToClipboard(text) {
139 | navigator.clipboard.writeText(text).then(() => {
140 | showSnackbar('Copied to clipboard!');
141 | });
142 | }
143 |
144 | document.getElementById('addLink').addEventListener('click', () => {
145 | addCustomLink(); // Call with no arguments for new empty fields
146 | });
147 |
148 | function showSnackbar(message) {
149 | const snackbar = document.getElementById('snackbar');
150 | snackbar.textContent = message;
151 | snackbar.style.visibility = 'visible';
152 | snackbar.style.animation = 'fadein 0.5s, fadeout 0.5s 2.5s';
153 | setTimeout(() => {
154 | snackbar.style.visibility = 'hidden';
155 | }, 3000);
156 | }
157 |
158 | /**
159 | * Fetches the favicon (website icon) for a specified URL.
160 | *
161 | * This function constructs a URL for the favicon using Google’s favicon service
162 | * and attempts to load it as an image. If successful, it returns the favicon URL;
163 | * if unsuccessful, it returns null and logs an error message.
164 | */
165 | async function getFavicon(url) {
166 | try {
167 | // Construct the favicon URL using Google's favicon service
168 | const faviconUrl = `https://www.google.com/s2/favicons?domain=${url}`;
169 |
170 | const img = new Image();
171 | img.src = faviconUrl;
172 |
173 | return new Promise((resolve, reject) => {
174 | img.onload = () => resolve(faviconUrl); // Resolve with the favicon URL
175 | img.onerror = () => {
176 | img.src = 'utils/duck.png'; // Default icon
177 | reject(new Error('Favicon not found'));
178 | } // Reject if not found
179 | });
180 | } catch (error) {
181 | console.error('Error fetching favicon:', error);
182 | return null;
183 | }
184 | }
185 |
186 | /**
187 | * Adds a custom link with an associated icon, name, and URL input fields to the UI.
188 | *
189 | * This function creates a container for a new link entry, which includes:
190 | * - An icon representing the link (fetched from the provided URL if available)
191 | * - Input fields for the site name and URL
192 | * - A delete button to remove the link entry from the UI
193 | * - A copy icon for copying the URL to the clipboard
194 | */
195 | async function addCustomLink(name = '', url = '') {
196 | const newLinkContainer = document.createElement('div');
197 | newLinkContainer.className = 'input-container custom-link';
198 |
199 | const newNameLabel = document.createElement('img');
200 | newNameLabel.className = 'icon';
201 | newNameLabel.src = 'utils/duck.png'; // Default icon
202 | newLinkContainer.appendChild(newNameLabel);
203 |
204 | const newNameInput = document.createElement('input');
205 | newNameInput.className = 'custom-name';
206 | newNameInput.type = 'text';
207 | newNameInput.placeholder = 'site';
208 | newNameInput.value = name; // Pre-fill if provided
209 | newLinkContainer.appendChild(newNameInput);
210 |
211 | const newLinkInput = document.createElement('input');
212 | newLinkInput.type = 'text';
213 | newLinkInput.value = url;
214 | newLinkInput.className = 'custom-link-url';
215 | newLinkInput.placeholder = 'Enter URL';
216 | newLinkContainer.appendChild(newLinkInput);
217 |
218 | // Create and add delete button
219 | const deleteButton = document.createElement('button');
220 | deleteButton.textContent = 'x';
221 | deleteButton.title = 'Delete field';
222 | deleteButton.className = 'button-35 delete-button';
223 | deleteButton.addEventListener('click', () => {
224 | newLinkContainer.remove(); // Remove the custom link container
225 | conditionalSave();
226 | });
227 | newLinkContainer.appendChild(deleteButton);
228 |
229 | const newCopyIcon = document.createElement('img');
230 | newCopyIcon.src = 'utils/copy-icon.png';
231 | newCopyIcon.className = 'copy-icon';
232 | newCopyIcon.alt = 'Copy';
233 | newCopyIcon.addEventListener('click', () => {
234 | copyToClipboard(newLinkInput.value); // Copy the link URL to the clipboard
235 | });
236 | newLinkContainer.appendChild(newCopyIcon);
237 |
238 | customLinksContainer.appendChild(newLinkContainer);
239 |
240 | // Attach auto-save to each custom input
241 | initializeAutoSave(newNameInput);
242 | saveIfEnter(newNameInput);
243 | initializeAutoSave(newLinkInput);
244 | saveIfEnter(newLinkInput);
245 |
246 | conditionalSave();
247 |
248 | // Fetch favicon asynchronously and update the icon if found
249 | // if (url) {
250 | // updateFavicon(newNameLabel, url);
251 | // }
252 | }
253 |
254 | /**
255 | * Updates the favicon for a specified icon element using the URL provided.
256 | */
257 | async function updateFavicon(iconElement, url) {
258 | try {
259 | const favicon = await getFavicon(new URL(url).hostname);
260 | if (favicon) {
261 | iconElement.src = favicon;
262 | }
263 | } catch (error) {
264 | console.error('Error fetching favicon:', error);
265 | }
266 | }
267 |
268 | // Add event listener for window unload
269 | window.addEventListener('unload', () => {
270 | conditionalSave();
271 | });
272 | });
273 |
--------------------------------------------------------------------------------