Skip to content
Merged
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
2 changes: 1 addition & 1 deletion backend/leetcode/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class UserProblemRecordSerializer(serializers.ModelSerializer):

class Meta:
model = UserProblemRecord
fields = ["problem", "solved_at"]
fields = ["problem", "problem_id", "solved_at"]

def create(self, validated_data):
return super().create(validated_data)
8 changes: 6 additions & 2 deletions backend/leetcode/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ class UserProblemRecordViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated]

def get_queryset(self):
# return user records
return UserProblemRecord.objects.filter(user=self.request.user).select_related("problem")
# return user records, ordered by problem_id
return (
UserProblemRecord.objects.filter(user=self.request.user)
.select_related("problem")
.order_by("problem__problem_id")
)

def perform_create(self, serializer):
serializer.save(user=self.request.user)
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
command: >
sh -c "
python manage.py migrate &&
python manage.py get_leetcode_problem &&
python manage.py runserver 0.0.0.0:8000
"
volumes:
Expand Down
299 changes: 196 additions & 103 deletions frontend/vite-project/src/LeetCode.jsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,146 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import "./LeetCode.css";

function LeetCode() {
const navigate = useNavigate();
const [problemNumber, setProblemNumber] = useState("");
const [records, setRecords] = useState([
// Example placeholder record
{
id: 1,
number: 1,
name: "Two Sum",
difficulty: "Easy",
topics: ["Array", "Hash Table"],
completedAt: "2024-01-15 10:30:00",
},
]);
const [records, setRecords] = useState([]);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);

