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
17 changes: 10 additions & 7 deletions src/app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1127,20 +1127,20 @@ function SettingsPageContent() {
<button
type="button"
onClick={() => handleMovePin(index, "up")}
disabled={index === 0}
disabled={index === 0 || saving}
title="Move Up"
aria-label={`Move ${repoName} up`}
className="p-1.5 rounded-lg border border-[var(--border)] hover:bg-[var(--control-hover)] text-[var(--card-foreground)] disabled:opacity-40"
className="p-1.5 rounded-lg border border-[var(--border)] hover:bg-[var(--control-hover)] text-[var(--card-foreground)] disabled:opacity-40 disabled:cursor-not-allowed"
>
</button>
<button
type="button"
onClick={() => handleMovePin(index, "down")}
disabled={index === (settings.pinned_repos || []).length - 1}
disabled={index === (settings.pinned_repos || []).length - 1 || saving}
title="Move Down"
aria-label={`Move ${repoName} down`}
className="p-1.5 rounded-lg border border-[var(--border)] hover:bg-[var(--control-hover)] text-[var(--card-foreground)] disabled:opacity-40"
className="p-1.5 rounded-lg border border-[var(--border)] hover:bg-[var(--control-hover)] text-[var(--card-foreground)] disabled:opacity-40 disabled:cursor-not-allowed"
>
</button>
Expand All @@ -1149,8 +1149,9 @@ function SettingsPageContent() {
<button
type="button"
onClick={() => handleUnpinRepo(repoName)}
disabled={saving}
aria-label={`Unpin ${repoName}`}
className="ml-2 rounded-lg border border-[var(--destructive-muted-border)] hover:bg-[var(--destructive-muted)] px-3 py-1.5 text-xs font-semibold text-[var(--destructive)]"
className="ml-2 rounded-lg border border-[var(--destructive-muted-border)] hover:bg-[var(--destructive-muted)] px-3 py-1.5 text-xs font-semibold text-[var(--destructive)] disabled:opacity-50 disabled:cursor-not-allowed"
>
Unpin
</button>
Expand All @@ -1171,9 +1172,10 @@ function SettingsPageContent() {
type="text"
value={repoSearchQuery}
onChange={(e) => setRepoSearchQuery(e.target.value)}
disabled={saving}
placeholder="Type to search your repositories..."
aria-label="Search repositories to pin"
className="w-full rounded-lg border border-[var(--border)] bg-[var(--control)] px-4 py-2 text-sm text-[var(--card-foreground)] placeholder:text-[var(--muted-foreground)] focus-visible:ring-2 focus-visible:ring-[var(--accent)] mb-4"
className="w-full rounded-lg border border-[var(--border)] bg-[var(--control)] px-4 py-2 text-sm text-[var(--card-foreground)] placeholder:text-[var(--muted-foreground)] focus-visible:ring-2 focus-visible:ring-[var(--accent)] mb-4 disabled:opacity-50 disabled:cursor-not-allowed"
/>

{loadingRepos ? (
Expand All @@ -1200,8 +1202,9 @@ function SettingsPageContent() {
<button
type="button"
onClick={() => handlePinRepo(repoName)}
disabled={saving}
aria-label={`Pin ${repoName}`}
className="rounded-lg bg-[var(--accent)] text-[var(--accent-foreground)] px-3 py-1 text-xs font-semibold hover:opacity-90 transition-opacity"
className="rounded-lg bg-[var(--accent)] text-[var(--accent-foreground)] px-3 py-1 text-xs font-semibold hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
Pin
</button>
Expand Down
26 changes: 18 additions & 8 deletions src/app/rooms/[roomId]/RoomClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function RoomClient({
const router = useRouter();
const [messages, setMessages] = useState<RoomMessage[]>(initialMessages);
const [members, setMembers] = useState<RoomMember[]>(initialMembers);
const [deleting, setDeleting] = useState(false);

function handleSent(msg: RoomMessage) {
setMessages((prev) => [...prev, msg]);
Expand All @@ -42,12 +43,20 @@ export default function RoomClient({

async function handleDeleteRoom() {
if (!confirm('Are you sure you want to delete this room? This cannot be undone.')) return;
const res = await fetch(`/api/rooms/${room.id}`, { method: 'DELETE' });
if (res.ok) {
router.push('/rooms');
} else {
const data = await res.json();
alert(data.error ?? 'Failed to delete room');
setDeleting(true);
try {
const res = await fetch(`/api/rooms/${room.id}`, { method: 'DELETE' });
if (res.ok) {
router.push('/rooms');
} else {
const data = await res.json();
alert(data.error ?? 'Failed to delete room');
}
} catch (e) {
console.error(e);
alert('Failed to delete room. Please check your connection.');
} finally {
setDeleting(false);
}
}

Expand Down Expand Up @@ -75,9 +84,10 @@ export default function RoomClient({
{room.is_owner && (
<button
onClick={handleDeleteRoom}
className="text-xs px-3 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700"
disabled={deleting}
className="text-xs px-3 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Delete Room
{deleting ? 'Deleting...' : 'Delete Room'}
</button>
)}
</header>
Expand Down
14 changes: 9 additions & 5 deletions src/components/ConfirmModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface ConfirmModalProps {
cancelLabel?: string;
onConfirm: () => void;
onCancel: () => void;
disabled?: boolean;
}

export default function ConfirmModal({
Expand All @@ -20,14 +21,15 @@ export default function ConfirmModal({
cancelLabel = "Cancel",
onConfirm,
onCancel,
disabled = false,
}: ConfirmModalProps) {
const modalRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!isOpen) return;

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
if (e.key === "Escape" && !disabled) {
onCancel();
}
};
Expand All @@ -40,7 +42,7 @@ export default function ConfirmModal({
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "unset";
};
}, [isOpen, onCancel]);
}, [isOpen, onCancel, disabled]);

if (!isOpen) return null;

Expand All @@ -49,7 +51,7 @@ export default function ConfirmModal({
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
onClick={onCancel}
onClick={disabled ? undefined : onCancel}
aria-hidden="true"
/>

Expand All @@ -73,14 +75,16 @@ export default function ConfirmModal({
<button
type="button"
onClick={onCancel}
className="w-full sm:w-auto rounded-xl border border-[var(--border)] bg-[var(--control)] px-5 py-2.5 text-sm font-semibold text-[var(--card-foreground)] transition-all hover:bg-[var(--card-muted)] active:scale-95"
disabled={disabled}
className="w-full sm:w-auto rounded-xl border border-[var(--border)] bg-[var(--control)] px-5 py-2.5 text-sm font-semibold text-[var(--card-foreground)] transition-all hover:bg-[var(--card-muted)] active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100"
>
{cancelLabel}
</button>
<button
type="button"
onClick={onConfirm}
className="w-full sm:w-auto rounded-xl bg-[var(--accent)] px-5 py-2.5 text-sm font-semibold text-[var(--accent-foreground)] transition-all hover:opacity-90 active:scale-95 shadow-lg shadow-[var(--accent)]/20"
disabled={disabled}
className="w-full sm:w-auto rounded-xl bg-[var(--accent)] px-5 py-2.5 text-sm font-semibold text-[var(--accent-foreground)] transition-all hover:opacity-90 active:scale-95 shadow-lg shadow-[var(--accent)]/20 disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100"
>
{confirmLabel}
</button>
Expand Down
48 changes: 30 additions & 18 deletions src/components/GoalTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function useGoalTracker() {
const [confirmingId, setConfirmingId] = useState<string | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const [incrementingId, setIncrementingId] = useState<string | null>(null);

const [activeConfettiGoalId, setActiveConfettiGoalId] = useState<string | null>(null);
const prevGoalsRef = useRef<Map<string, boolean>>(new Map());
Expand Down Expand Up @@ -174,22 +175,21 @@ export function useGoalTracker() {

async function handleDelete(id: string) {
const previousGoals = goals;
setGoals((prev) => prev.filter((g) => g.id !== id));
setConfirmingId(null);
setDeletingId(id);
setDeleteError(null);

try {
const res = await fetch(`/api/goals/${id}`, { method: "DELETE" });
if (!res.ok) {
setGoals(previousGoals);
setDeleteError("Failed to delete goal. Please try again.");
} else {
setGoals((prev) => prev.filter((g) => g.id !== id));
}
} catch (e) {
setGoals(previousGoals);
setDeleteError("Failed to delete goal. Please check your connection.");
} finally {
setDeletingId(null);
setConfirmingId(null);
}
}

Expand Down Expand Up @@ -277,6 +277,8 @@ export function useGoalTracker() {
deletingId,
deleteError,
setDeleteError,
incrementingId,
setIncrementingId,
activeConfettiGoalId,
handleSync,
handleCreate,
Expand Down Expand Up @@ -312,6 +314,8 @@ export default function GoalTracker() {
deletingId,
deleteError,
setDeleteError,
incrementingId,
setIncrementingId,
activeConfettiGoalId,
handleSync,
handleCreate,
Expand Down Expand Up @@ -556,24 +560,31 @@ export default function GoalTracker() {
onClick={async () => {
const newCurrent = goal.current + 1;
if (newCurrent > goal.target) return;
const res = await fetch(`/api/goals/${goal.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ current: newCurrent }),
});
if (res.ok) {
setGoals((prevGoals) =>
prevGoals.map((g) =>
g.id === goal.id ? { ...g, current: newCurrent } : g
)
);
setIncrementingId(goal.id);
try {
const res = await fetch(`/api/goals/${goal.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ current: newCurrent }),
});
if (res.ok) {
setGoals((prevGoals) =>
prevGoals.map((g) =>
g.id === goal.id ? { ...g, current: newCurrent } : g
)
);
}
} catch (e) {
console.error("Failed to increment goal:", e);
} finally {
setIncrementingId(null);
}
}}
disabled={goal.current >= goal.target}
disabled={goal.current >= goal.target || incrementingId === goal.id}
aria-label={`Increment "${goal.title}" progress by 1`}
className="rounded bg-blue-600 px-2 py-1 text-xs text-white hover:bg-blue-700 disabled:opacity-50"
className="rounded bg-blue-600 px-2 py-1 text-xs text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
+1
{incrementingId === goal.id ? "..." : "+1"}
</button>
)}

Expand Down Expand Up @@ -794,6 +805,7 @@ export default function GoalTracker() {
message={`Are you sure you want to permanently remove your "${activeConfirmingGoal?.title || 'active coding'}" goal? This will erase all gathered progress history numbers parameters.`}
confirmLabel={deletingId ? "Deleting..." : "Permanently Delete"}
cancelLabel="Keep Goal"
disabled={deletingId !== null}
onConfirm={() => {
if (confirmingId) handleDelete(confirmingId);
}}
Expand Down
23 changes: 16 additions & 7 deletions src/components/ProjectMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default function ProjectMetrics() {
});
const [connecting, setConnecting] = useState(false);
const [connectionError, setConnectionError] = useState<string | null>(null);
const [disconnecting, setDisconnecting] = useState(false);

const fetchData = useCallback(() => {
setLoading(true);
Expand Down Expand Up @@ -111,11 +112,18 @@ export default function ProjectMetrics() {
}

async function handleDisconnect() {
await fetch("/api/integrations/jira/credentials", {
method: "DELETE",
});
setData(null);
setError(null);
setDisconnecting(true);
try {
await fetch("/api/integrations/jira/credentials", {
method: "DELETE",
});
setData(null);
setError(null);
} catch (e) {
console.error("Failed to disconnect Jira:", e);
} finally {
setDisconnecting(false);
}
}

if (loading) {
Expand Down Expand Up @@ -404,9 +412,10 @@ export default function ProjectMetrics() {
<button
type="button"
onClick={handleDisconnect}
className="text-xs text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
disabled={disconnecting}
className="text-xs text-[var(--muted-foreground)] hover:text-[var(--foreground)] disabled:opacity-50 disabled:cursor-not-allowed"
>
Disconnect
{disconnecting ? "Disconnecting..." : "Disconnect"}
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
Expand Down
21 changes: 11 additions & 10 deletions src/components/StreakTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ export default function StreakTracker() {
</p>
)}

{!freezeLoading && freeze?.hasFreeze && (
{freeze && freeze.hasFreeze && (
<div className="mt-4 flex items-center justify-between rounded-lg border border-[var(--accent)]/30 bg-[var(--accent-soft)] px-4 py-3">
<div className="flex items-center gap-2">
<CheckCircle size={18} className="text-[var(--accent)]" aria-hidden="true" />
Expand All @@ -699,16 +699,16 @@ export default function StreakTracker() {
<button
type="button"
onClick={handleCancelFreeze}
disabled={cancelling}
className="rounded-md bg-[var(--destructive)]/10 px-2.5 py-1 text-xs font-medium text-[var(--destructive)] transition hover:bg-[var(--destructive)]/20 disabled:opacity-60"
disabled={cancelling || freezeLoading}
className="rounded-md bg-[var(--destructive)]/10 px-2.5 py-1 text-xs font-medium text-[var(--destructive)] transition hover:bg-[var(--destructive)]/20 disabled:opacity-60 disabled:cursor-not-allowed"
>
{cancelling ? "Removing..." : "Yes, remove"}
</button>
<button
type="button"
onClick={() => setConfirmCancel(false)}
disabled={cancelling}
className="rounded-md border border-[var(--border)] px-2.5 py-1 text-xs font-medium text-[var(--muted-foreground)] transition hover:bg-[var(--control)]"
disabled={cancelling || freezeLoading}
className="rounded-md border border-[var(--border)] px-2.5 py-1 text-xs font-medium text-[var(--muted-foreground)] transition hover:bg-[var(--control)] disabled:opacity-50 disabled:cursor-not-allowed"
>
Keep
</button>
Expand All @@ -717,15 +717,16 @@ export default function StreakTracker() {
<button
type="button"
onClick={handleCancelFreeze}
className="rounded-md border border-[var(--border)] px-3 py-1 text-xs font-medium text-[var(--muted-foreground)] transition hover:bg-[var(--control)]"
disabled={cancelling || freezeLoading}
className="rounded-md border border-[var(--border)] px-3 py-1 text-xs font-medium text-[var(--muted-foreground)] transition hover:bg-[var(--control)] disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel freeze
</button>
)}
</div>
)}

{!freezeLoading && !freeze?.hasFreeze && (
{freeze && !freeze.hasFreeze && (
<div className="mt-4 flex items-center justify-between rounded-lg border border-[var(--border)] bg-[var(--control)] px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-[var(--foreground)]">Streak Freeze</span>
Expand All @@ -747,13 +748,13 @@ export default function StreakTracker() {
<button
type="button"
onClick={handleApplyFreeze}
disabled={freezeLoading || freeze?.hasFreeze}
className={`rounded-md px-3 py-1 text-xs font-medium transition ${freezeLoading || freeze?.hasFreeze
disabled={freezeLoading || cancelling}
className={`rounded-md px-3 py-1 text-xs font-medium transition ${freezeLoading || cancelling
? "cursor-not-allowed opacity-50 bg-[var(--accent)]"
: "bg-[var(--accent)] hover:opacity-90"
} text-[var(--accent-foreground)]`}
>
{freeze?.hasFreeze ? "Freeze Active" : "Freeze Streak"}
{freezeLoading ? "Freezing..." : "Freeze Streak"}
</button>
</div>
)}
Expand Down
Loading
Loading