Compare commits
5 Commits
04dc638cf0
...
darkmode
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dc45b9a9b | |||
| 57a7b0e68f | |||
| 68b7b81741 | |||
| e97dde65fb | |||
| 6079813e2c |
1
run.bat
Normal file
1
run.bat
Normal file
@@ -0,0 +1 @@
|
||||
python -m flask --app .\src\app.py run --host=0.0.0.0 --port=5000
|
||||
@@ -37,7 +37,7 @@ def script():
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory("templates", "favicon", "favicon.ico")
|
||||
return send_from_directory("templates", "favicon.ico")
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from models import db, Problem
|
||||
from flask import current_app
|
||||
|
||||
def schedule_problem_reload(app, json_path, interval_hours=10):
|
||||
def reload_loop():
|
||||
while True:
|
||||
with app.app_context():
|
||||
load_problems_from_json(json_path)
|
||||
time.sleep(interval_hours * 3600)
|
||||
t = threading.Thread(target=reload_loop, daemon=True)
|
||||
t.start()
|
||||
60
src/problems/Palindrome/description.md
Normal file
60
src/problems/Palindrome/description.md
Normal file
@@ -0,0 +1,60 @@
|
||||
## Problem: Check if a String is a Palindrome
|
||||
|
||||
Given a string `s`, determine whether it reads the same forward and backward.
|
||||
Return `True` if it is a palindrome, otherwise return `False`.
|
||||
|
||||
A **palindrome** is a sequence of characters that is identical when reversed.
|
||||
Comparison is **case-sensitive** and should consider all characters, including spaces and punctuation.
|
||||
|
||||
---
|
||||
|
||||
### Example 1
|
||||
|
||||
**Input:**
|
||||
|
||||
```
|
||||
s = "racecar"
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
True
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
Reversing `"racecar"` results in `"racecar"`, which is the same as the original string.
|
||||
|
||||
---
|
||||
|
||||
### Example 2
|
||||
|
||||
**Input:**
|
||||
|
||||
```
|
||||
s = "hello"
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
False
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
Reversing `"hello"` results in `"olleh"`, which is different from the original string.
|
||||
|
||||
---
|
||||
|
||||
### Constraints
|
||||
|
||||
* `0 <= len(s) <= 10^5`
|
||||
* `s` may contain letters, digits, symbols, and spaces.
|
||||
|
||||
---
|
||||
|
||||
### Function Signature (Python)
|
||||
|
||||
```python
|
||||
def palindrome(s: str) -> bool:
|
||||
```
|
||||
7
src/problems/Palindrome/manifest.json
Normal file
7
src/problems/Palindrome/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "Palindrome",
|
||||
"description": "Find out wether or not a String is a Palindrome",
|
||||
"description_md": "problems/Palindrome/description.md",
|
||||
"test_code": "problems/Palindrome/test.py",
|
||||
"difficulty": "medium"
|
||||
}
|
||||
32
src/problems/Palindrome/test.py
Normal file
32
src/problems/Palindrome/test.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import unittest
|
||||
|
||||
#<!-- User expected Function -->
|
||||
## def palindrome(s:str) -> bool:
|
||||
## return s == s[::-1]
|
||||
|
||||
class TestSolution(unittest.TestCase):
|
||||
def test_palindrome(self):
|
||||
test_cases = [
|
||||
("racecar", True), # Simple palindrome
|
||||
("hello", False), # Not a palindrome
|
||||
("", True), # Empty string
|
||||
("a", True), # Single character
|
||||
("madam", True), # Palindrome word
|
||||
("Madam", False), # Case-sensitive check
|
||||
("12321", True), # Numeric string palindrome
|
||||
("123456", False), # Numeric string non-palindrome
|
||||
]
|
||||
print("\nFUNCTION OUTPUT TEST RESULTS")
|
||||
|
||||
for input_val, expected in test_cases:
|
||||
try:
|
||||
actual = palindrome(input_val) # pyright: ignore[reportUndefinedVariable]
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | Input: '{input_val}' -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | Input: '{input_val}' -> Exception: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
58
src/problems/regex-phone/description.md
Normal file
58
src/problems/regex-phone/description.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Phone Number Regular Expression Validation
|
||||
|
||||
You are asked to write a function that checks if a given string is a valid phone number.
|
||||
|
||||
A valid phone number must follow this format:
|
||||
|
||||
```python
|
||||
123-456-7890
|
||||
```
|
||||
|
||||
* It contains **3 digits**, followed by a **dash (-)**
|
||||
* Then another **3 digits**, followed by a **dash (-)**
|
||||
* Then exactly **4 digits**
|
||||
|
||||
If the string matches this exact format, return **True**. Otherwise, return **False**.
|
||||
|
||||
---
|
||||
|
||||
### Example 1
|
||||
|
||||
```python
|
||||
Input: "123-456-7890"
|
||||
Output: True
|
||||
```
|
||||
|
||||
### Example 2
|
||||
|
||||
```python
|
||||
Input: "1234567890"
|
||||
Output: False
|
||||
```
|
||||
|
||||
### Example 3
|
||||
|
||||
```python
|
||||
Input: "abc-def-ghij"
|
||||
Output: False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Function Signature
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
def is_valid_phone_number(phone_number: str) -> bool:
|
||||
return bool("Your Solution Here!")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Hint 🔑
|
||||
|
||||
* Use the **`re`** (regular expression) library.
|
||||
* `\d` means “a digit” in regex.
|
||||
* You will need exactly **3 digits**, then a dash, then **3 digits**, another dash, then **4 digits**.
|
||||
* Anchors `^` (start of string) and `$` (end of string) can help ensure the whole string matches.
|
||||
7
src/problems/regex-phone/manifest.json
Normal file
7
src/problems/regex-phone/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"title": "Regex Phonenumber",
|
||||
"description": "A regex problem to match phone numbers in various formats.",
|
||||
"description_md": "problems/regex-phone/description.md",
|
||||
"difficulty": "hard",
|
||||
"test_code": "problems/regex-phone/test.py"
|
||||
}
|
||||
33
src/problems/regex-phone/test.py
Normal file
33
src/problems/regex-phone/test.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import re
|
||||
import unittest
|
||||
|
||||
## def is_valid_phone_number(phone_number : str):
|
||||
## return bool(re.search(r"^(\d{3}-){2}\d{4}$", phone_number))
|
||||
|
||||
import unittest
|
||||
|
||||
class TestPhoneNumberRegex(unittest.TestCase):
|
||||
def test_if_valid(self):
|
||||
test_cases = [
|
||||
("123-456-7890", True), # Valid format
|
||||
("111-222-3333", True), # Another valid format
|
||||
("abc-def-ghij", False), # Letters instead of digits
|
||||
("1234567890", False), # Missing dashes
|
||||
("123-45-67890", False), # Wrong grouping
|
||||
("12-3456-7890", False), # Wrong grouping again
|
||||
("", False), # Empty string
|
||||
]
|
||||
print("\nPHONE NUMBER VALIDATION TEST RESULTS")
|
||||
|
||||
for phone, expected in test_cases:
|
||||
try:
|
||||
actual = is_valid_phone_number(phone) # pyright: ignore[reportUndefinedVariable]
|
||||
status = "✓ PASS" if actual == expected else "✗ FAIL"
|
||||
print(f"{status} | Input: '{phone}' -> Got: {actual} | Expected: {expected}")
|
||||
self.assertEqual(actual, expected)
|
||||
except Exception as e:
|
||||
print(f"✗ ERROR | Input: '{phone}' -> Exception: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,24 +1,47 @@
|
||||
:root {
|
||||
--bg: #f6f8fb;
|
||||
--card: #fff;
|
||||
--text: #0f172a;
|
||||
--muted: #6b7280;
|
||||
--accent: #2563eb;
|
||||
--border: #e5e7eb;
|
||||
--hover: #f3f4f6;
|
||||
--shadow: 0 4px 12px rgba(16, 24, 40, 0.06);
|
||||
--radius: 8px;
|
||||
--mono: 'JetBrains Mono', monospace;
|
||||
--mono: "JetBrains Mono", monospace;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
|
||||
/* Dark mode variables */
|
||||
html.dark {
|
||||
--bg: #0f172a;
|
||||
--card: #1e293b;
|
||||
--text: #f1f5f9;
|
||||
--muted: #94a3b8;
|
||||
--accent: #3b82f6;
|
||||
--border: #334155;
|
||||
--hover: #334155;
|
||||
--shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: Inter, sans-serif;
|
||||
background: var(--bg);
|
||||
color: #0f172a;
|
||||
color: var(--text);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
}
|
||||
.wrap {
|
||||
width: 100%;
|
||||
@@ -27,14 +50,49 @@ body {
|
||||
header {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
header h1 {
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
color: #111827;
|
||||
color: var(--text);
|
||||
}
|
||||
header p {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.dark-mode-toggle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
font-size: 1.2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.dark-mode-toggle:hover {
|
||||
background: var(--hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
html.dark .dark-mode-icon::before {
|
||||
content: "☀︎️";
|
||||
}
|
||||
html:not(.dark) .dark-mode-icon::before {
|
||||
content: "⏾";
|
||||
}
|
||||
.dark-mode-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
.dark-mode-icon::before {
|
||||
font-size: 1em;
|
||||
}
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -58,32 +116,53 @@ header p {
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.filter-select {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background: white;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
/* Problems list */
|
||||
.problems-list .problem-item {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.problem-item:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
.problem-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.problem-item a {
|
||||
text-decoration: none;
|
||||
color: #0077ff;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
.problem-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* Difficulty badge */
|
||||
.difficulty {
|
||||
@@ -98,14 +177,14 @@ header p {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.difficulty[data-difficulty="easy"] {
|
||||
background-color: #4CAF50; /* Green */
|
||||
background-color: #4caf50; /* Green */
|
||||
}
|
||||
.difficulty[data-difficulty="medium"] {
|
||||
background-color: #FFC107; /* Amber */
|
||||
background-color: #ffc107; /* Amber */
|
||||
color: #333;
|
||||
}
|
||||
.difficulty[data-difficulty="hard"] {
|
||||
background-color: #F44336; /* Red */
|
||||
background-color: #f44336; /* Red */
|
||||
}
|
||||
/* Leaderboard */
|
||||
.leaderboard-head {
|
||||
@@ -127,16 +206,16 @@ header p {
|
||||
.leaderboard-table th,
|
||||
.leaderboard-table td {
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: left;
|
||||
}
|
||||
.leaderboard-table th {
|
||||
background: #f9fafb;
|
||||
background: var(--hover);
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
.leaderboard-table tr:hover {
|
||||
background: #f3f4f6;
|
||||
background: var(--hover);
|
||||
}
|
||||
/* Sort indicators */
|
||||
.sortable {
|
||||
@@ -178,8 +257,70 @@ header p {
|
||||
background: rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.content { grid-template-columns: 1fr; }
|
||||
.content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.leaderboard-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Leaderboard horizontal collapse */
|
||||
#leaderboardSection {
|
||||
transition:
|
||||
max-width 0.35s ease,
|
||||
opacity 0.25s ease;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#leaderboardSection.hidden {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#leaderboardSection.visible {
|
||||
max-width: 100%; /* take full available space in grid column */
|
||||
opacity: 1;
|
||||
}
|
||||
#rankingExplanation {
|
||||
transition: all 0.35s ease;
|
||||
}
|
||||
|
||||
/* Pagination Controls */
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.pagination-btn {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
background: var(--hover);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.pagination-info {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Hide pagination when not needed */
|
||||
.pagination-controls.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
296
src/static/problem.css
Normal file
296
src/static/problem.css
Normal file
@@ -0,0 +1,296 @@
|
||||
:root {
|
||||
--bg: #f9f9f9;
|
||||
--card: #fff;
|
||||
--text: #333;
|
||||
--muted: #666;
|
||||
--accent: #007bff;
|
||||
--accent-hover: #0069d9;
|
||||
--border: #eaeaea;
|
||||
--hover: #f8f9fa;
|
||||
--code-bg: #f6f8fa;
|
||||
--editor-border: #ddd;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--bg: #0f172a;
|
||||
--card: #1e293b;
|
||||
--text: #f1f5f9;
|
||||
--muted: #94a3b8;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--border: #334155;
|
||||
--hover: #334155;
|
||||
--code-bg: #1e293b;
|
||||
--editor-border: #475569;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh; /* allow content to grow */
|
||||
overflow-y: auto; /* allow vertical scroll */
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* wrap on small screens */
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.problem-panel {
|
||||
flex: 1 1 400px; /* grow/shrink with base 400px */
|
||||
min-width: 300px;
|
||||
background: var(--card);
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
border-right: 1px solid var(--border);
|
||||
max-height: 100vh;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1 1 400px;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--card);
|
||||
max-height: 100vh;
|
||||
overflow: hidden; /* internal scroll handling */
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.problem-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: var(--muted);
|
||||
margin-right: 15px;
|
||||
padding: 6px 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: var(--text);
|
||||
background: var(--hover);
|
||||
}
|
||||
|
||||
.dark-mode-toggle {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
font-size: 1.2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark-mode-toggle:hover {
|
||||
background: var(--hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
html.dark .dark-mode-icon::before {
|
||||
content: "☀";
|
||||
}
|
||||
|
||||
html:not(.dark) .dark-mode-icon::before {
|
||||
content: "⏾";
|
||||
}
|
||||
|
||||
.dark-mode-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dark-mode-icon::before {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.problem-desc {
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.problem-desc pre {
|
||||
background: var(--code-bg);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.problem-desc code {
|
||||
background: var(--code-bg);
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
padding: 15px 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-actions button {
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.editor-actions button:hover {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
#editor {
|
||||
flex: 1 1 auto;
|
||||
min-height: 300px;
|
||||
border: 1px solid var(--editor-border);
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.result-panel {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: var(--hover);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
min-height: 120px;
|
||||
overflow-y: auto;
|
||||
max-height: 30vh;
|
||||
border: 1px solid var(--border);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.result-panel h3 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result-panel pre {
|
||||
background: var(--code-bg);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 14px;
|
||||
margin: 5px 0;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
font-family: "Inter", sans-serif;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.problem-panel,
|
||||
.editor-container {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
max-height: none;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
#editor {
|
||||
min-height: 400px;
|
||||
max-height: none;
|
||||
}
|
||||
.result-panel {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
403
src/static/script.js
Normal file
403
src/static/script.js
Normal file
@@ -0,0 +1,403 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Dark mode functionality
|
||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||
const html = document.documentElement;
|
||||
|
||||
// Load saved dark mode preference
|
||||
const savedDarkMode = localStorage.getItem("darkMode");
|
||||
if (
|
||||
savedDarkMode === "true" ||
|
||||
(savedDarkMode === null &&
|
||||
// detect if the user already has a dark mode enabled in the system settings ( works for all systems )
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
html.classList.add("dark");
|
||||
}
|
||||
|
||||
darkModeToggle?.addEventListener("click", () => {
|
||||
html.classList.toggle("dark");
|
||||
localStorage.setItem("darkMode", html.classList.contains("dark"));
|
||||
});
|
||||
|
||||
// Problem search and pagination
|
||||
const problemSearch = document.getElementById("problemSearch");
|
||||
const problemsContainer = document.getElementById("problemsContainer");
|
||||
const problemsPagination = document.getElementById("problemsPagination");
|
||||
const problemsPrevBtn = document.getElementById("problemsPrevBtn");
|
||||
const problemsNextBtn = document.getElementById("problemsNextBtn");
|
||||
const problemsPaginationInfo = document.getElementById(
|
||||
"problemsPaginationInfo",
|
||||
);
|
||||
|
||||
let allProblemItems = [];
|
||||
let filteredProblemItems = [];
|
||||
let currentPage = 1;
|
||||
const itemsPerPage = 5;
|
||||
|
||||
// Initialize problem items
|
||||
function initializeProblemItems() {
|
||||
allProblemItems = Array.from(
|
||||
problemsContainer?.querySelectorAll(".problem-item") || [],
|
||||
);
|
||||
filteredProblemItems = [...allProblemItems];
|
||||
updatePagination();
|
||||
}
|
||||
|
||||
function updatePagination() {
|
||||
const totalPages = Math.ceil(filteredProblemItems.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
|
||||
// Hide all items first
|
||||
allProblemItems.forEach((item) => {
|
||||
item.style.display = "none";
|
||||
});
|
||||
|
||||
// Show current page items
|
||||
filteredProblemItems.slice(startIndex, endIndex).forEach((item) => {
|
||||
item.style.display = "";
|
||||
});
|
||||
|
||||
// Update pagination controls
|
||||
if (problemsPrevBtn) problemsPrevBtn.disabled = currentPage <= 1;
|
||||
if (problemsNextBtn) problemsNextBtn.disabled = currentPage >= totalPages;
|
||||
if (problemsPaginationInfo) {
|
||||
problemsPaginationInfo.textContent =
|
||||
totalPages > 0
|
||||
? `Page ${currentPage} of ${totalPages}`
|
||||
: "No problems found";
|
||||
}
|
||||
|
||||
// Hide pagination if not needed
|
||||
if (problemsPagination) {
|
||||
problemsPagination.classList.toggle("hidden", totalPages <= 1);
|
||||
}
|
||||
}
|
||||
|
||||
function filterProblems() {
|
||||
const term = problemSearch?.value.toLowerCase().trim() || "";
|
||||
filteredProblemItems = allProblemItems.filter((item) => {
|
||||
const name = item.dataset.name?.toLowerCase() || "";
|
||||
const desc = item.dataset.desc?.toLowerCase() || "";
|
||||
return !term || name.includes(term) || desc.includes(term);
|
||||
});
|
||||
currentPage = 1;
|
||||
updatePagination();
|
||||
}
|
||||
|
||||
// Event listeners for pagination
|
||||
problemsPrevBtn?.addEventListener("click", () => {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
updatePagination();
|
||||
}
|
||||
});
|
||||
|
||||
problemsNextBtn?.addEventListener("click", () => {
|
||||
const totalPages = Math.ceil(filteredProblemItems.length / itemsPerPage);
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
updatePagination();
|
||||
}
|
||||
});
|
||||
|
||||
problemSearch?.addEventListener("input", filterProblems);
|
||||
|
||||
// Initialize problems pagination
|
||||
if (problemsContainer) {
|
||||
initializeProblemItems();
|
||||
}
|
||||
|
||||
// Leaderboard functionality
|
||||
const problemFilter = document.getElementById("problemFilter");
|
||||
const runtimeFilter = document.getElementById("runtimeFilter");
|
||||
const leaderboardBody = document.getElementById("leaderboardBody");
|
||||
const sortableHeaders = document.querySelectorAll(".sortable");
|
||||
|
||||
let currentSort = { column: "rank", direction: "asc" };
|
||||
let allRows = [];
|
||||
|
||||
// Initialize rows array
|
||||
function initializeRows() {
|
||||
allRows = Array.from(leaderboardBody.querySelectorAll("tr")).map((row) => {
|
||||
return {
|
||||
element: row,
|
||||
user: row.dataset.user || "",
|
||||
problem: row.dataset.problem || "",
|
||||
runtime: parseFloat(row.dataset.runtime) || 0,
|
||||
memory: parseFloat(row.dataset.memory) || 0,
|
||||
timestamp: new Date(row.dataset.timestamp || Date.now()).getTime(),
|
||||
language: row.dataset.language || "",
|
||||
originalIndex: Array.from(leaderboardBody.children).indexOf(row),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updateRankClasses() {
|
||||
const visibleRows = allRows.filter(
|
||||
(row) => row.element.style.display !== "none",
|
||||
);
|
||||
visibleRows.forEach((rowData, index) => {
|
||||
const rank = index + 1;
|
||||
const row = rowData.element;
|
||||
|
||||
// Update rank cell
|
||||
const rankCell = row.cells[0];
|
||||
if (rankCell) rankCell.textContent = rank;
|
||||
|
||||
// Update rank classes
|
||||
row.className = row.className.replace(/\brank-\d+\b/g, "");
|
||||
if (rank === 1) row.classList.add("rank-1");
|
||||
else if (rank <= 3) row.classList.add("rank-top3");
|
||||
});
|
||||
}
|
||||
|
||||
function calculateOverallRanking() {
|
||||
const visibleRows = allRows.filter(
|
||||
(row) => row.element.style.display !== "none",
|
||||
);
|
||||
|
||||
if (visibleRows.length === 0) return;
|
||||
|
||||
// Group submissions by problem to find the best performance for each
|
||||
const problemBests = {};
|
||||
|
||||
visibleRows.forEach((rowData) => {
|
||||
const problem = rowData.problem;
|
||||
if (!problemBests[problem]) {
|
||||
problemBests[problem] = {
|
||||
bestRuntime: Infinity,
|
||||
bestMemory: Infinity,
|
||||
};
|
||||
}
|
||||
|
||||
problemBests[problem].bestRuntime = Math.min(
|
||||
problemBests[problem].bestRuntime,
|
||||
rowData.runtime,
|
||||
);
|
||||
problemBests[problem].bestMemory = Math.min(
|
||||
problemBests[problem].bestMemory,
|
||||
rowData.memory,
|
||||
);
|
||||
});
|
||||
|
||||
// Calculate normalized scores for each submission
|
||||
visibleRows.forEach((rowData) => {
|
||||
const problemBest = problemBests[rowData.problem];
|
||||
|
||||
// Prevent division by zero
|
||||
const runtimeScore =
|
||||
problemBest.bestRuntime > 0
|
||||
? rowData.runtime / problemBest.bestRuntime
|
||||
: 1;
|
||||
const memoryScore =
|
||||
problemBest.bestMemory > 0
|
||||
? rowData.memory / problemBest.bestMemory
|
||||
: 1;
|
||||
|
||||
// Weighted overall score (70% runtime, 30% memory)
|
||||
rowData.overallScore = runtimeScore * 0.7 + memoryScore * 0.3;
|
||||
});
|
||||
|
||||
// Sort by overall score (lower is better), then by timestamp (earlier is better for ties)
|
||||
visibleRows.sort((a, b) => {
|
||||
const scoreDiff = a.overallScore - b.overallScore;
|
||||
if (Math.abs(scoreDiff) > 0.000001) return scoreDiff; // Use small epsilon for float comparison
|
||||
|
||||
// If scores are essentially equal, prefer earlier submission
|
||||
return a.timestamp - b.timestamp;
|
||||
});
|
||||
|
||||
// Reorder DOM elements and update ranks
|
||||
visibleRows.forEach((rowData, index) => {
|
||||
leaderboardBody.appendChild(rowData.element);
|
||||
});
|
||||
|
||||
updateRankClasses();
|
||||
}
|
||||
|
||||
function filterLeaderboard() {
|
||||
const problemTerm = (problemFilter?.value || "").toLowerCase().trim();
|
||||
const runtimeType = runtimeFilter?.value || "all";
|
||||
|
||||
// Reset all rows to visible first
|
||||
allRows.forEach((rowData) => {
|
||||
rowData.element.style.display = "";
|
||||
});
|
||||
|
||||
// Apply problem filter
|
||||
if (problemTerm) {
|
||||
allRows.forEach((rowData) => {
|
||||
const problemMatch = rowData.problem
|
||||
.toLowerCase()
|
||||
.includes(problemTerm);
|
||||
if (!problemMatch) {
|
||||
rowData.element.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply runtime filter (best/worst per user per problem)
|
||||
if (runtimeType === "best" || runtimeType === "worst") {
|
||||
const userProblemGroups = {};
|
||||
|
||||
// Group by user + problem combination
|
||||
allRows.forEach((rowData) => {
|
||||
if (rowData.element.style.display === "none") return;
|
||||
|
||||
const key = `${rowData.user}::${rowData.problem}`;
|
||||
if (!userProblemGroups[key]) {
|
||||
userProblemGroups[key] = [];
|
||||
}
|
||||
userProblemGroups[key].push(rowData);
|
||||
});
|
||||
|
||||
// Hide all except best/worst for each user-problem combination
|
||||
Object.values(userProblemGroups).forEach((group) => {
|
||||
if (group.length <= 1) return;
|
||||
|
||||
// Sort by runtime
|
||||
group.sort((a, b) => a.runtime - b.runtime);
|
||||
|
||||
const keepIndex = runtimeType === "best" ? 0 : group.length - 1;
|
||||
group.forEach((rowData, index) => {
|
||||
if (index !== keepIndex) {
|
||||
rowData.element.style.display = "none";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
calculateOverallRanking();
|
||||
}
|
||||
|
||||
function getCellValue(rowData, column) {
|
||||
switch (column) {
|
||||
case "rank":
|
||||
return parseInt(rowData.element.cells[0]?.textContent) || 0;
|
||||
case "user":
|
||||
return rowData.user.toLowerCase();
|
||||
case "problem":
|
||||
return rowData.problem.toLowerCase();
|
||||
case "runtime":
|
||||
return rowData.runtime;
|
||||
case "memory":
|
||||
return rowData.memory;
|
||||
case "timestamp":
|
||||
return rowData.timestamp;
|
||||
case "language":
|
||||
return rowData.language.toLowerCase();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function sortLeaderboard(column, direction) {
|
||||
if (column === "rank") {
|
||||
calculateOverallRanking();
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleRows = allRows.filter(
|
||||
(row) => row.element.style.display !== "none",
|
||||
);
|
||||
|
||||
visibleRows.sort((a, b) => {
|
||||
const valueA = getCellValue(a, column);
|
||||
const valueB = getCellValue(b, column);
|
||||
|
||||
let comparison = 0;
|
||||
if (typeof valueA === "number" && typeof valueB === "number") {
|
||||
comparison = valueA - valueB;
|
||||
} else {
|
||||
comparison = valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
|
||||
}
|
||||
|
||||
return direction === "asc" ? comparison : -comparison;
|
||||
});
|
||||
|
||||
// Reorder DOM elements
|
||||
visibleRows.forEach((rowData) => {
|
||||
leaderboardBody.appendChild(rowData.element);
|
||||
});
|
||||
|
||||
updateRankClasses();
|
||||
}
|
||||
|
||||
// Event listeners for sorting
|
||||
sortableHeaders.forEach((header) => {
|
||||
header.addEventListener("click", () => {
|
||||
const column = header.dataset.sort;
|
||||
if (!column) return;
|
||||
|
||||
// Remove sorting classes from all headers
|
||||
sortableHeaders.forEach((h) =>
|
||||
h.classList.remove("sort-asc", "sort-desc"),
|
||||
);
|
||||
|
||||
// Toggle sort direction
|
||||
if (currentSort.column === column) {
|
||||
currentSort.direction =
|
||||
currentSort.direction === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
currentSort.column = column;
|
||||
currentSort.direction = "asc";
|
||||
}
|
||||
|
||||
// Add sorting class to current header
|
||||
header.classList.add(`sort-${currentSort.direction}`);
|
||||
|
||||
sortLeaderboard(column, currentSort.direction);
|
||||
});
|
||||
});
|
||||
|
||||
// Filter event listeners
|
||||
problemFilter?.addEventListener("input", filterLeaderboard);
|
||||
runtimeFilter?.addEventListener("change", filterLeaderboard);
|
||||
|
||||
// Rank info popout
|
||||
const rankInfoBtn = document.getElementById("rankInfoBtn");
|
||||
const rankingExplanation = document.getElementById("rankingExplanation");
|
||||
|
||||
rankInfoBtn?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
rankingExplanation?.classList.toggle("active");
|
||||
rankInfoBtn?.classList.toggle("active");
|
||||
});
|
||||
|
||||
// Close ranking explanation when clicking outside
|
||||
document.addEventListener("click", (e) => {
|
||||
if (
|
||||
rankingExplanation?.classList.contains("active") &&
|
||||
!rankingExplanation.contains(e.target) &&
|
||||
!rankInfoBtn?.contains(e.target)
|
||||
) {
|
||||
rankingExplanation.classList.remove("active");
|
||||
rankInfoBtn?.classList.remove("active");
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize everything
|
||||
if (leaderboardBody && leaderboardBody.children.length > 0) {
|
||||
initializeRows();
|
||||
calculateOverallRanking();
|
||||
|
||||
// Set initial sort indicator
|
||||
const defaultHeader = document.querySelector('[data-sort="rank"]');
|
||||
if (defaultHeader) {
|
||||
defaultHeader.classList.add("sort-asc");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply dark mode to dynamically created elements
|
||||
function applyDarkModeToElements() {
|
||||
const isDark = html.classList.contains("dark");
|
||||
// Any additional dark mode styling for dynamically created elements can go here
|
||||
}
|
||||
|
||||
// Watch for dark mode changes
|
||||
new MutationObserver(applyDarkModeToElements).observe(html, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,49 @@
|
||||
:root {
|
||||
--bg: #f8f9fa;
|
||||
--card: #fff;
|
||||
--text: #333;
|
||||
--heading: #2c3e50;
|
||||
--heading-secondary: #34495e;
|
||||
--accent: #3498db;
|
||||
--accent-hover: #2980b9;
|
||||
--success: #27ae60;
|
||||
--success-hover: #229954;
|
||||
--error: #e74c3c;
|
||||
--muted: #6c757d;
|
||||
--muted-hover: #5a6268;
|
||||
--border: #ddd;
|
||||
--code-bg: #f4f4f4;
|
||||
--success-bg: #d4edda;
|
||||
--success-text: #155724;
|
||||
--error-bg: #f8d7da;
|
||||
--error-text: #721c24;
|
||||
--hover-bg: #e3f2fd;
|
||||
--shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--bg: #0f172a;
|
||||
--card: #1e293b;
|
||||
--text: #f1f5f9;
|
||||
--heading: #3b82f6;
|
||||
--heading-secondary: #94a3b8;
|
||||
--accent: #3b82f6;
|
||||
--accent-hover: #2563eb;
|
||||
--success: #22c55e;
|
||||
--success-hover: #16a34a;
|
||||
--error: #ef4444;
|
||||
--muted: #64748b;
|
||||
--muted-hover: #475569;
|
||||
--border: #334155;
|
||||
--code-bg: #1e293b;
|
||||
--success-bg: #065f46;
|
||||
--success-text: #d1fae5;
|
||||
--error-bg: #7f1d1d;
|
||||
--error-text: #fecaca;
|
||||
--hover-bg: #1e40af;
|
||||
--shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Reset and base styles */
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -6,39 +52,43 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f8f9fa;
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Main heading */
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
color: var(--heading);
|
||||
margin-bottom: -10px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 3px solid #3498db;
|
||||
border-bottom: 3px solid var(--accent);
|
||||
font-size: 2.2em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #34495e;
|
||||
color: var(--heading-secondary);
|
||||
margin: 30px 0 20px 0;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #34495e;
|
||||
color: var(--heading-secondary);
|
||||
margin: 25px 0 15px 0;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
/* Links and buttons */
|
||||
a {
|
||||
color: #3498db;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
@@ -46,13 +96,13 @@ a {
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: #e3f2fd;
|
||||
background-color: var(--hover-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Primary action link (Submit New Problem) */
|
||||
a[href="/problem/new"] {
|
||||
background-color: #3498db;
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
margin-bottom: 30px;
|
||||
@@ -62,22 +112,23 @@ a[href="/problem/new"] {
|
||||
}
|
||||
|
||||
a[href="/problem/new"]:hover {
|
||||
background-color: #2980b9;
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Problem list */
|
||||
ul {
|
||||
list-style: none;
|
||||
background: white;
|
||||
background: var(--card);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 25px;
|
||||
margin: 20px 0;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
li:last-child {
|
||||
@@ -93,7 +144,7 @@ li a {
|
||||
}
|
||||
|
||||
li a:hover {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--hover-bg);
|
||||
transform: translateX(5px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@@ -107,7 +158,7 @@ li a:hover {
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background-color: #95a5a6;
|
||||
background-color: var(--muted);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
@@ -119,30 +170,32 @@ li a:hover {
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background-color: #7f8c8d;
|
||||
background-color: var(--muted-hover);
|
||||
}
|
||||
|
||||
.problem-desc {
|
||||
background: white;
|
||||
background: var(--card);
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.7;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Editor section */
|
||||
.editor-section {
|
||||
background: white;
|
||||
background: var(--card);
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 30px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#editor {
|
||||
border: 2px solid #ddd;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
height: 400px;
|
||||
@@ -155,7 +208,7 @@ li a:hover {
|
||||
}
|
||||
|
||||
form button[type="submit"] {
|
||||
background-color: #27ae60;
|
||||
background-color: var(--success);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
@@ -167,50 +220,52 @@ form button[type="submit"] {
|
||||
}
|
||||
|
||||
form button[type="submit"]:hover {
|
||||
background-color: #229954;
|
||||
background-color: var(--success-hover);
|
||||
}
|
||||
|
||||
/* Results section */
|
||||
b {
|
||||
color: #2c3e50;
|
||||
color: var(--heading);
|
||||
display: inline-block;
|
||||
margin: 10px 0 5px 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f4f4f4;
|
||||
background-color: var(--code-bg);
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #3498db;
|
||||
border-left: 4px solid var(--accent);
|
||||
margin: 10px 0 20px 0;
|
||||
overflow-x: auto;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-family: "JetBrains Mono", "Courier New", monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
border: 1px solid var(--border);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
pre[style*="color:red"] {
|
||||
border-left-color: #e74c3c;
|
||||
background-color: #fdf2f2;
|
||||
border-left-color: var(--error);
|
||||
background-color: var(--error-bg);
|
||||
}
|
||||
|
||||
/* Status messages */
|
||||
p[style*="color:green"] {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
background-color: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
padding: 15px 20px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #27ae60;
|
||||
border-left: 4px solid var(--success);
|
||||
margin: 20px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p[style*="color:red"] {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
background-color: var(--error-bg);
|
||||
color: var(--error-text);
|
||||
padding: 15px 20px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #e74c3c;
|
||||
border-left: 4px solid var(--error);
|
||||
margin: 20px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -219,7 +274,7 @@ p[style*="color:red"] {
|
||||
a[href="/"] {
|
||||
display: inline-block;
|
||||
margin-top: 30px;
|
||||
background-color: #6c757d;
|
||||
background-color: var(--muted);
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
@@ -227,7 +282,7 @@ a[href="/"] {
|
||||
}
|
||||
|
||||
a[href="/"]:hover {
|
||||
background-color: #5a6268;
|
||||
background-color: var(--muted-hover);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@@ -246,7 +301,9 @@ a[href="/"]:hover {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.problem-desc, .editor-section, ul {
|
||||
.problem-desc,
|
||||
.editor-section,
|
||||
ul {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,56 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Quick Problem Platform</title>
|
||||
<!--<link rel="favicon" href="/favicon/favicon.ico">-->
|
||||
<script src="script.js" async defer></script>
|
||||
<link rel="stylesheet" href="/static/index.css">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Quick Problem Platform</title>
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='index.css') }}">
|
||||
</head>
|
||||
<style>
|
||||
/* Popout explanation */
|
||||
#rankingExplanation {
|
||||
grid-column: 1 / span 2;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease, opacity 0.4s ease, padding 0.4s ease;
|
||||
padding: 0 12px;
|
||||
}
|
||||
#rankingExplanation.active {
|
||||
max-height: 800px;
|
||||
opacity: 1;
|
||||
padding: 12px;
|
||||
}
|
||||
#rankInfoBtn.active { color: #2563eb; cursor:pointer; transition: transform 0.3s ease; }
|
||||
#rankInfoBtn.active { transform: rotate(90deg); }
|
||||
|
||||
/* Highlight top rank */
|
||||
.rank-1 td:first-child { font-weight: bold; }
|
||||
.sort-asc::after { content: " ↑"; }
|
||||
.sort-desc::after { content: " ↓"; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<h1>Quick Problem Platform</h1>
|
||||
<button id="darkModeToggle" class="dark-mode-toggle" title="Toggle dark mode">
|
||||
<span class="dark-mode-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content" id="contentContainer">
|
||||
<!-- Problems -->
|
||||
<section class="card problems-list">
|
||||
<section class="card problems-list">
|
||||
<div class="search-controls">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
id="problemSearch"
|
||||
placeholder="Search problems..."
|
||||
/>
|
||||
<input type="text" class="search-input" id="problemSearch" placeholder="Search problems..." />
|
||||
</div>
|
||||
<h2 style="margin-bottom:6px;font-size:1.1rem">Problems</h2>
|
||||
<div id="problemsContainer">
|
||||
{% for folder, description, test_code, difficulty in problems %}
|
||||
<div class="problem-item" data-name="{{ folder.replace('_',' ').title() }}" data-desc="{{ description }}">
|
||||
<div class="problem-item" data-name="{{ folder.replace('_',' ').title() }}" data-desc="{{ description }}" data-difficulty="{{ difficulty|lower }}">
|
||||
<a href="/problem/{{ folder }}">{{ folder.replace('_',' ').title() }}</a>
|
||||
<span class="difficulty" data-difficulty="{{ difficulty|lower }}">{{ difficulty }}</span>
|
||||
</div>
|
||||
@@ -36,27 +58,22 @@
|
||||
<div class="problem-item">No problems yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
<div class="pagination-controls" id="problemsPagination">
|
||||
<button class="pagination-btn" id="problemsPrevBtn" disabled>← Previous</button>
|
||||
<span class="pagination-info" id="problemsPaginationInfo">Page 1 of 1</span>
|
||||
<button class="pagination-btn" id="problemsNextBtn" disabled>Next →</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Leaderboard -->
|
||||
<section class="card" id="leaderboardSection">
|
||||
<div class="leaderboard-head">
|
||||
<h2 style="font-size:1.1rem;margin:0">Leaderboard</h2>
|
||||
<button class="btn" id="toggleLeaderboard">Hide</button>
|
||||
<h2 style="font-size:1.1rem;margin:0">Leaderboard
|
||||
<span id="rankInfoBtn" title="How ranking works">ℹ️</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="leaderboard-controls">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
id="userSearch"
|
||||
placeholder="Filter by user..."
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
id="problemFilter"
|
||||
placeholder="Filter by problem..."
|
||||
/>
|
||||
<input type="text" class="search-input" id="problemFilter" placeholder="Filter by problem..." />
|
||||
<select class="filter-select" id="runtimeFilter">
|
||||
<option value="">All runtimes</option>
|
||||
<option value="best">Best runtime</option>
|
||||
@@ -83,7 +100,14 @@
|
||||
data-timestamp="{{ entry[5] }}">
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ entry[0] }}</td>
|
||||
<td>{{ problem_titles.get(entry[1], 'Unknown') }}</td>
|
||||
<td>
|
||||
<a href="/problem/{{ problem_titles.get(entry[1], 'Unknown') }}"
|
||||
style="color:#2563eb; text-decoration: none;"
|
||||
onmouseover="this.style.textDecoration='underline';"
|
||||
onmouseout="this.style.textDecoration='none';">
|
||||
{{ problem_titles.get(entry[1], 'Unknown') }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ '%.4f'|format(entry[2]) }}</td>
|
||||
<td>{{ entry[3] }}</td>
|
||||
<td>{{ entry[4] if entry[4] else '-' }}</td>
|
||||
@@ -96,7 +120,27 @@
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Ranking explanation -->
|
||||
<section class="card" id="rankingExplanation">
|
||||
<h2 style="font-size:1.1rem;margin-bottom:6px">How Ranking Works</h2>
|
||||
<p>The leaderboard uses a <strong>weighted scoring system</strong> to determine overall rank:</p>
|
||||
<ul style="margin-left: 15px;">
|
||||
<li><strong>Runtime:</strong> How fast the solution runs (lower is better).</li>
|
||||
<li><strong>Memory Usage:</strong> How much memory the solution uses (lower is better).</li>
|
||||
</ul>
|
||||
<p>Overall score is calculated as:</p>
|
||||
<div style="background:#f9f9f9; border-left:4px solid #007acc; padding:1rem; margin:1rem 0;">
|
||||
<code>
|
||||
runtimeScore = yourRuntime / bestRuntime<br>
|
||||
memoryScore = yourMemory / bestMemory<br>
|
||||
overallScore = runtimeScore × 0.7 + memoryScore × 0.3
|
||||
</code>
|
||||
</div>
|
||||
<p>Lower overall scores are better. If scores are equal, earlier submission ranks higher.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,247 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!doctype html>
|
||||
<html lang="en" class="">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ problem.title }} - Coding Problem</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
min-height: 100vh; /* allow content to grow */
|
||||
overflow-y: auto; /* allow vertical scroll */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*, *::before, *::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* wrap on small screens */
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.problem-panel {
|
||||
flex: 1 1 400px; /* grow/shrink with base 400px */
|
||||
min-width: 300px;
|
||||
background: white;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
border-right: 1px solid #eaeaea;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1 1 400px;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
max-height: 100vh;
|
||||
overflow: hidden; /* internal scroll handling */
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.problem-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-right: 15px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.problem-desc {
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.problem-desc pre {
|
||||
background: #f6f8fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.problem-desc code {
|
||||
background: #f6f8fa;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
padding: 15px 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-actions button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.editor-actions button:hover {
|
||||
background-color: #0069d9;
|
||||
}
|
||||
|
||||
#editor {
|
||||
flex: 1 1 auto;
|
||||
min-height: 300px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.result-panel {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
min-height: 120px;
|
||||
overflow-y: auto;
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
||||
.result-panel h3 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result-panel pre {
|
||||
background: #f6f8fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 14px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.problem-panel, .editor-container {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
max-height: none;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
}
|
||||
#editor {
|
||||
min-height: 400px;
|
||||
max-height: none;
|
||||
}
|
||||
.result-panel {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<link rel="stylesheet" href="/static/problem.css" />
|
||||
<!-- this is stoopid fucking html link for favicon. just cause of flask-->
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href="{{ url_for('static', filename='favicon.ico') }}"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.cdnfonts.com/css/jetbrains-mono"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<div class="problem-panel">
|
||||
<div class="problem-header">
|
||||
<button class="back-btn" onclick="window.location.href='/'">← Back</button>
|
||||
<button class="back-btn" onclick="window.location.href='/'">
|
||||
← Back
|
||||
</button>
|
||||
<h1>{{ problem.title }}</h1>
|
||||
<button
|
||||
id="darkModeToggle"
|
||||
class="dark-mode-toggle"
|
||||
title="Toggle dark mode"
|
||||
>
|
||||
<span class="dark-mode-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="problem-desc">
|
||||
{{ problem.description | safe | markdown }}
|
||||
</div>
|
||||
<div class="problem-desc">{{ problem.description | safe | markdown }}</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<div class="editor-header">
|
||||
<h2 style="margin:0;font-size:18px;">Submit Your Solution (Python)</h2>
|
||||
<h2 style="margin: 0; font-size: 18px">
|
||||
Submit Your Solution (Python)
|
||||
</h2>
|
||||
</div>
|
||||
<div class="editor-wrapper">
|
||||
<form method="post">
|
||||
<label for="username">Username (optional):</label>
|
||||
<input type="text" name="username" id="username" placeholder="Anonymous">
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Anonymous"
|
||||
/>
|
||||
<div id="editor"></div>
|
||||
<textarea name="user_code" id="user_code" style="display:none;"></textarea>
|
||||
<textarea
|
||||
name="user_code"
|
||||
id="user_code"
|
||||
style="display: none"
|
||||
></textarea>
|
||||
<div class="editor-actions">
|
||||
<button type="submit">Run & Submit</button>
|
||||
</div>
|
||||
@@ -250,14 +71,16 @@ input[type="text"] {
|
||||
<div class="result-panel">
|
||||
<h3>Result</h3>
|
||||
{% if result %}
|
||||
<p><b>Runtime:</b> {{ '%.4f'|format(result.runtime) }} seconds</p>
|
||||
<p>
|
||||
<b>Runtime:</b> {{ '%.4f'|format(result.runtime) }}
|
||||
seconds
|
||||
</p>
|
||||
<p><b>Output:</b></p>
|
||||
<pre>{{ result.output }}</pre>
|
||||
{% if result.error %}
|
||||
<p><b>Error:</b></p>
|
||||
<pre>{{ result.error }}</pre>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% endif %} {% else %}
|
||||
<div class="placeholder">
|
||||
Your code execution results will appear here
|
||||
</div>
|
||||
@@ -269,28 +92,67 @@ input[type="text"] {
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
||||
<script>
|
||||
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
|
||||
require(['vs/editor/editor.main'], function() {
|
||||
var editor = monaco.editor.create(document.getElementById('editor'), {
|
||||
value: '',
|
||||
language: 'python',
|
||||
theme: 'vs-light',
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
// Dark mode functionality
|
||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||
const html = document.documentElement;
|
||||
|
||||
// Load saved dark mode preference
|
||||
const savedDarkMode = localStorage.getItem("darkMode");
|
||||
if (
|
||||
savedDarkMode === "true" ||
|
||||
(savedDarkMode === null &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
html.classList.add("dark");
|
||||
}
|
||||
|
||||
darkModeToggle.addEventListener("click", () => {
|
||||
html.classList.toggle("dark");
|
||||
localStorage.setItem(
|
||||
"darkMode",
|
||||
html.classList.contains("dark"),
|
||||
);
|
||||
// Update Monaco editor theme
|
||||
if (window.monacoEditor) {
|
||||
const isDark = html.classList.contains("dark");
|
||||
window.monacoEditor.updateOptions({
|
||||
theme: isDark ? "vs-dark" : "vs-light",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
require.config({
|
||||
paths: {
|
||||
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs",
|
||||
},
|
||||
});
|
||||
require(["vs/editor/editor.main"], function () {
|
||||
const isDark = html.classList.contains("dark");
|
||||
window.monacoEditor = monaco.editor.create(
|
||||
document.getElementById("editor"),
|
||||
{
|
||||
value: "",
|
||||
language: "python",
|
||||
theme: isDark ? "vs-dark" : "vs-light",
|
||||
fontFamily: "JetBrains Mono, monospace",
|
||||
fontLigatures: true,
|
||||
automaticLayout: true,
|
||||
fontSize: 16,
|
||||
minimap: { enabled: false }
|
||||
});
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
var code = editor.getValue();
|
||||
minimap: { enabled: false },
|
||||
},
|
||||
);
|
||||
document
|
||||
.querySelector("form")
|
||||
.addEventListener("submit", function (e) {
|
||||
var code = window.monacoEditor.getValue();
|
||||
if (!code.trim()) {
|
||||
alert('Please enter your code before submitting.');
|
||||
alert("Please enter your code before submitting.");
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
document.getElementById('user_code').value = code;
|
||||
document.getElementById("user_code").value = code;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,152 +0,0 @@
|
||||
// Toggle leaderboard visibility
|
||||
const toggleBtn = document.getElementById('toggleLeaderboard');
|
||||
const leaderboardSection = document.getElementById('leaderboardSection');
|
||||
const contentContainer = document.getElementById('contentContainer');
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
if (leaderboardSection.style.display === 'none') {
|
||||
leaderboardSection.style.display = '';
|
||||
toggleBtn.textContent = 'Hide';
|
||||
contentContainer.classList.remove('single-column');
|
||||
} else {
|
||||
leaderboardSection.style.display = 'none';
|
||||
toggleBtn.textContent = 'Show';
|
||||
contentContainer.classList.add('single-column');
|
||||
}
|
||||
});
|
||||
|
||||
// Problem search functionality
|
||||
const problemSearch = document.getElementById('problemSearch');
|
||||
const problemsContainer = document.getElementById('problemsContainer');
|
||||
const problemItems = problemsContainer.querySelectorAll('.problem-item');
|
||||
|
||||
problemSearch.addEventListener('input', () => {
|
||||
const searchTerm = problemSearch.value.toLowerCase();
|
||||
problemItems.forEach(item => {
|
||||
const name = item.dataset.name.toLowerCase();
|
||||
const desc = item.dataset.desc?.toLowerCase() || '';
|
||||
if (name.includes(searchTerm) || desc.includes(searchTerm)) {
|
||||
item.style.display = '';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Leaderboard filtering and sorting
|
||||
const userSearch = document.getElementById('userSearch');
|
||||
const problemFilter = document.getElementById('problemFilter');
|
||||
const runtimeFilter = document.getElementById('runtimeFilter');
|
||||
const leaderboardBody = document.getElementById('leaderboardBody');
|
||||
const leaderboardRows = Array.from(leaderboardBody.querySelectorAll('tr'));
|
||||
const sortableHeaders = document.querySelectorAll('.sortable');
|
||||
|
||||
// Current sort state
|
||||
let currentSort = {
|
||||
column: null,
|
||||
direction: 'asc'
|
||||
};
|
||||
|
||||
// Filter leaderboard
|
||||
function filterLeaderboard() {
|
||||
const userTerm = userSearch.value.toLowerCase();
|
||||
const problemTerm = problemFilter.value.toLowerCase();
|
||||
const runtimeType = runtimeFilter.value;
|
||||
|
||||
leaderboardRows.forEach(row => {
|
||||
const user = row.dataset.user.toLowerCase();
|
||||
const problem = row.dataset.problem.toLowerCase();
|
||||
const runtime = parseFloat(row.dataset.runtime);
|
||||
const showUser = user.includes(userTerm);
|
||||
const showProblem = problem.includes(problemTerm);
|
||||
|
||||
let showRuntime = true;
|
||||
if (runtimeType === 'best') {
|
||||
// Find if this is the best runtime for this user+problem combo
|
||||
const userProblemRows = leaderboardRows.filter(r =>
|
||||
r.dataset.user === row.dataset.user &&
|
||||
r.dataset.problem === row.dataset.problem
|
||||
);
|
||||
const bestRuntime = Math.min(...userProblemRows.map(r => parseFloat(r.dataset.runtime)));
|
||||
showRuntime = runtime === bestRuntime;
|
||||
} else if (runtimeType === 'worst') {
|
||||
// Find if this is the worst runtime for this user+problem combo
|
||||
const userProblemRows = leaderboardRows.filter(r =>
|
||||
r.dataset.user === row.dataset.user &&
|
||||
r.dataset.problem === row.dataset.problem
|
||||
);
|
||||
const worstRuntime = Math.max(...userProblemRows.map(r => parseFloat(r.dataset.runtime)));
|
||||
showRuntime = runtime === worstRuntime;
|
||||
}
|
||||
|
||||
if (showUser && showProblem && showRuntime) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sort leaderboard
|
||||
function sortLeaderboard(column, direction) {
|
||||
const rows = Array.from(leaderboardBody.querySelectorAll('tr'));
|
||||
const index = Array.from(document.querySelectorAll('th')).findIndex(th => th.dataset.sort === column);
|
||||
|
||||
rows.sort((a, b) => {
|
||||
let aValue = a.cells[index].textContent;
|
||||
let bValue = b.cells[index].textContent;
|
||||
|
||||
// Special handling for numeric columns
|
||||
if (column === 'runtime' || column === 'memory' || column === 'rank') {
|
||||
aValue = parseFloat(aValue) || 0;
|
||||
bValue = parseFloat(bValue) || 0;
|
||||
return direction === 'asc' ? aValue - bValue : bValue - aValue;
|
||||
}
|
||||
|
||||
// Special handling for timestamps
|
||||
if (column === 'timestamp') {
|
||||
aValue = new Date(aValue).getTime();
|
||||
bValue = new Date(bValue).getTime();
|
||||
return direction === 'asc' ? aValue - bValue : bValue - aValue;
|
||||
}
|
||||
|
||||
// Default string comparison
|
||||
aValue = aValue.toLowerCase();
|
||||
bValue = bValue.toLowerCase();
|
||||
if (aValue < bValue) return direction === 'asc' ? -1 : 1;
|
||||
if (aValue > bValue) return direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Re-append rows in sorted order
|
||||
rows.forEach(row => leaderboardBody.appendChild(row));
|
||||
}
|
||||
|
||||
// Set up event listeners
|
||||
userSearch.addEventListener('input', filterLeaderboard);
|
||||
problemFilter.addEventListener('input', filterLeaderboard);
|
||||
runtimeFilter.addEventListener('change', filterLeaderboard);
|
||||
|
||||
// Set up sorting
|
||||
sortableHeaders.forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
const column = header.dataset.sort;
|
||||
|
||||
// Reset all sort indicators
|
||||
sortableHeaders.forEach(h => {
|
||||
h.classList.remove('sort-asc', 'sort-desc');
|
||||
});
|
||||
|
||||
// Determine new sort direction
|
||||
if (currentSort.column === column) {
|
||||
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
currentSort.column = column;
|
||||
currentSort.direction = 'asc';
|
||||
}
|
||||
|
||||
// Apply new sort
|
||||
header.classList.add(`sort-${currentSort.direction}`);
|
||||
sortLeaderboard(column, currentSort.direction);
|
||||
});
|
||||
});
|
||||
225
src/utils.py
225
src/utils.py
@@ -5,8 +5,127 @@ import io
|
||||
import tempfile
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
import ast
|
||||
|
||||
# Security configuration
|
||||
ALLOWED_IMPORTS = {
|
||||
'math', 'random', 'datetime', 'json', 'collections', 'itertools',
|
||||
'functools', 'operator', 'copy', 'unittest', 're', 'string'
|
||||
}
|
||||
|
||||
DANGEROUS_PATTERNS = [
|
||||
r'import\s+os(?:\s|$|\.)',
|
||||
r'from\s+os\s+import',
|
||||
r'import\s+subprocess(?:\s|$|\.)',
|
||||
r'from\s+subprocess\s+import',
|
||||
r'import\s+sys(?:\s|$|\.)',
|
||||
r'from\s+sys\s+import',
|
||||
r'import\s+shutil(?:\s|$|\.)',
|
||||
r'from\s+shutil\s+import',
|
||||
r'import\s+pathlib(?:\s|$|\.)',
|
||||
r'from\s+pathlib\s+import',
|
||||
r'__import__\s*\(',
|
||||
r'exec\s*\(',
|
||||
r'eval\s*\(',
|
||||
r'compile\s*\(',
|
||||
r'open\s*\(',
|
||||
r'file\s*\(',
|
||||
r'input\s*\(',
|
||||
r'raw_input\s*\(',
|
||||
r'\.unlink\s*\(',
|
||||
r'\.remove\s*\(',
|
||||
r'\.rmdir\s*\(',
|
||||
r'\.rmtree\s*\(',
|
||||
r'\.delete\s*\(',
|
||||
r'\.kill\s*\(',
|
||||
r'\.terminate\s*\(',
|
||||
]
|
||||
|
||||
def validate_code_security(code):
|
||||
"""
|
||||
Validates code for security issues.
|
||||
Returns (is_safe, error_message)
|
||||
"""
|
||||
# Check for dangerous patterns
|
||||
for pattern in DANGEROUS_PATTERNS:
|
||||
if re.search(pattern, code, re.IGNORECASE):
|
||||
return False, f"Dangerous operation detected: {pattern}"
|
||||
|
||||
# Parse AST to check imports
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
module_name = alias.name.split('.')[0]
|
||||
if module_name not in ALLOWED_IMPORTS:
|
||||
return False, f"Import not allowed: {module_name}"
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
module_name = node.module.split('.')[0]
|
||||
if module_name not in ALLOWED_IMPORTS:
|
||||
return False, f"Import not allowed: {module_name}"
|
||||
except SyntaxError as e:
|
||||
return False, f"Syntax error in code: {str(e)}"
|
||||
|
||||
return True, None
|
||||
|
||||
def create_restricted_globals():
|
||||
"""Create a restricted global namespace for code execution."""
|
||||
safe_builtins = {
|
||||
'abs', 'all', 'any', 'bin', 'bool', 'chr', 'dict', 'dir',
|
||||
'divmod', 'enumerate', 'filter', 'float', 'format', 'frozenset',
|
||||
'hex', 'id', 'int', 'isinstance', 'issubclass', 'iter', 'len',
|
||||
'list', 'map', 'max', 'min', 'next', 'oct', 'ord', 'pow',
|
||||
'print', 'range', 'repr', 'reversed', 'round', 'set', 'slice',
|
||||
'sorted', 'str', 'sum', 'tuple', 'type', 'zip'
|
||||
}
|
||||
|
||||
restricted_globals = {
|
||||
'__builtins__': {name: __builtins__[name] for name in safe_builtins if name in __builtins__}
|
||||
}
|
||||
|
||||
# Add allowed modules
|
||||
for module in ALLOWED_IMPORTS:
|
||||
try:
|
||||
restricted_globals[module] = __import__(module)
|
||||
except ImportError:
|
||||
pass # Module not available
|
||||
|
||||
return restricted_globals
|
||||
|
||||
def run_code_against_tests(user_code, test_code, max_execution_time=5):
|
||||
"""
|
||||
Securely run user code against test code with safety restrictions.
|
||||
|
||||
Args:
|
||||
user_code: The user's solution code
|
||||
test_code: The test code to validate the solution
|
||||
max_execution_time: Maximum execution time in seconds (default: 5)
|
||||
|
||||
Returns:
|
||||
dict: Result containing passed, output, runtime, and error information
|
||||
"""
|
||||
# Validate security for both user code and test code
|
||||
user_safe, user_error = validate_code_security(user_code)
|
||||
if not user_safe:
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': f"Security violation in user code: {user_error}"
|
||||
}
|
||||
|
||||
test_safe, test_error = validate_code_security(test_code)
|
||||
if not test_safe:
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': f"Security violation in test code: {test_error}"
|
||||
}
|
||||
|
||||
def run_code_against_tests(user_code, test_code):
|
||||
local_ns = {}
|
||||
output = ''
|
||||
start = time.perf_counter()
|
||||
@@ -17,60 +136,83 @@ def run_code_against_tests(user_code, test_code):
|
||||
try:
|
||||
# Check if unittest is used in test_code
|
||||
if 'unittest' in test_code:
|
||||
# Write user code + test code to a temp file
|
||||
with tempfile.NamedTemporaryFile('w+', suffix='.py', delete=False, encoding='utf-8') as f:
|
||||
# Create temp file in a secure temporary directory
|
||||
temp_dir = tempfile.mkdtemp(prefix='secure_code_')
|
||||
try:
|
||||
temp_file = os.path.join(temp_dir, 'test_code.py')
|
||||
combined_code = f"{user_code}\n\n{test_code}"
|
||||
f.write(combined_code)
|
||||
f.flush()
|
||||
temp_file = f.name
|
||||
|
||||
# Run the file as a subprocess
|
||||
# Write to temp file with restricted permissions
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
f.write(combined_code)
|
||||
os.chmod(temp_file, 0o600) # Read/write for owner only
|
||||
|
||||
# Run the file as a subprocess with additional security
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, temp_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
encoding='utf-8'
|
||||
timeout=max_execution_time,
|
||||
encoding='utf-8',
|
||||
cwd=temp_dir, # Run in the temporary directory
|
||||
env={'PYTHONPATH': ''} # Restrict Python path
|
||||
)
|
||||
output = proc.stdout
|
||||
if proc.stderr:
|
||||
output += f"\n{proc.stderr}"
|
||||
|
||||
# Combine both stdout and stderr to capture all output
|
||||
combined_output = ""
|
||||
if proc.stdout:
|
||||
combined_output += proc.stdout
|
||||
if proc.stderr:
|
||||
if combined_output:
|
||||
combined_output += "\n" + proc.stderr
|
||||
else:
|
||||
combined_output = proc.stderr
|
||||
|
||||
output = combined_output
|
||||
passed = proc.returncode == 0
|
||||
|
||||
if not passed:
|
||||
error = f"Tests failed. Return code: {proc.returncode}\n{output}"
|
||||
else:
|
||||
# For successful unittest runs, the stderr contains the test results
|
||||
if proc.stderr and "OK" in proc.stderr:
|
||||
output = proc.stderr # Use stderr as the main output for unittest
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
passed = False
|
||||
error = "Code execution timed out after 10 seconds"
|
||||
error = f"Code execution timed out after {max_execution_time} seconds"
|
||||
output = "Execution timed out"
|
||||
|
||||
finally:
|
||||
# Secure cleanup of temporary directory and files
|
||||
try:
|
||||
if temp_file and os.path.exists(temp_file):
|
||||
os.chmod(temp_file, 0o600) # Ensure we can delete
|
||||
os.unlink(temp_file)
|
||||
if os.path.exists(temp_dir):
|
||||
os.rmdir(temp_dir)
|
||||
except Exception as cleanup_error:
|
||||
print(f"Warning: Could not clean up temp files: {cleanup_error}")
|
||||
else:
|
||||
# Capture stdout
|
||||
# Direct execution with restricted globals
|
||||
old_stdout = sys.stdout
|
||||
captured_output = io.StringIO()
|
||||
sys.stdout = captured_output
|
||||
|
||||
try:
|
||||
# Execute user code
|
||||
exec(user_code, {}, local_ns)
|
||||
# Create restricted execution environment
|
||||
restricted_globals = create_restricted_globals()
|
||||
|
||||
# Execute user code in restricted environment
|
||||
exec(user_code, restricted_globals, local_ns)
|
||||
|
||||
# Execute test code (should raise AssertionError if fail)
|
||||
exec(test_code, local_ns, local_ns)
|
||||
exec(test_code, {**restricted_globals, **local_ns}, local_ns)
|
||||
passed = True
|
||||
|
||||
except AssertionError as e:
|
||||
passed = False
|
||||
error = f"Assertion failed: {str(e)}"
|
||||
|
||||
except Exception as e:
|
||||
passed = False
|
||||
error = f"Runtime error: {traceback.format_exc()}"
|
||||
|
||||
finally:
|
||||
output = captured_output.getvalue()
|
||||
sys.stdout = old_stdout
|
||||
@@ -79,16 +221,13 @@ def run_code_against_tests(user_code, test_code):
|
||||
passed = False
|
||||
error = f"Execution error: {traceback.format_exc()}"
|
||||
|
||||
finally:
|
||||
# Clean up temporary file
|
||||
if temp_file and os.path.exists(temp_file):
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not delete temp file {temp_file}: {e}")
|
||||
|
||||
runtime = time.perf_counter() - start
|
||||
|
||||
# Limit output size to prevent memory issues
|
||||
max_output_size = 10000 # 10KB limit
|
||||
if len(output) > max_output_size:
|
||||
output = output[:max_output_size] + "\n... (output truncated)"
|
||||
|
||||
result = {
|
||||
'passed': passed,
|
||||
'output': output.strip() if output else '',
|
||||
@@ -97,3 +236,27 @@ def run_code_against_tests(user_code, test_code):
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
# Example usage with additional safety wrapper
|
||||
def safe_code_runner(user_code, test_code):
|
||||
"""
|
||||
Additional wrapper for extra safety checks.
|
||||
"""
|
||||
# Additional length checks
|
||||
if len(user_code) > 50000: # 50KB limit
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': "User code too large (maximum 50KB allowed)"
|
||||
}
|
||||
|
||||
if len(test_code) > 10000: # 10KB limit for test code
|
||||
return {
|
||||
'passed': False,
|
||||
'output': '',
|
||||
'runtime': 0,
|
||||
'error': "Test code too large (maximum 10KB allowed)"
|
||||
}
|
||||
|
||||
return run_code_against_tests(user_code, test_code)
|
||||
Reference in New Issue
Block a user