const handleSubmit = (e) => {
const fetchRecords = async (token) => {
setLoading(true);
try {
const response = await fetch("http://localhost:8000/leetcode/records/", {
method: "GET",
headers: {
Authorization: `Token ${token}`,
"Content-Type": "application/json",
},
});

if (!response.ok) {
throw new Error("Failed to fetch records");
}

const responseData = await response.json();
const recordsList = responseData.results || responseData;
const sortedRecords = Array.isArray(recordsList)
? recordsList.sort(
(a, b) => a.problem.problem_id - b.problem.problem_id,
)
: [];
setRecords(sortedRecords);
} catch (error) {
console.error("Error fetching records:", error);
setError("Failed to load records");
} finally {
setLoading(false);
}
};

useEffect(() => {
const token = localStorage.getItem("token");
if (token) {
fetchRecords(token);
} else {
navigate("/login");
}
}, [navigate]);

const handleSubmit = async (e) => {
e.preventDefault();
setError("");
setSubmitting(true);

const number = parseInt(problemNumber.trim());
if (isNaN(number) || number <= 0) {
setError("Please enter a valid positive integer for the problem number.");
setSubmitting(false);
return;
}

if (records.some((record) => record.number === number)) {
const token = localStorage.getItem("token");
if (!token) {
setError("Please login first.");
setSubmitting(false);
navigate("/login");
return;
}

if (records.some((record) => record.problem.problem_id === number)) {
setError(`Problem #${number} already exists in your records.`);
setSubmitting(false);
return;
}

const exampleNames = [
"Two Sum",
"Add Two Numbers",
"Longest Substring Without Repeating Characters",
"Median of Two Sorted Arrays",
"Longest Palindromic Substring",
"ZigZag Conversion",
"Reverse Integer",
"String to Integer (atoi)",
"Palindrome Number",
"Regular Expression Matching",
];

const difficulties = ["Easy", "Medium", "Hard"];
const allTopics = [
"Array",
"Hash Table",
"Two Pointers",
"String",
"Dynamic Programming",
"Math",
"Tree",
"Graph",
"Backtracking",
"Greedy",
"Binary Search",
"Stack",
"Queue",
"Linked List",
];

const randomName =
exampleNames[Math.floor(Math.random() * exampleNames.length)];
const randomDifficulty =
difficulties[Math.floor(Math.random() * difficulties.length)];
const topicCount = Math.floor(Math.random() * 3) + 1; // 1-3 topics
const shuffledTopics = [...allTopics].sort(() => 0.5 - Math.random());
const randomTopics = shuffledTopics.slice(0, topicCount);

const now = new Date();
const completedAt = `${now.getFullYear()}-${String(
now.getMonth() + 1,
).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(
now.getHours(),
).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(
now.getSeconds(),
).padStart(2, "0")}`;

const newRecord = {
id: Date.now(),
number: number,
name: randomName,
difficulty: randomDifficulty,
topics: randomTopics,
completedAt: completedAt,
};

setRecords([...records, newRecord].sort((a, b) => a.number - b.number));
setProblemNumber("");
try {
const recordResponse = await fetch(
"http://localhost:8000/leetcode/records/",
{
method: "POST",
headers: {
Authorization: `Token ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
problem_id: number,
}),
},
);

if (!recordResponse.ok) {
let errorData;
try {
errorData = await recordResponse.json();
} catch (e) {
errorData = {};
}

if (recordResponse.status === 400) {
let errorMsg = errorData.detail;

if (!errorMsg && errorData.problem_id) {
if (Array.isArray(errorData.problem_id)) {
errorMsg = errorData.problem_id[0];
} else {
errorMsg = errorData.problem_id;
}
}

if (!errorMsg && errorData.non_field_errors) {
if (Array.isArray(errorData.non_field_errors)) {
errorMsg = errorData.non_field_errors[0];
} else {
errorMsg = errorData.non_field_errors;
}
}

if (!errorMsg) {
errorMsg = `Problem #${number} may already exist in your records, or does not exist in the database.`;
}

setError(errorMsg);
} else if (recordResponse.status === 404) {
setError(`Problem #${number} does not exist in the database. Please ensure the problem is loaded.`);
} else {
setError(`Failed to create record (status: ${recordResponse.status}). Please try again later.`);
}
setSubmitting(false);
return;
}

await fetchRecords(token);
setProblemNumber("");
setError("");
} catch (error) {
console.error("Error creating record:", error);
setError("Failed to create record. Please try again later.");
} finally {
setSubmitting(false);
}
};

const getDifficultyColor = (difficulty) => {
Expand Down Expand Up @@ -121,7 +170,7 @@ function LeetCode() {
<div className="input-section">
<form onSubmit={handleSubmit} className="problem-input-form">
<div className="input-group">
<label htmlFor="problem-number">Problem number</label>
<label htmlFor="problem-number">Problem Number</label>
<input
id="problem-number"
type="text"
Expand All @@ -132,52 +181,96 @@ function LeetCode() {
}}
placeholder="Please enter the problem number"
className="problem-input"
disabled={submitting}
/>
{error && <div className="error-message">{error}</div>}
</div>
<button type="submit" className="submit-btn">
confirm
<button
type="submit"
className="submit-btn"
disabled={submitting}
>
{submitting ? "Submitting..." : "Confirm"}
</button>
</form>
</div>

{/* Records Section */}
<div className="records-section">
<h2>Completed Problems</h2>
{records.length === 0 ? (
{loading ? (
<div className="empty-state">
<p>Loading...</p>
</div>
) : records.length === 0 ? (
<div className="empty-state">
<p>Null</p>
<p className="hint">Please enter the problem number.</p>
<p>No records</p>
<p className="hint">Please enter a problem number to add a record.</p>
</div>
) : (
<div className="records-list">
{records.map((record) => (
<div key={record.id} className="record-card">
<div className="record-header">
<div className="record-number">#{record.number}</div>
<div
className="difficulty-badge"
style={{
backgroundColor: getDifficultyColor(record.difficulty),
}}
>
{record.difficulty}
{records.map((record) => {
const problem = record.problem;
const solvedAt = record.solved_at
? new Date(record.solved_at).toLocaleString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
: "Not recorded";

return (
<div
key={`record-${problem.problem_id}`}
className="record-card"
>
<div className="record-header">
<div className="record-number">#{problem.problem_id}</div>
<div
className="difficulty-badge"
style={{
backgroundColor: getDifficultyColor(problem.difficulty),
}}
>
{problem.difficulty}
</div>
</div>
</div>
<div className="record-name">{record.name}</div>
<div className="record-topics">
<span className="topics-label">Topics:</span>
<div className="topics-list">
{record.topics.map((topic, index) => (
<span key={index} className="topic-tag">
{topic}
</span>
))}
<div className="record-name">
{problem.url ? (
<a
href={problem.url}
target="_blank"
rel="noopener noreferrer"
style={{
color: "#007bff",
textDecoration: "none",
}}
>
{problem.title}
</a>
) : (
problem.title
)}
</div>
{problem.tags && problem.tags.length > 0 && (
<div className="record-topics">
<span className="topics-label">Tags:</span>
<div className="topics-list">
{problem.tags.map((tag, index) => (
<span key={index} className="topic-tag">
{tag}
</span>
))}
</div>
</div>
)}
<div className="record-time">Solved at: {solvedAt}</div>
</div>
<div className="record-time">Date: {record.completedAt}</div>
</div>
))}
);
})}
</div>
)}
</div>
Expand Down