Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 119 additions & 9 deletions src/contentScript/contentScript.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,121 @@
const PROBLEM_NAME_CLASS = "mr-2"
const DIFFICULTY_CONTAINER_CLASS = "mt-3"
// LeetCode selectors, based on 2025 website structure
const PROBLEM_NAME_SELECTORS = [".mr-2", "h4", ".text-title-large"]
const DIFFICULTY_SELECTORS = [
".mt-3 div span", // Legacy selector
"[role='difficulty']", // Possible new role attribute
".difficulty-pill" // Possible difficulty label class
]

/**
* Extract problem slug from URL
*/
function extractSlugFromUrl(url: string): string | null {
const match = url.match(/\/problems\/([^\/]+)/)
return match ? match[1] : null
}

/**
* Fetch problem metadata using LeetCode's GraphQL API
*/
async function getQuestionMeta(slug: string) {
const body = {
query: `
query ($titleSlug: String!) {
question(titleSlug: $titleSlug) {
difficulty
questionId
title
likes
dislikes
}
}`,
variables: { titleSlug: slug },
}

try {
const res = await fetch("https://leetcode.com/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})

const { data } = await res.json()
return data?.question
} catch (error) {
console.error("Error fetching from LeetCode GraphQL:", error)
return null
}
}

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
const title = document.querySelector(".mr-2").textContent
const link = window.location.href
const difficulty =
document.querySelector(".mt-3").firstChild.firstChild.textContent
console.log(title, link, difficulty)
sendResponse([title, link, difficulty])
})
// Process using async function
(async () => {
try {
// Get slug from URL
const slug = extractSlugFromUrl(window.location.href)
let title = ""
let difficulty = "Unknown"

// If slug exists, try using GraphQL API
if (slug) {
const meta = await getQuestionMeta(slug)
if (meta) {
title = meta.title
difficulty = meta.difficulty
const link = window.location.href

console.log("LeetCode2Notion extracted (API):", { title, link, difficulty })
sendResponse([title, link, difficulty])
return
}
}

// Fallback to DOM parsing method
// Try multiple possible problem name selectors
let titleElement = null
for (const selector of PROBLEM_NAME_SELECTORS) {
titleElement = document.querySelector(selector)
if (titleElement) break
}

// Try multiple possible difficulty selectors
let difficultyElement = null
for (const selector of DIFFICULTY_SELECTORS) {
const elements = document.querySelectorAll(selector)
for (const el of elements) {
// Check if text content contains difficulty keywords
const text = el.textContent?.toLowerCase() || ""
if (text.includes("easy") || text.includes("medium") || text.includes("hard")) {
difficultyElement = el
break
}
}
if (difficultyElement) break
}

// If problem name not found, try parsing from URL
title = titleElement?.textContent?.trim() || ""
if (!title && slug) {
title = slug.split("-").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ")
}

const link = window.location.href

// Parse difficulty
if (difficultyElement) {
difficulty = difficultyElement.textContent?.trim() || "Unknown"
}

console.log("LeetCode2Notion extracted (DOM):", { title, link, difficulty })
sendResponse([title, link, difficulty])
} catch (error) {
console.error("Error in LeetCode extension:", error)
sendResponse({ error: error.message })
}
})()

// Return true to indicate async response handling
return true
})
85 changes: 68 additions & 17 deletions src/popup/popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,63 @@ import getDateString from "../utils/helpers"

const App: React.FC<{}> = () => {
const [noteInput, setNoteInput] = useState<string>("")
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState<boolean>(false)

const handleButtonClick = () => {
setError(null)
setLoading(true)
let data = [noteInput, getDateString()]

chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(
tabs[0].id,
"ask for lc title from pop",
(problemData) => {
// console.log("problemData=", problemData)
data = [...problemData, ...data]
console.log(data)
addItem(data)
}
)
if (!tabs || !tabs[0] || !tabs[0].id) {
setError("Cannot get current tab information")
setLoading(false)
return
}

try {
chrome.tabs.sendMessage(
tabs[0].id,
"ask for lc title from pop",
(problemData) => {
// Check for Chrome extension API errors
if (chrome.runtime.lastError) {
console.error("Chrome extension error:", chrome.runtime.lastError)
setError(`Failed to get problem info: ${chrome.runtime.lastError.message}`)
setLoading(false)
return
}

// Check if problemData is valid
if (!problemData || !Array.isArray(problemData) || problemData.length < 3) {
setError("Invalid problem data received")
setLoading(false)
return
}

console.log("Successfully received problem data:", problemData)
data = [...problemData, ...data]
console.log("Complete data:", data)

// Add to Notion
addItem(data)
.catch(err => {
console.error("Failed to add to Notion:", err)
// Display more detailed error information
const errorMessage = err.message || "Failed to add to Notion";
setError(errorMessage);
})
.finally(() => {
setLoading(false)
})
}
)
} catch (err) {
console.error("Error sending message:", err)
setError("Failed to send message to content script")
setLoading(false)
}
})
}

Expand All @@ -31,28 +73,37 @@ const App: React.FC<{}> = () => {
mx="8px"
my="16px"
>
{/* <InputBase
placeholder="Add notes ..."
value={noteInput}
onChange={(event) => setNoteInput(event.target.value)}
/> */}
<TextField
style={{ width: 270 }}
placeholder="Add notes ..."
// label="fullWidth"
placeholder="Add notes..."
value={noteInput}
onChange={(event) => setNoteInput(event.target.value)}
disabled={loading}
/>
<IconButton
size="small"
onClick={handleButtonClick}
disabled={loading}
>
<AddIcon />
</IconButton>

{error && (
<Box color="error.main" mt={1} fontSize="0.8rem">
Error: {error}
</Box>
)}

{loading && (
<Box mt={1} fontSize="0.8rem">
Processing...
</Box>
)}
</Box>
)
}

const root = document.createElement("div")
document.body.appendChild(root)
// Use React 17 compatible rendering method
ReactDOM.render(<App />, root)
34 changes: 29 additions & 5 deletions src/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import axios from "axios"

import { NOTION_KEY, NOTION_DATABASE_ID } from "../secret"

// Extend Error type to include originalError property
interface EnhancedError extends Error {
originalError?: any;
}

const BASE_URL = "https://api.notion.com/v1"

const getDatabase = async () => {
Expand Down Expand Up @@ -118,15 +124,33 @@ const addItem = async (data) => {
},
}

axios
.request(options)
// Return Promise for subsequent processing
return axios.request(options)
.then(function (response) {
console.log(response.data)
console.log("Successfully added to Notion:", response.data)
window.close()
return response.data
})
.catch(function (error) {
console.error(error)
alert("error")
console.error("Error adding to Notion:", error);
// Convert error object to more useful error message
let errorMessage = "Unknown error";
if (error.response) {
// Server responded with a status code outside of 2xx range
errorMessage = `API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`;
console.error("Error response data:", error.response.data);
} else if (error.request) {
// Request was made but no response was received
errorMessage = "Notion API did not respond, please check your network connection";
} else {
// Error occurred while setting up the request
errorMessage = `Request error: ${error.message}`;
}
// Throw error with more information
const enhancedError = new Error(errorMessage) as EnhancedError;
enhancedError.originalError = error;
throw enhancedError;
})
}
export { getDatabase, getPages, addItem }