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
6 changes: 5 additions & 1 deletion src/app/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { after } from "next/server";
import { generateMockHeatmap, computeStreaks } from "@/lib/mock";
import {
fetchContributionCalendar,

Check warning on line 19 in src/app/[username]/page.tsx

View workflow job for this annotation

GitHub Actions / build

'fetchContributionCalendar' is defined but never used
fetchContributorProfile,
} from "@/lib/github";
import { calculateScore } from "@/lib/score";
Expand Down Expand Up @@ -228,6 +228,7 @@
updated_at?: string | null;
badges?: Array<{ program?: string; years?: Array<string | number> }> | null;
headline?: string | null;
readme?: string | null;
pinned_repos?: string[] | null;
custom_links?: Array<{ label: string; url: string }> | null;
visibility?: string | null;
Expand All @@ -243,7 +244,7 @@
try {
const { data, error } = await getProfileByUsername(
username,
"id, score, updated_at, badges, headline, pinned_repos, custom_links, visibility",
"id, score, updated_at, badges, headline, readme, pinned_repos, custom_links, visibility",
);
customizationFetchSettled = true;

Expand Down Expand Up @@ -318,6 +319,8 @@
? {
headline:
typeof profileRow.headline === "string" ? profileRow.headline : null,
readme:
typeof profileRow.readme === "string" ? profileRow.readme : null,
pinnedRepos: Array.isArray(profileRow.pinned_repos)
? (profileRow.pinned_repos as string[])
: [],
Expand Down Expand Up @@ -364,6 +367,7 @@
customLinks={customization?.customLinks ?? []}
pinnedRepos={customization?.pinnedRepos ?? []}
customizationLoaded={customizationFetchSettled}
readme={customization?.readme ?? undefined}
/>
</main>
<Footer />
Expand Down
7 changes: 6 additions & 1 deletion src/app/api/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@/lib/validators/api";

import { sanitizeFundingLinks, sanitizeSponsors } from "@/lib/sponsors";
import { sanitizeMarkdownContent } from "@/lib/readme";

// Runtime managed by @opennextjs/cloudflare

Expand Down Expand Up @@ -47,7 +48,7 @@ export async function GET(request: NextRequest) {

const { data, error } = await supabase
.from("profiles")
.select("headline, pinned_repos, custom_links, badges, visibility, funding_links, sponsors")
.select("headline, pinned_repos, custom_links, badges, visibility, funding_links, sponsors, readme")
.eq("id", user.id)
.single();

Expand Down Expand Up @@ -89,6 +90,10 @@ export async function PUT(request: NextRequest) {
if (sanitized) updates.headline = sanitized;
}

if (body.readme !== undefined) {
updates.readme = sanitizeMarkdownContent(body.readme);
}

if (Array.isArray(body.pinned_repos)) {
updates.pinned_repos = body.pinned_repos
.slice(0, 6)
Expand Down
121 changes: 121 additions & 0 deletions src/app/settings/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface Badge {
}

import type { FundingLink, SponsorItem } from "@/types";
import { ProfileReadme } from "@/components/profile/ProfileReadme";

interface ApiKeyInfo {
id: string;
Expand All @@ -27,6 +28,7 @@ interface ApiKeyInfo {

interface ProfileSettings {
headline: string;
readme: string;
pinned_repos: string[];
custom_links: CustomLink[];
badges: Badge[];
Expand All @@ -51,6 +53,7 @@ export function SettingsClient() {
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [readmeTab, setReadmeTab] = useState<"edit" | "preview">("edit");

// Account deletion. `deleteConfirm` holds what the user has typed into the confirmation box: the
// button stays disabled until it matches their username exactly, so this cannot be triggered by a
Expand All @@ -71,6 +74,7 @@ export function SettingsClient() {
const [copiedKey, setCopiedKey] = useState(false);
const [settings, setSettings] = useState<ProfileSettings>({
headline: "",
readme: "",
pinned_repos: [],
custom_links: [],
badges: [],
Expand All @@ -88,6 +92,7 @@ export function SettingsClient() {
const data = await resp.json();
setSettings({
headline: data.headline || "",
readme: data.readme || "",
pinned_repos: data.pinned_repos || [],
custom_links: data.custom_links || [],
badges: data.badges || [],
Expand Down Expand Up @@ -187,6 +192,7 @@ export function SettingsClient() {

const payload = {
headline: settings.headline.trim(),
readme: settings.readme,
pinned_repos: settings.pinned_repos
.map((r) => r.trim())
.filter((r) => r.length > 0),
Expand Down Expand Up @@ -394,6 +400,121 @@ export function SettingsClient() {
</div>
</div>

<div style={sectionStyle}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "8px",
}}
>
<label style={{ ...labelStyle, margin: 0 }}>Profile README (Markdown)</label>
<div style={{ display: "flex", gap: "6px" }}>
<button
type="button"
onClick={() => setReadmeTab("edit")}
style={{
fontSize: "12px",
fontWeight: 600,
padding: "3px 10px",
borderRadius: "6px",
border: "1px solid var(--color-hairline)",
backgroundColor:
readmeTab === "edit"
? "var(--color-primary-deep)"
: "var(--color-canvas)",
color: readmeTab === "edit" ? "#ffffff" : "var(--color-ink-mute)",
cursor: "pointer",
}}
>
Write
</button>
<button
type="button"
onClick={() => setReadmeTab("preview")}
style={{
fontSize: "12px",
fontWeight: 600,
padding: "3px 10px",
borderRadius: "6px",
border: "1px solid var(--color-hairline)",
backgroundColor:
readmeTab === "preview"
? "var(--color-primary-deep)"
: "var(--color-canvas)",
color:
readmeTab === "preview" ? "#ffffff" : "var(--color-ink-mute)",
cursor: "pointer",
}}
>
Preview
</button>
</div>
</div>

<p
style={{
fontSize: "13px",
color: "var(--color-ink-mute)",
margin: "0 0 8px 0",
}}
>
Introduce yourself in detail using Markdown. You can format headings, list accomplishments, add code blocks, or embed images.
</p>

{readmeTab === "edit" ? (
<>
<textarea
placeholder="## Hi there 👋&#10;&#10;I'm a full-stack open source contributor..."
value={settings.readme}
onChange={(e) =>
setSettings((s) => ({ ...s, readme: e.target.value }))
}
maxLength={10000}
rows={10}
style={{
...inputStyle,
fontFamily: "monospace",
fontSize: "14px",
lineHeight: 1.5,
resize: "vertical",
}}
aria-label="Profile README Markdown"
/>
<p
style={{
fontSize: "12px",
color: "var(--color-ink-mute-2)",
marginTop: "4px",
}}
>
{settings.readme.length}/10000 characters
</p>
</>
) : (
<div style={{ marginTop: "12px" }}>
{settings.readme.trim() ? (
<ProfileReadme readme={settings.readme} />
) : (
<p
style={{
fontSize: "13px",
color: "var(--color-ink-mute)",
fontStyle: "italic",
padding: "16px",
border: "1px dashed var(--color-hairline)",
borderRadius: "8px",
textAlign: "center",
}}
>
Nothing to preview yet. Switch to &quot;Write&quot; to add custom Markdown.
</p>
)}
</div>
)}
</div>

<div style={sectionStyle}>
<label style={labelStyle}>Pinned Repositories (up to 6)</label>
<p
Expand Down
Loading
Loading