Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 106 additions & 29 deletions app/generator/components/sections/CommitPulseSection.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
'use client';

import Image from 'next/image';
import { useState, useEffect } from 'react';
import { Loader2, Search, X, ExternalLink } from 'lucide-react';
import { useState, useEffect, lazy, Suspense } from 'react';
import { Loader2, Search, X, ExternalLink, PlaySquare } from 'lucide-react';
import type { ActivityData } from '@/types/dashboard';

const ContributionCity3D = lazy(() => import('@/components/dashboard/ContributionCity3D'));

import { SectionCard, FieldLabel } from '../SectionCard';
import { validateGitHubUsername } from '@/lib/validations';
Expand Down Expand Up @@ -89,6 +92,10 @@ export function CommitPulseSection({
const [badgeLoaded, setBadgeLoaded] = useState(false);
const [badgeError, setBadgeError] = useState(false);

const [timeLapseMode, setTimeLapseMode] = useState(false);
const [fullActivityData, setFullActivityData] = useState<ActivityData[] | null>(null);
const [loadingFullData, setLoadingFullData] = useState(false);

useEffect(() => {
if (!debouncedUsername) {
// eslint-disable-next-line react-hooks/set-state-in-effect
Expand Down Expand Up @@ -146,6 +153,33 @@ export function CommitPulseSection({
};
}, [debouncedUsername]);

useEffect(() => {
if (timeLapseMode && !fullActivityData && debouncedUsername && userDetails && !fetchError) {
let cancelled = false;
// eslint-disable-next-line react-hooks/set-state-in-effect
setLoadingFullData(true);
fetch(`/api/github?username=${encodeURIComponent(debouncedUsername)}`)
.then((res) => {
if (!res.ok) throw new Error('Failed to fetch full data');
return res.json();
})
.then((data) => {
if (!cancelled) {
setFullActivityData(data.activity);
setLoadingFullData(false);
}
})
.catch(() => {
if (!cancelled) {
setLoadingFullData(false);
}
});
return () => {
cancelled = true;
};
}
}, [timeLapseMode, debouncedUsername, userDetails, fetchError, fullActivityData]);

const badgeUrl = userDetails && !fetchError ? buildBadgeUrl(debouncedUsername, safeAccent) : null;

const accentIsValid = /^[0-9a-fA-F]{6}$/.test(safeAccent.replace(/^#/, ''));
Expand Down Expand Up @@ -333,7 +367,21 @@ export function CommitPulseSection({
{badgeUrl && userDetails && (
<div>
<div className="flex items-center justify-between mb-2">
<FieldLabel>Live Preview</FieldLabel>
<div className="flex items-center gap-3">
<FieldLabel>Live Preview</FieldLabel>
{/* Time-Lapse Toggle */}
<button
onClick={() => setTimeLapseMode((v) => !v)}
className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-[10px] font-semibold transition-all ${
timeLapseMode
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30'
: 'bg-gray-100 dark:bg-white/5 text-gray-500 hover:text-gray-900 dark:hover:text-white border border-black/5 dark:border-white/10'
}`}
>
<PlaySquare size={12} />
Time-Lapse Preview
</button>
</div>
<a
href={`${DASHBOARD_BASE}/${debouncedUsername}`}
target="_blank"
Expand All @@ -345,33 +393,62 @@ export function CommitPulseSection({
</div>

<div className="relative rounded-xl border border-gray-200 dark:border-white/8 bg-[#0d1117] p-4 flex items-center justify-center min-h-[140px] overflow-hidden">
{!badgeLoaded && !badgeError && (
<div className="absolute inset-0 flex items-center justify-center">
<Loader2 size={24} className="animate-spin text-zinc-600" />
</div>
)}
{badgeError && (
<p className="text-xs text-red-400 text-center px-4">
Could not load badge preview. The streak data may still be generating β€” check
the full dashboard link above.
</p>
{timeLapseMode ? (
fullActivityData ? (
<Suspense
fallback={<Loader2 size={24} className="animate-spin text-zinc-600" />}
>
<div className="w-full">
<ContributionCity3D
data={fullActivityData}
theme="dark" // Using default dark theme for preview, or could map accent color
timeLapseMode={true}
/>
</div>
</Suspense>
) : loadingFullData ? (
<div className="flex flex-col items-center gap-2">
<Loader2 size={24} className="animate-spin text-zinc-600" />
<span className="text-[10px] text-gray-400">
Loading full activity data...
</span>
</div>
) : (
<p className="text-xs text-red-400 text-center px-4">
Failed to load time-lapse data.
</p>
)
) : (
<>
{!badgeLoaded && !badgeError && (
<div className="absolute inset-0 flex items-center justify-center">
<Loader2 size={24} className="animate-spin text-zinc-600" />
</div>
)}
{badgeError && (
<p className="text-xs text-red-400 text-center px-4">
Could not load badge preview. The streak data may still be generating β€”
check the full dashboard link above.
</p>
)}
<img
key={`${badgeKey}-${safeAccent}`}
src={badgeUrl}
alt={`CommitPulse badge for ${debouncedUsername}`}
className={`w-full h-auto max-w-[480px] transition-opacity duration-500 ${
badgeLoaded ? 'opacity-100' : 'opacity-0 absolute'
}`}
onLoad={() => {
setBadgeLoaded(true);
setBadgeError(false);
}}
onError={() => {
setBadgeError(true);
setBadgeLoaded(false);
}}
/>
</>
)}
<img
key={`${badgeKey}-${safeAccent}`}
src={badgeUrl}
alt={`CommitPulse badge for ${debouncedUsername}`}
className={`w-full h-auto max-w-[480px] transition-opacity duration-500 ${
badgeLoaded ? 'opacity-100' : 'opacity-0 absolute'
}`}
onLoad={() => {
setBadgeLoaded(true);
setBadgeError(false);
}}
onError={() => {
setBadgeError(true);
setBadgeLoaded(false);
}}
/>
</div>

{userDetails.stats && (
Expand Down
42 changes: 40 additions & 2 deletions components/dashboard/ActivityLandscape.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export default function ActivityLandscape({ data }: { data: ActivityData[] }) {
const [mode, setMode] = useState<'commits' | 'loc'>('commits');
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
const [is3D, setIs3D] = useState(false);
const [timeLapseMode, setTimeLapseMode] = useState(false);
const { t } = useTranslation();
const theme3D = use3DTheme();

Expand Down Expand Up @@ -190,7 +191,10 @@ export default function ActivityLandscape({ data }: { data: ActivityData[] }) {

{/* ── 3D City View toggle ── */}
<button
onClick={() => setIs3D((v) => !v)}
onClick={() => {
if (is3D) setTimeLapseMode(false);
setIs3D((v) => !v);
}}
aria-pressed={is3D}
title={is3D ? 'Switch to flat view' : 'Switch to 3D City view'}
className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-semibold transition-all duration-200 ${
Expand All @@ -217,6 +221,40 @@ export default function ActivityLandscape({ data }: { data: ActivityData[] }) {
</svg>
3D City
</button>

{/* ── Time Lapse View toggle ── */}
<AnimatePresence>
{is3D && (
<motion.button
initial={{ opacity: 0, scale: 0.9, width: 0 }}
animate={{ opacity: 1, scale: 1, width: 'auto' }}
exit={{ opacity: 0, scale: 0.9, width: 0 }}
onClick={() => setTimeLapseMode((v) => !v)}
aria-pressed={timeLapseMode}
title={timeLapseMode ? 'Turn off Time-Lapse' : 'Turn on Time-Lapse'}
className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-semibold transition-all duration-200 overflow-hidden whitespace-nowrap ${
timeLapseMode
? 'border-emerald-400/40 bg-emerald-500/15 text-emerald-500 dark:border-emerald-400/30 dark:bg-emerald-500/10'
: 'border-black/10 bg-transparent text-gray-500 hover:border-black/20 hover:text-black dark:border-white/10 dark:hover:border-white/20 dark:hover:text-white'
}`}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
Time-Lapse
</motion.button>
)}
</AnimatePresence>
</div>
</div>

Expand All @@ -233,7 +271,7 @@ export default function ActivityLandscape({ data }: { data: ActivityData[] }) {
</div>
}
>
<ContributionCity3D data={data} theme={theme3D} />
<ContributionCity3D data={data} theme={theme3D} timeLapseMode={timeLapseMode} />
</Suspense>
</div>
)}
Expand Down
Loading
Loading