Repository: raphaelheinz/LeetHub-3.0
Pull request #53: Add timestamp to solution filenames to prevent overwrite
Author: login: girish-kanjiyani7
Created: 2025-03-25T14:03:23Z | Merged: 2025-04-13T22:21:26Z

This PR introduces an enhancement to allow multiple solutions for the same LeetCode problem to be saved uniquely in the GitHub repo, avoiding overwrites and preserving version history.

**_Key changes:_**
Modified the loader() function to append a timestamp to each solution filename.

Utilized the existing getTodaysDate() function to generate a clean, filesystem-safe timestamp.

Format used: MM-DD-YYYY_HH-MM-SS

Example filename: 0001-two-sum-03-25-2025_15-30-12.py

If a right-click suffix is provided, it is combined with the timestamp (e.g., 0001-two-sum-dfs-03-25-2025_15-30-12.cpp).

**_Why This Is Useful:_**
Maintains a complete history of all attempts/strategies for the same problem.

Helps track progress and improvement over time.

Prevents accidental overwrites when using the "Push" button or auto-upload after resubmission.

 **_Additional Notes:_**
Changes are backward-compatible. Default behavior (with overwrite) is preserved if users modify the logic back.

Timestamp-based filenames are especially useful for practicing multiple approaches (e.g., brute force, optimized, recursive, etc.).


Thanks for maintaining this amazing project — it’s been incredibly helpful. I hope this feature can add value to the LeetHub user base!




Diff:
diff --git a/src/css/popup.css b/src/css/popup.css
index bfe2306..e928c03 100644
--- a/src/css/popup.css
+++ b/src/css/popup.css
@@ -50,6 +50,8 @@ body {
 }
 
 .collapsible-container {
+  margin-top: 1em;
+  margin-bottom: 1em;
   border: 1px dotted black;
   padding: 1em;
   display: flex;
diff --git a/src/html/popup.html b/src/html/popup.html
index 9cc4660..1db00bd 100644
--- a/src/html/popup.html
+++ b/src/html/popup.html
@@ -109,6 +109,26 @@ <h1 id="title">Leet<span style="color: #ff6c0a">Hub</span>-3.0</h1>
             </div>
           </div>
 
+          <!-- Toggle useTimestampFilename setting -->
+          <div>
+            <span id="collapsible-timestamp-icon" class="collapsible-icon">&#9654;</span> Enable
+            Timestamped Filenames
+            <div
+              id="collapsible-timestamp-container"
+              class="collapsible-container"
+              style="display: none"
+            >
+              <p>
+                Save submissions like <code>0001-two-sum_03-30-2025_12-30-15.js</code> to preserve
+                versions.
+              </p>
+              <div class="centered-toggle">
+                <input type="checkbox" id="use-timestamp-filename" />
+                <label for="use-timestamp-filename">Add versioned filenames (timestamped)</label>
+              </div>
+            </div>
+          </div>
+
           <div class="ui small header">
             Want more features?
             <a
diff --git a/src/js/background.js b/src/js/background.js
index b6a8832..546be05 100644
--- a/src/js/background.js
+++ b/src/js/background.js
@@ -1,15 +1,15 @@
 const displayWelcomePage = () => {
   const url = chrome.runtime.getURL('src/html/welcome.html');
   chrome.tabs.create({ url: url, active: true });
-}
+};
 
 const closeTab = () => {
   chrome.tabs.query({ active: true, lastFocusedWindow: true }, tabs => {
     chrome.tabs.remove(tabs[0].id);
   });
-}
+};
 
-const handleMessage = (request) => {
+const handleMessage = request => {
   if (!request) {
     console.log('Received undefined message');
     return;
@@ -31,6 +31,6 @@ const handleMessage = (request) => {
       closeTab();
     }
   }
-}
+};
 
 chrome.runtime.onMessage.addListener(handleMessage);
diff --git a/src/js/leetcode.js b/src/js/leetcode.js
index 060194f..ab7c874 100644
--- a/src/js/leetcode.js
+++ b/src/js/leetcode.js
@@ -55,6 +55,20 @@ function getTodaysDate() {
   return `${formattedMonth}-${formattedDay}-${year}`;
 }
 
+/* returns time in hh-mm-ss format */
+function getTime() {
+  const today = new Date();
+  const hours = today.getHours();
+  const minutes = today.getMinutes();
+  const seconds = today.getSeconds();
+
+  const formattedHours = hours < 10 ? '0' + hours : hours;
+  const formattedMinutes = minutes < 10 ? '0' + minutes : minutes;
+  const formattedSeconds = seconds < 10 ? '0' + seconds : seconds;
+
+  return `${formattedHours}-${formattedMinutes}-${formattedSeconds}`;
+}
+
 /**
  * Constructs the full GitHub API URL to upload a file to a specific path in the repository.
  *
@@ -192,13 +206,9 @@ const incrementStats = () => {
   });
 };
 
-const checkAlreadyCompleted = problemName => {
-  return chrome.storage.local.get('stats').then(({ stats }) => {
-    if (stats?.shas?.[problemName] == null) {
-      return false;
-    }
-    return true;
-  });
+const checkAlreadyCompleted = async problemName => {
+  const { stats } = await chrome.storage.local.get('stats');
+  return stats?.shas?.[problemName] ?? false;
 };
 
 /* Main function for updating code on GitHub Repo */
