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
11 changes: 11 additions & 0 deletions devboard/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions devboard/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "devboard-client",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
Expand All @@ -10,6 +11,7 @@
"dependencies": {
"@hello-pangea/dnd": "^16.3.0",
"axios": "^1.5.0",
"canvas-confetti": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.16.0",
Expand Down
82 changes: 73 additions & 9 deletions devboard/client/src/components/Task/TaskModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,45 @@ const TaskModal = ({
const [snippetLang, setSnippetLang] = useState("javascript");
const [loading, setLoading] = useState(false);

// AI Loading & Error States
const [isGenerating, setIsGenerating] = useState(false);
const [aiError, setAiError] = useState("");

const handleGenerateAI = async () => {
if (!form.title.trim()) {
setAiError("Please enter a task title first.");
return;
}

setIsGenerating(true);
setAiError("");

try {
const res = await fetch("/api/ai/generate-description", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title: form.title }),
});

const data = await res.json();

if (!res.ok) {
throw new Error(data.error || "Failed to generate description.");
}

setForm((prev) => ({
...prev,
description: data.description,
}));
} catch (err) {
setAiError(err.message || "Error generating AI description.");
} finally {
setIsGenerating(false);
}
};

const handleSave = async () => {
if (!form.title.trim()) return;
setLoading(true);
Expand All @@ -39,6 +78,7 @@ const TaskModal = ({
};
await onSave(payload);
setLoading(false);
onClose();
};

return (
Expand All @@ -61,17 +101,41 @@ const TaskModal = ({
type="text"
placeholder="Task title *"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
onChange={(e) => {
setForm({ ...form, title: e.target.value });
if (aiError) setAiError("");
}}
className="w-full bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg px-3 py-2 text-sm text-[#f0f0f0] placeholder-[#555] focus:outline-none focus:border-purple-500"
/>

<textarea
placeholder="Description (optional)"
value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })}
rows={3}
className="w-full bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg px-3 py-2 text-sm text-[#f0f0f0] placeholder-[#555] focus:outline-none focus:border-purple-500 resize-none"
/>
{/* Description Section with ✨ AI Button */}
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="text-xs text-[#888]">Description</label>
<button
type="button"
onClick={handleGenerateAI}
disabled={isGenerating || !form.title.trim()}
className="text-xs font-medium text-purple-400 hover:text-purple-300 disabled:opacity-40 transition flex items-center gap-1 bg-purple-950/40 border border-purple-800/50 hover:border-purple-600 px-2.5 py-1 rounded-md"
>
{isGenerating ? "✨ Generating..." : "✨ Generate with AI"}
</button>
</div>

<textarea
placeholder="Description (optional) or generate with AI..."
value={form.description}
onChange={(e) =>
setForm({ ...form, description: e.target.value })
}
rows={3}
className="w-full bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg px-3 py-2 text-sm text-[#f0f0f0] placeholder-[#555] focus:outline-none focus:border-purple-500 resize-none"
/>

{aiError && (
<p className="text-xs text-red-400 mt-1">{aiError}</p>
)}
</div>

<div className="flex gap-2">
<select
Expand Down Expand Up @@ -190,4 +254,4 @@ const TaskModal = ({
);
};

export default TaskModal;
export default TaskModal;
52 changes: 42 additions & 10 deletions devboard/client/src/context/BoardContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,57 @@ export const BoardProvider = ({ children }) => {
const [allTasks, setAllTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");

const [activeTag, setActiveTag] = useState(null);

const [user, setUser] = useState(() => {
const saved = localStorage.getItem("devboard_user");
return saved ? JSON.parse(saved) : null;
try {
return saved ? JSON.parse(saved) : null;
} catch {
return null;
}
});

const authHeaders = () => ({
headers: { Authorization: `Bearer ${user?.token}` },
});
// Safe Token Extractor
const getToken = () => {
if (!user) return null;
return user.token || user.jwt || (typeof user === "string" ? user : null);
};

const authHeaders = () => {
const token = getToken();
return {
headers: { Authorization: token ? `Bearer ${token}` : "" },
};
};

const fetchTasks = async () => {
if (!user) return;
const token = getToken();
if (!token) {
setLoading(false);
return;
}

try {
setLoading(true);
const { data } = await axios.get("/api/tasks", authHeaders());
setAllTasks(data);
} catch (err) {
console.error(err);
console.error("Error fetching tasks:", err);
} finally {
setLoading(false);
}
};

useEffect(() => { fetchTasks(); }, [user]);
useEffect(() => {
if (user) {
fetchTasks();
} else {
setAllTasks([]);
setLoading(false);
}
}, [user]);

const addTask = async (taskData) => {
const { data } = await axios.post("/api/tasks", taskData, authHeaders());
Expand All @@ -59,8 +86,13 @@ export const BoardProvider = ({ children }) => {
};

const login = (userData) => {
setUser(userData);
localStorage.setItem("devboard_user", JSON.stringify(userData));
// Standardize user object structure
const formattedUser = userData.token
? userData
: { token: userData.token || userData.jwt, ...userData };

setUser(formattedUser);
localStorage.setItem("devboard_user", JSON.stringify(formattedUser));
};

const logout = () => {
Expand Down Expand Up @@ -130,4 +162,4 @@ export const BoardProvider = ({ children }) => {
);
};

export const useBoard = () => useContext(BoardContext);
export const useBoard = () => useContext(BoardContext);
35 changes: 33 additions & 2 deletions devboard/client/src/hooks/usePomodoro.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ const WORK_DURATION = 25 * 60; // 25 minutes
const BREAK_DURATION = 5 * 60; // 5 minutes
const LONG_BREAK_DURATION = 15 * 60; // 15 minutes

// 🎵 Web Audio API helper function for soft ding sound
const playSoftDing = () => {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;

const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();

osc.type = "sine";
osc.frequency.setValueAtTime(587.33, ctx.currentTime);


gain.gain.setValueAtTime(0.15, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.4);

osc.connect(gain);
gain.connect(ctx.destination);

osc.start();
osc.stop(ctx.currentTime + 0.4);
} catch (err) {
console.error("Audio playback error:", err);
}
};

export const usePomodoro = (onSessionComplete) => {
const [timeLeft, setTimeLeft] = useState(WORK_DURATION);
const [isRunning, setIsRunning] = useState(false);
Expand All @@ -19,11 +46,15 @@ export const usePomodoro = (onSessionComplete) => {
setTimeLeft((prev) => {
if (prev <= 1) {
clearInterval(intervalRef.current);

// 🚨 Play sound alert when timer reaches 0
playSoftDing();

setIsRunning(false);
if (!isBreak) {
const nextCount = sessionCount + 1;
setSessionCount(nextCount);
localStorage.setItem("pom_sessions", sessionCount + 1);
localStorage.setItem("pom_sessions", nextCount);
if (onSessionComplete) onSessionComplete();
setIsBreak(true);
const isLongBreak = nextCount % 4 === 0 && nextCount > 0;
Expand Down Expand Up @@ -78,4 +109,4 @@ export const usePomodoro = (onSessionComplete) => {
format,
progress,
};
};
};
Loading