Skip to content

Commit a96d58e

Browse files
authored
Merge pull request #78 from devakone/develop
Release: develop → main
2 parents 5578fbe + 156fc13 commit a96d58e

10 files changed

Lines changed: 302 additions & 7 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
".": "0.1.0-alpha.24",
3-
"apps/web": "0.1.0-alpha.22",
2+
".": "0.1.0-alpha.25",
3+
"apps/web": "0.1.0-alpha.23",
44
"apps/worker": "0.1.0-alpha.3"
55
}

CHANGELOG-DEVELOP.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [0.1.0-alpha.25](https://github.com/devakone/vibe-coding-profiler/compare/vibe-coding-profiler-v0.1.0-alpha.24...vibe-coding-profiler-v0.1.0-alpha.25) (2026-02-15)
4+
5+
6+
### Features
7+
8+
* improve public profile discoverability with CTAs and smart sharing ([10e12fa](https://github.com/devakone/vibe-coding-profiler/commit/10e12fa47c13a1f08546842349e042f5730c7598))
9+
310
## [0.1.0-alpha.24](https://github.com/devakone/vibe-coding-profiler/compare/vibe-coding-profiler-v0.1.0-alpha.23...vibe-coding-profiler-v0.1.0-alpha.24) (2026-02-15)
411

512

apps/web/CHANGELOG-DEVELOP.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [0.1.0-alpha.23](https://github.com/devakone/vibe-coding-profiler/compare/web-v0.1.0-alpha.22...web-v0.1.0-alpha.23) (2026-02-15)
4+
5+
6+
### Features
7+
8+
* improve public profile discoverability with CTAs and smart sharing ([10e12fa](https://github.com/devakone/vibe-coding-profiler/commit/10e12fa47c13a1f08546842349e042f5730c7598))
9+
310
## [0.1.0-alpha.22](https://github.com/devakone/vibe-coding-profiler/compare/web-v0.1.0-alpha.21...web-v0.1.0-alpha.22) (2026-02-15)
411

512

apps/web/src/app/analysis/[jobId]/AnalysisClient.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ProfileContributionCard,
1717
} from "@/components/vcp/repo";
1818
import { VCPAIToolsSection } from "@/components/vcp/blocks";
19+
import { PublicProfileCTABanner } from "@/components/public-profile/PublicProfileCTABanner";
1920

2021
type Job = {
2122
id: string;
@@ -35,6 +36,8 @@ type ApiResponse = {
3536
profileContribution?: unknown | null;
3637
userAvatarUrl?: string | null;
3738
userId?: string | null;
39+
username?: string | null;
40+
profileEnabled?: boolean;
3841
vibeInsights?: VibeInsightsRow | null;
3942
};
4043

@@ -491,10 +494,23 @@ export default function AnalysisClient({ jobId }: { jobId: string }) {
491494
return `${shareTemplate.headline}\n${taglineLine}\n${metricsLine}\n#VCP`;
492495
}, [shareTemplate]);
493496

497+
// Build share URL - use public profile URL when available
494498
const shareUrl = useMemo(() => {
495499
if (!shareOrigin) return "";
500+
501+
// If user has public profile enabled and we have a repo name, share the public URL
502+
const username = data?.username;
503+
const profileEnabled = data?.profileEnabled;
504+
const repoName = profileContribution?.repoName;
505+
506+
if (username && profileEnabled && repoName) {
507+
const repoSlug = repoName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
508+
return `${shareOrigin}/u/${username}/repo/${repoSlug}`;
509+
}
510+
511+
// Fall back to the analysis URL
496512
return `${shareOrigin}/analysis/${jobId}`;
497-
}, [jobId, shareOrigin]);
513+
}, [jobId, shareOrigin, data?.username, data?.profileEnabled, profileContribution?.repoName]);
498514

499515
const shareCaption = useMemo(() => {
500516
if (!shareTemplate) return "";
@@ -849,6 +865,15 @@ export default function AnalysisClient({ jobId }: { jobId: string }) {
849865
userId={data?.userId ?? undefined}
850866
storyEndpoint={storyEndpoint}
851867
/>
868+
{/* CTA to enable public profile when disabled */}
869+
{!data?.profileEnabled && (
870+
<div className="mt-4">
871+
<PublicProfileCTABanner
872+
hasUsername={Boolean(data?.username)}
873+
variant="inline"
874+
/>
875+
</div>
876+
)}
852877
</div>
853878
) : null}
854879

apps/web/src/app/api/analysis/[id]/route.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,17 +289,27 @@ export async function GET(
289289
let insights = null;
290290
let profileContribution: unknown | null = null;
291291
let userAvatarUrl: string | null = null;
292+
let username: string | null = null;
293+
let profileEnabled = false;
292294

293-
// Fetch user avatar from users table
295+
// Fetch user avatar, username, and public profile settings from users table
294296
const { data: userData } = await (supabase as unknown as SupabaseQueryLike)
295297
.from("users")
296-
.select("avatar_url")
298+
.select("avatar_url, username, public_profile_settings")
297299
.eq("id", user.id)
298300
.single();
299301

300302
if (userData && typeof userData === "object") {
301-
const avatarUrl = (userData as { avatar_url?: unknown }).avatar_url;
302-
userAvatarUrl = typeof avatarUrl === "string" ? avatarUrl : null;
303+
const userRow = userData as {
304+
avatar_url?: unknown;
305+
username?: unknown;
306+
public_profile_settings?: Record<string, unknown> | null;
307+
};
308+
userAvatarUrl = typeof userRow.avatar_url === "string" ? userRow.avatar_url : null;
309+
username = typeof userRow.username === "string" ? userRow.username : null;
310+
profileEnabled =
311+
username !== null &&
312+
userRow.public_profile_settings?.profile_enabled === true;
303313
}
304314

305315
if (job.status === "done") {
@@ -416,6 +426,8 @@ export async function GET(
416426
profileContribution,
417427
userAvatarUrl,
418428
userId: user.id,
429+
username,
430+
profileEnabled,
419431
vibeInsights: computedVibeInsights,
420432
});
421433
}
@@ -428,6 +440,8 @@ export async function GET(
428440
profileContribution,
429441
userAvatarUrl,
430442
userId: user.id,
443+
username,
444+
profileEnabled,
431445
vibeInsights: null,
432446
});
433447
}

apps/web/src/app/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
UnifiedMethodologySection,
2424
} from "@/components/vcp/unified";
2525
import { VCPAIToolsSection } from "@/components/vcp/blocks";
26+
import { FirstTimePublicProfileBanner } from "@/components/public-profile/FirstTimePublicProfileBanner";
2627

2728
const heroFeatures = [
2829
"A Vibe Coding Profile (VCP) built from AI-assisted engineering signals in your commit history",
@@ -1222,6 +1223,14 @@ function AuthenticatedDashboard({
12221223
/>
12231224
) : null}
12241225

1226+
{/* First-time onboarding banner for public profile */}
1227+
{stats.userProfile ? (
1228+
<FirstTimePublicProfileBanner
1229+
profileEnabled={profileEnabled}
1230+
hasUsername={Boolean(username)}
1231+
/>
1232+
) : null}
1233+
12251234
{/* Profile History - Version selector */}
12261235
{stats.userProfile ? (
12271236
<ProfileVersionSelector currentUpdatedAt={stats.userProfile.updatedAt} />
@@ -1251,6 +1260,7 @@ function AuthenticatedDashboard({
12511260
highlights={narrativeHighlights}
12521261
isLLMGenerated={hasLLMNarrative}
12531262
llmModel={stats.userProfile.llmModel}
1263+
llmKeySource={stats.userProfile.llmKeySource}
12541264
/>
12551265
) : null}
12561266

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"use client";
2+
3+
import { useState, useSyncExternalStore, useCallback } from "react";
4+
import Link from "next/link";
5+
import { X, Sparkles } from "lucide-react";
6+
7+
const STORAGE_KEY = "vcp_public_profile_intro_seen";
8+
9+
// Subscribe to localStorage changes (for cross-tab sync if needed)
10+
function subscribe(callback: () => void) {
11+
window.addEventListener("storage", callback);
12+
return () => window.removeEventListener("storage", callback);
13+
}
14+
15+
function getSnapshot(): boolean {
16+
return localStorage.getItem(STORAGE_KEY) === "1";
17+
}
18+
19+
function getServerSnapshot(): boolean {
20+
return true; // On server, assume dismissed to avoid hydration mismatch
21+
}
22+
23+
interface FirstTimePublicProfileBannerProps {
24+
/** Whether user's public profile is already enabled */
25+
profileEnabled: boolean;
26+
/** Whether user has claimed a username */
27+
hasUsername: boolean;
28+
}
29+
30+
/**
31+
* Dismissible onboarding banner shown after first VCP generation.
32+
* Prompts users to enable their public profile.
33+
* Uses localStorage to track dismissal.
34+
*/
35+
export function FirstTimePublicProfileBanner({
36+
profileEnabled,
37+
hasUsername,
38+
}: FirstTimePublicProfileBannerProps) {
39+
// Use useSyncExternalStore to safely read from localStorage
40+
const dismissedFromStorage = useSyncExternalStore(
41+
subscribe,
42+
getSnapshot,
43+
getServerSnapshot
44+
);
45+
46+
const [localDismissed, setLocalDismissed] = useState(false);
47+
const dismissed = dismissedFromStorage || localDismissed;
48+
49+
const dismiss = useCallback(() => {
50+
localStorage.setItem(STORAGE_KEY, "1");
51+
setLocalDismissed(true);
52+
}, []);
53+
54+
// Don't render if dismissed or if profile already enabled
55+
if (dismissed || profileEnabled) {
56+
return null;
57+
}
58+
59+
return (
60+
<div className="relative overflow-hidden rounded-2xl border border-violet-300/50 bg-gradient-to-r from-violet-100 via-indigo-50 to-violet-100 px-6 py-5 shadow-sm">
61+
{/* Decorative sparkle */}
62+
<div className="absolute -right-4 -top-4 h-24 w-24 rounded-full bg-violet-200/30 blur-2xl" />
63+
<div className="absolute -left-4 -bottom-4 h-20 w-20 rounded-full bg-indigo-200/30 blur-2xl" />
64+
65+
<div className="relative flex items-start gap-4">
66+
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-violet-500 to-indigo-500 shadow-sm">
67+
<Sparkles className="h-5 w-5 text-white" />
68+
</div>
69+
70+
<div className="min-w-0 flex-1">
71+
<p className="font-semibold text-zinc-900">
72+
Your Vibe Coding Profile is ready!
73+
</p>
74+
<p className="mt-1 text-sm text-zinc-600">
75+
{hasUsername
76+
? "Enable your public profile to share your VCP on Twitter, LinkedIn, and more."
77+
: "Claim a username and enable your public profile to share your VCP with the world."}
78+
</p>
79+
<div className="mt-3 flex flex-wrap items-center gap-3">
80+
<Link
81+
href="/settings/public-profile"
82+
className="inline-flex items-center gap-1.5 rounded-full bg-gradient-to-r from-violet-600 to-indigo-500 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:brightness-110"
83+
>
84+
{hasUsername ? "Enable Public Profile" : "Set Up Public Profile"}
85+
</Link>
86+
<button
87+
type="button"
88+
onClick={dismiss}
89+
className="text-sm font-medium text-zinc-500 hover:text-zinc-700"
90+
>
91+
Maybe later
92+
</button>
93+
</div>
94+
</div>
95+
96+
<button
97+
type="button"
98+
onClick={dismiss}
99+
className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full text-zinc-400 transition hover:bg-zinc-200/50 hover:text-zinc-600"
100+
aria-label="Dismiss"
101+
>
102+
<X size={16} />
103+
</button>
104+
</div>
105+
</div>
106+
);
107+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { ExternalLink } from "lucide-react";
5+
6+
interface PublicProfileCTABannerProps {
7+
/** Whether user has claimed a username */
8+
hasUsername: boolean;
9+
/** Visual variant */
10+
variant?: "card" | "inline";
11+
/** Optional username to show preview link */
12+
username?: string | null;
13+
}
14+
15+
/**
16+
* CTA banner prompting users to enable their public profile.
17+
* Shows on VCP pages when public profile is not enabled.
18+
*/
19+
export function PublicProfileCTABanner({
20+
hasUsername,
21+
variant = "card",
22+
username,
23+
}: PublicProfileCTABannerProps) {
24+
if (variant === "inline") {
25+
return (
26+
<div className="flex flex-wrap items-center gap-2 text-sm text-zinc-600">
27+
<span>
28+
{hasUsername
29+
? "Enable your public profile to share on social media."
30+
: "Claim a username to share your VCP publicly."}
31+
</span>
32+
<Link
33+
href="/settings/public-profile"
34+
className="inline-flex items-center gap-1 font-medium text-violet-600 hover:text-violet-800"
35+
>
36+
{hasUsername ? "Enable" : "Set up"} public profile
37+
<ExternalLink size={12} />
38+
</Link>
39+
</div>
40+
);
41+
}
42+
43+
return (
44+
<div className="rounded-2xl border border-violet-200/50 bg-gradient-to-r from-violet-50 to-indigo-50 px-6 py-5">
45+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
46+
<div>
47+
<p className="font-semibold text-zinc-900">
48+
{hasUsername
49+
? "Share your VCP publicly"
50+
: "Create your public VCP profile"}
51+
</p>
52+
<p className="mt-1 text-sm text-zinc-600">
53+
{hasUsername
54+
? "Enable your public profile so others can see your Vibe Coding Profile."
55+
: "Claim a username and enable your public profile to share on social media."}
56+
</p>
57+
</div>
58+
<div className="flex flex-shrink-0 items-center gap-3">
59+
{username && (
60+
<Link
61+
href={`/u/${username}`}
62+
className="text-sm font-medium text-zinc-500 hover:text-zinc-700"
63+
target="_blank"
64+
>
65+
Preview
66+
</Link>
67+
)}
68+
<Link
69+
href="/settings/public-profile"
70+
className="inline-flex items-center gap-1.5 rounded-full bg-gradient-to-r from-violet-600 to-indigo-500 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:brightness-110"
71+
>
72+
{hasUsername ? "Enable Public Profile" : "Set Up Profile"}
73+
</Link>
74+
</div>
75+
</div>
76+
</div>
77+
);
78+
}

apps/web/src/components/share/ProfileShareSection.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client";
22

3+
import Link from "next/link";
34
import { useMemo, useSyncExternalStore } from "react";
5+
import { ExternalLink } from "lucide-react";
46
import { computeShareCardMetrics } from "@/lib/vcp/metrics";
57
import type { VibeAxes } from "@vibe-coding-profiler/core";
68
import { ShareCard, ShareActions } from "./index";
@@ -181,6 +183,23 @@ export function ProfileShareSection({
181183
entityId={userId}
182184
shareJson={shareJson}
183185
/>
186+
{/* CTA to enable public profile when disabled */}
187+
{!profileEnabled && (
188+
<div className="mt-3 flex flex-wrap items-center gap-2 text-sm text-zinc-600">
189+
<span>
190+
{username
191+
? "Enable your public profile to share on social media."
192+
: "Claim a username to share your VCP publicly."}
193+
</span>
194+
<Link
195+
href="/settings/public-profile"
196+
className="inline-flex items-center gap-1 font-medium text-violet-600 hover:text-violet-800"
197+
>
198+
{username ? "Enable" : "Set up"} public profile
199+
<ExternalLink size={12} />
200+
</Link>
201+
</div>
202+
)}
184203
</div>
185204
);
186205
}

0 commit comments

Comments
 (0)