@@ -265,7 +275,7 @@ function uploadGit(
 
   let token;
   let hook;
-  let useDifficultyFolder = true;
+  let useDifficultyFolder = false;
 
   return chrome.storage.local
     .get('leethub_token')
@@ -296,10 +306,7 @@ function uploadGit(
     .then(({ stats }) => {
       if (action === 'upload') {
         /* Get SHA, if it exists */
-        const sha =
-          stats?.shas?.[problemName]?.[fileName] !== undefined
-            ? stats.shas[problemName][fileName]
-            : '';
+        const sha = stats?.shas?.[problemName]?.[fileName] ?? '';
 
         return upload(
           token,
@@ -1235,13 +1242,21 @@ const loader = (leetCode, suffix) => {
       const probStatsCommitMsg = `Time: ${probStats.time} (${probStats.timePercentile}%), Space: ${probStats.space} (${probStats.spacePercentile}%) - LeetHub`; // default commit
       const commitMsg = (await getCustomCommitMessage(problemContext)) || probStatsCommitMsg;
 
+      const { useTimestampFilename = false } =
+        await chrome.storage.local.get('useTimestampFilename');
+
+      let fileName;
+      if (useTimestampFilename) {
+        const timestamp = `${getTodaysDate()}-${getTime()}`.replace(/[:\s]/g, '--');
+        fileName = suffix
+          ? `${problemName}${suffix}-${timestamp}${language}`
+          : `${problemName}-${timestamp}${language}`;
+      } else {
+        fileName = suffix ? `${problemName}${suffix}${language}` : `${problemName}${language}`;
+      }
+
       /* Upload code to Git */
-      const updateCode = leetCode.findAndUploadCode(
-        problemName,
-        suffix ? problemName + suffix + language : problemName + language,
-        commitMsg,
-        'upload',
-      );
+      const updateCode = leetCode.findAndUploadCode(problemName, fileName, commitMsg, 'upload');
 
       await Promise.all([updateReadMe, updateNotes, updateCode]);
 
diff --git a/src/js/popup.js b/src/js/popup.js
index da33ef0..2c532d2 100644
--- a/src/js/popup.js
+++ b/src/js/popup.js
@@ -46,6 +46,23 @@ $('#use-difficulty-folder').change(function () {
   chrome.storage.local.set({ useDifficultyFolder: isChecked });
 });
 
+// Toggle timestamped filenames section
+$('#collapsible-timestamp-icon').click(() => {
+  $('#collapsible-timestamp-icon').toggleClass('open');
+  $('#collapsible-timestamp-container').toggle();
+
+  // Load stored toggle state
+  chrome.storage.local.get({ useTimestampFilename: false }, data => {
+    $('#use-timestamp-filename').prop('checked', data.useTimestampFilename);
+  });
+});
+
+// Save toggle state when checkbox changes
+$('#use-timestamp-filename').change(function () {
+  const isChecked = $(this).is(':checked');
+  chrome.storage.local.set({ useTimestampFilename: isChecked });
+});
+
 $('#msg-save-btn').click(() => {
   const commitMessage = $('#custom-commit-msg').val();
   chrome.runtime.sendMessage({


Reviews:
user: login: raphaelheinz
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-12T16:25:04Z
user: login: girish-kanjiyani7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T20:59:38Z
user: login: girish-kanjiyani7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T21:03:57Z
user: login: girish-kanjiyani7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T21:12:05Z

Review comments:
diff_hunk: @@ -150,8 +164,11 @@ const upload = (
     })
     .then(async body => {
       updatedSha = body.content.sha; // get updated SHA.
+      const { useTimestampFilename = false } =
+        await chrome.storage.local.get('useTimestampFilename');
       const stats = await getAndInitializeStats(problem);
-      stats.shas[problem][filename] = updatedSha;
+      const shaKey = useTimestampFilename ? filename : problem;
+      stats.shas[problem][shaKey] = updatedSha;
path: src/js/leetcode.js
user: login: raphaelheinz
body: I think we should always use `const shaKey = filename;` and don't need to care if timestamps are enabled. The sha hash is unique for every single file. If we create multiple files with different timestamps, each file has different sha hash, all good. But we could also have different files when we use different programming languages (and timestamps are disabled).

So I guess we can keep the original version. Am I thinking wrong? 🤔 
created_at: 2025-04-12T16:21:13Z
author_association: OWNER
diff_hunk: @@ -291,15 +308,17 @@ function uploadGit(
     })
     .then(result => {
       useDifficultyFolder = result.useDifficultyFolder || false;
+      return chrome.storage.local.get('useTimestampFilename');
+    })
+    .then(result => {
+      useTimestampFilename = result.useTimestampFilename || false;
       return chrome.storage.local.get('stats');
     })
     .then(({ stats }) => {
       if (action === 'upload') {
         /* Get SHA, if it exists */
-        const sha =
-          stats?.shas?.[problemName]?.[fileName] !== undefined
-            ? stats.shas[problemName][fileName]
-            : '';
+        const shaKey = useTimestampFilename ? fileName : problemName;
+        const sha = stats?.shas?.[problemName]?.[shaKey] ?? '';
path: src/js/leetcode.js
user: login: raphaelheinz
body: Same here as above
created_at: 2025-04-12T16:22:55Z
author_association: OWNER
diff_hunk: @@ -192,13 +209,12 @@ const incrementStats = () => {
   });
 };
 
-const checkAlreadyCompleted = problemName => {
-  return chrome.storage.local.get('stats').then(({ stats }) => {
-    if (stats?.shas?.[problemName] == null) {
-      return false;
-    }
-    return true;
-  });
+const checkAlreadyCompleted = async (problemName, filename) => {
+  const { useTimestampFilename = false } = await chrome.storage.local.get('useTimestampFilename');
+  const { stats } = await chrome.storage.local.get('stats');
+
+  const shaKey = useTimestampFilename ? filename : problemName;
+  return stats?.shas?.[problemName]?.[shaKey] != null;
 };
path: src/js/leetcode.js
user: login: raphaelheinz
body: This method is just checking if problem has been solved before (on any programming language). So we shouldn't check on specific filename. It's sufficient to check if the problem name already existing.
created_at: 2025-04-12T16:24:47Z
author_association: OWNER
diff_hunk: @@ -150,8 +164,11 @@ const upload = (
     })
     .then(async body => {
       updatedSha = body.content.sha; // get updated SHA.
+      const { useTimestampFilename = false } =
+        await chrome.storage.local.get('useTimestampFilename');
       const stats = await getAndInitializeStats(problem);
-      stats.shas[problem][filename] = updatedSha;
+      const shaKey = useTimestampFilename ? filename : problem;
+      stats.shas[problem][shaKey] = updatedSha;
path: src/js/leetcode.js
user: login: girish-kanjiyani7
body: I was originally trying to enforce a single file per problem (regardless of language) when timestamping was off, but I agree that allowing different language versions even without timestamps could be useful. 
created_at: 2025-04-13T20:59:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -291,15 +308,17 @@ function uploadGit(
     })
     .then(result => {
       useDifficultyFolder = result.useDifficultyFolder || false;
+      return chrome.storage.local.get('useTimestampFilename');
+    })
+    .then(result => {
+      useTimestampFilename = result.useTimestampFilename || false;
       return chrome.storage.local.get('stats');
     })
     .then(({ stats }) => {
       if (action === 'upload') {
         /* Get SHA, if it exists */
-        const sha =
-          stats?.shas?.[problemName]?.[fileName] !== undefined
-            ? stats.shas[problemName][fileName]
-            : '';
+        const shaKey = useTimestampFilename ? fileName : problemName;
+        const sha = stats?.shas?.[problemName]?.[shaKey] ?? '';
path: src/js/leetcode.js
user: login: girish-kanjiyani7
body: Yep, it's the same thing can be removed and changed to just the filename.
created_at: 2025-04-13T21:03:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,13 +209,12 @@ const incrementStats = () => {
   });
 };
 
-const checkAlreadyCompleted = problemName => {
-  return chrome.storage.local.get('stats').then(({ stats }) => {
-    if (stats?.shas?.[problemName] == null) {
-      return false;
-    }
-    return true;
-  });
+const checkAlreadyCompleted = async (problemName, filename) => {
+  const { useTimestampFilename = false } = await chrome.storage.local.get('useTimestampFilename');
+  const { stats } = await chrome.storage.local.get('stats');
+
+  const shaKey = useTimestampFilename ? filename : problemName;
+  return stats?.shas?.[problemName]?.[shaKey] != null;
 };
path: src/js/leetcode.js
user: login: girish-kanjiyani7
body: Thanks for the clarification! That makes complete sense — we only need to check if the problem has been solved in general, regardless of language or filename.
created_at: 2025-04-13T21:12:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: raphaelheinz
created_at: 2025-03-27T21:20:01Z
body: Hi @girish-kanjiyani7, many thanks for your PR :) 
author_association: OWNER
user: login: raphaelheinz
created_at: 2025-03-28T10:48:09Z
body: I really appreciate your PR 🙏 Already checked and everything is fine. 

I'm just wondering if we should make this change mandatory for everyone. I talked to some friends and they would prefer a clean repository containing only their latest submissions. How about making this feature optional? We could add a checkbox, like we did before for custom commit messages. What do you think?
author_association: OWNER
user: login: girish-kanjiyani7
created_at: 2025-03-28T23:03:37Z
body: Thanks so much for reviewing the PR and for the kind words! 😊

That’s a great point — I completely understand how some users would prefer a clean repo with just the latest version.

I’d be happy to make this feature optional, similar to how the custom commit message toggle works. I can add a checkbox in the settings like:

[x] Enable Timestamped Filenames for Submissions

When enabled, filenames will include a timestamp to preserve versions. When disabled, it reverts to the original overwrite behavior.

I’ll work on the update. Let me know if you have any preferences for the checkbox label or default behavior!
author_association: CONTRIBUTOR
user: login: raphaelheinz
created_at: 2025-03-28T23:30:38Z
body: That would be great, thanks for your support! 

Yes, I agree with you, let's implement that way. I would label the checkbox something like "[X] Add timestamp to filenames"

Let's disable the checkbox by default so we don't break existing behavior for our plugin users. After there has been some positive feedback after some time, we could enable the checkbox by default in the future.
author_association: OWNER
user: login: raphaelheinz
created_at: 2025-03-31T06:41:34Z
body: Btw @girish-kanjiyani7, fyi yesterday I merged another PR which is a very similar use case. If you'd like you can check that out. Many thanks for your support :)

PRs: #52, #54 
author_association: OWNER
user: login: girish-kanjiyani7
created_at: 2025-03-31T15:43:29Z
body: Yes, I'll take these two in consideration and implement accordingly. Thanks for sharing this!
author_association: CONTRIBUTOR
user: login: girish-kanjiyani7
created_at: 2025-04-02T22:58:46Z
body: Conflicts are resolved and the timestamp versioning feature is now working as expected (both when the toggle is on and off).


author_association: CONTRIBUTOR
user: login: raphaelheinz
created_at: 2025-04-03T06:17:00Z
body: That's great! Many thanks for your support. I'll merge your changes soon.
author_association: OWNER
user: login: girish-kanjiyani7
created_at: 2025-04-03T21:47:02Z
body: Hi @raphaelheinz,
I've cleaned up the branch and removed the previous commits that were not part of the final implementation. This PR now includes only the updated and finalized version of the optional timestamp toggle feature to prevent solution file overwrite.

Please let me know if any further changes or adjustments are needed. Thanks for your time and for reviewing this!
author_association: CONTRIBUTOR
user: login: girish-kanjiyani7
created_at: 2025-04-10T01:49:37Z
body: I've reviewed all the changes and it looks good to me, let me know if there's any other problem or fixes you want my help with!
author_association: CONTRIBUTOR
user: login: raphaelheinz
created_at: 2025-04-10T06:13:16Z
body: Hi @girish-kanjiyani7, sorry I have not merged yet and I'll finish as soon as possible. To me everything looks okay, I'm just testing if works as expected. I'll let you know.
author_association: OWNER
user: login: raphaelheinz
created_at: 2025-04-12T16:02:42Z
body: Hi @girish-kanjiyani7, finally I had some free time and reviewed. Thanks again for your work, I'm impressed and appreciate your contribution. I took this pull request as a chance for me to get more familiar with the overall code, that's why it took some time. 

In [b1271e1](https://github.com/raphaelheinz/LeetHub-3.0/pull/53/commits/b1271e14183d68e43086bc3eca27d6470b963d54) I just pushed some refactoring tasks, I think using `useTimestampFilename` as parameter was obsolete because it has not been used. Please note I've not changed anything on the functionality though. Code is just a bit simpler now.

I've one last thing to clarify before merging. Maybe you can help me with understanding: I'll create GitHub review with some notes below
author_association: OWNER
user: login: raphaelheinz
created_at: 2025-04-12T16:26:39Z
body: @girish-kanjiyani7 Can you check above if everything OK from your side? After you agreed, I can push the necessary changes, test and finally merge 😃 
author_association: OWNER
user: login: raphaelheinz
created_at: 2025-04-13T21:33:00Z
body: OK thanks for your feedback. I've pushed the necessary changes. I think we are ready for merge 🚀  
Many thanks for your support @girish-kanjiyani7 🙏 
author_association: OWNER
user: login: girish-kanjiyani7
created_at: 2025-04-13T22:53:24Z
body: Awesome! Thanks a ton for the review and helpful feedback throughout 🙌
Glad we could get this wrapped up.
author_association: CONTRIBUTOR