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
170 changes: 170 additions & 0 deletions frontend/js/user/goal-setter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
(function () {
function getUsername() {
return window.location.pathname.split("/").filter(Boolean).pop() || "";
}

function storageKey(username) {
return "goal_" + username.toLowerCase();
}

function getMonday(date) {
const d = new Date(date);
const day = d.getDay();
const diff = day === 0 ? -6 : 1 - day;
d.setDate(d.getDate() + diff);
d.setHours(0, 0, 0, 0);
return d;
}

function loadGoal(username) {
try {
const raw = localStorage.getItem(storageKey(username));
if (!raw) return null;
const goal = JSON.parse(raw);
const monday = getMonday(new Date()).getTime();
if (!goal.weekStart || goal.weekStart < monday) {
goal.weekStart = monday;
goal.baseCount = null;
localStorage.setItem(storageKey(username), JSON.stringify(goal));
}
return goal;
} catch {
return null;
}
}

function saveGoal(username, metric, target) {
const goal = {
metric,
target: parseInt(target, 10),
weekStart: getMonday(new Date()).getTime(),
baseCount: null,
};
localStorage.setItem(storageKey(username), JSON.stringify(goal));
return goal;
}

function deleteGoal(username) {
localStorage.removeItem(storageKey(username));
}

function renderBar(current, target) {
const BAR = 20;
const pct = Math.min(Math.floor((current / target) * 100), 100);
const filled = Math.min(Math.floor((current / target) * BAR), BAR);
const empty = BAR - filled;
let bar;
if (filled >= BAR) {
bar = "=".repeat(BAR);
} else {
bar = "=".repeat(filled) + ">" + ".".repeat(empty - 1);
}
return "[" + bar + "] " + pct + "%";
}

const CELEBRATIONS = [
">> TARGET_REACHED :: WEEK_COMPLETE",
">> GOAL_MET :: COMMIT_LOGGED",
">> WEEKLY_TARGET :: ACHIEVED",
];

function init() {
const username = getUsername();
if (!username) return;

const elDisplay = document.getElementById("goal-display");
const elSetter = document.getElementById("goal-setter");
const elBar = document.getElementById("goal-bar");
const elLabel = document.getElementById("goal-label");
const elCount = document.getElementById("goal-count");
const elCelebration = document.getElementById("goal-celebration");
const elCelebrateText = document.getElementById("celebrate-text");
const elReset = document.getElementById("btn-reset-goal");
const elSaveBtn = document.getElementById("btn-save-goal");
const elInput = document.getElementById("goal-target-input");
const elMetric = document.getElementById("goal-metric");

function showProgress(goal, solvedCount) {
if (goal.baseCount === null || goal.baseCount === undefined) {
goal.baseCount = solvedCount;
localStorage.setItem(storageKey(username), JSON.stringify(goal));
}
const progress = Math.max(0, solvedCount - goal.baseCount);

elLabel.textContent =
"TARGET: " + goal.target + " " + goal.metric.toUpperCase() + " / WEEK";
elBar.textContent = renderBar(progress, goal.target);
elCount.textContent =
progress + " / " + goal.target + " solved this week";

if (progress >= goal.target) {
elCelebration.style.display = "block";
elCelebrateText.textContent =
CELEBRATIONS[Math.floor(Math.random() * CELEBRATIONS.length)];
elBar.style.color = "var(--gold)";
} else {
elCelebration.style.display = "none";
elBar.style.color = "var(--green)";
}

elDisplay.style.display = "block";
elSetter.style.display = "none";
}

function showSetter() {
elDisplay.style.display = "none";
elSetter.style.display = "block";
}

async function fetchSolvedCount(metric) {
try {
const res = await fetch("/api/user/" + username);
if (!res.ok) return 0;
const data = await res.json();
if (metric === "easy") return data.easySolved ?? 0;
if (metric === "medium") return data.mediumSolved ?? 0;
if (metric === "hard") return data.hardSolved ?? 0;
return data.totalSolved ?? 0;
} catch {
return 0;
}
}

async function render() {
const goal = loadGoal(username);
if (goal) {
const solved = await fetchSolvedCount(goal.metric);
showProgress(goal, solved);
} else {
showSetter();
}
}

elSaveBtn.addEventListener("click", async function () {
const metric = elMetric.value;
const target = parseInt(elInput.value, 10);
if (!target || target < 1) {
elInput.style.borderColor = "#ff4c4c";
return;
}
elInput.style.borderColor = "";
const goal = saveGoal(username, metric, target);
const solved = await fetchSolvedCount(metric);
showProgress(goal, solved);
});

elReset.addEventListener("click", function () {
deleteGoal(username);
elInput.value = "";
showSetter();
});

render();
}

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
98 changes: 98 additions & 0 deletions frontend/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,103 @@ <h1 id="username-display" class="page-title">[USER_PROFILE]</h1>
<span class="card-title">[SYS_TARGETS]</span>
<span style="color: var(--text-dim)">// LOCAL_STORE</span>
</div>

<!-- progress view -->
<div id="goal-display" style="display: none">
<div
id="goal-label"
style="
font-family: &quot;Space Mono&quot;, monospace;
font-size: 0.78rem;
color: var(--text-dim);
margin-bottom: 0.4rem;
"
>
TARGET: --
</div>
<div
id="goal-bar"
style="
font-family: &quot;Fira Code&quot;, monospace;
font-size: 0.85rem;
color: var(--green);
letter-spacing: 1px;
margin-bottom: 0.4rem;
word-break: break-all;
"
>
[....................] 0%
</div>
<div
id="goal-count"
style="
font-size: 0.78rem;
color: var(--text-dim);
font-family: &quot;Space Mono&quot;, monospace;
margin-bottom: 0.8rem;
"
>
0 / 0 solved this week
</div>
<div
id="goal-celebration"
style="display: none; margin-bottom: 0.6rem"
>
<span
id="celebrate-text"
style="
color: var(--gold);
font-family: &quot;Fira Code&quot;, monospace;
font-size: 0.82rem;
text-shadow: 0 0 8px rgba(255, 215, 0, 0.6);
"
></span>
</div>
<button
id="btn-reset-goal"
class="btn-save"
style="font-size: 0.78rem"
>
[RESET_TARGET]
</button>
</div>

<!-- set goal view -->
<div id="goal-setter" style="display: none">
<div
style="
font-size: 0.78rem;
color: var(--text-dim);
font-family: &quot;Space Mono&quot;, monospace;
margin-bottom: 0.6rem;
"
>
$ set weekly target
</div>
<div style="display: flex; gap: 0.5rem; margin-bottom: 0.5rem">
<select
id="goal-metric"
class="goal-input"
style="flex: 1; cursor: pointer"
>
<option value="total">TOTAL</option>
<option value="easy">EASY</option>
<option value="medium">MEDIUM</option>
<option value="hard">HARD</option>
</select>
</div>
<div class="goal-input-wrapper">
<input
id="goal-target-input"
class="goal-input"
type="number"
min="1"
max="500"
placeholder="e.g. 10"
/>
<button id="btn-save-goal" class="btn-save">[SET]</button>
</div>
</div>
</div>

<!-- Badge Wall -->
Expand Down Expand Up @@ -475,5 +572,6 @@ <h1 id="username-display" class="page-title">[USER_PROFILE]</h1>
<script src="/js/footer.js"></script>
<script src="/js/matrix.js"></script>
<script src="/js/terminal_ui.js"></script>
<script src="/js/user/goal-setter.js"></script>
</body>
</html>
Loading