Skip to content
Closed
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
30 changes: 30 additions & 0 deletions dev_output.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

> github-user-summary@0.1.0 dev
> next dev

â–² Next.js 16.1.6 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.0.2:3000

✓ Starting...
✓ Ready in 1120ms
â—‹ Compiling /dashboard/settings ...
[next-auth][warn][NEXTAUTH_URL]
https://next-auth.js.org/warnings#nextauth_url
GET /dashboard/settings 307 in 5.9s (compile: 5.5s, render: 388ms)
GET / 200 in 769ms (compile: 692ms, render: 77ms)
[next-auth][warn][NEXTAUTH_URL]
https://next-auth.js.org/warnings#nextauth_url
GET /api/auth/session 200 in 2.4s (compile: 2.4s, render: 52ms)
GET / 200 in 106ms (compile: 6ms, render: 99ms)
GET /api/auth/session 200 in 40ms (compile: 27ms, render: 12ms)
âš  metadataBase property in metadata export is not set for resolving social open graph or twitter images, using "http://localhost:3000". See https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadatabase
React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an Array with length 4 instead. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value which is why Arrays of length greater than 1 are not supported. When using JSX it can be common to combine text nodes and value nodes. For example: <title>hello {nameOfUser}</title>. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop is using this form try rewriting it using a template string: <title>{`hello ${nameOfUser}`}</title>.
GET /api/auth/session 200 in 41ms (compile: 28ms, render: 13ms)
GET /test-settings 200 in 3.8s (compile: 2.1s, render: 1679ms)
GET /test-settings 200 in 974ms (compile: 740ms, render: 235ms)
[next-auth][warn][NEXTAUTH_URL]
https://next-auth.js.org/warnings#nextauth_url
GET /api/auth/session 200 in 369ms (compile: 294ms, render: 75ms)
GET /api/auth/session 200 in 38ms (compile: 9ms, render: 30ms)
✓ Compiled in 61ms
217 changes: 8 additions & 209 deletions src/components/DashboardSettingsClient.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
"use client";

import { useMemo } from "react";
import { useState } from "react";
import { useSession } from "next-auth/react";

import LayoutEditor from "@/components/LayoutEditor";
import DisplayOptionsSection from "@/components/DisplayOptionsSection";
import ReadmeCardUrlSection from "@/components/ReadmeCardUrlSection";
import {
getDefaultCardSettings,
loadCardSettings,
saveCardSettings,
} from "@/lib/cardSettings";
import type { CardBlockId, CardDisplayOptions, CardLayout } from "@/lib/types";

const toggles: Array<{ key: keyof CardDisplayOptions; label: string }> = [
{ key: "showCompany", label: "Company" },
{ key: "showLocation", label: "Location" },
{ key: "showWebsite", label: "Website" },
{ key: "showTwitter", label: "Twitter" },
{ key: "showJoinedDate", label: "Joined date" },
{ key: "showTopics", label: "Topics" },
{ key: "showContributionBreakdown", label: "Contribution breakdown" },
{ key: "showStreaks", label: "Streaks" },
{ key: "showInterests", label: "Interests" },
{ key: "showActivityBreakdown", label: "Activity breakdown" },
];

export default function DashboardSettingsClient() {
const { data: session } = useSession();
const [layout, setLayout] = useState<CardLayout>(
Expand All @@ -34,11 +22,6 @@ export default function DashboardSettingsClient() {
() => loadCardSettings().options,
);
const [status, setStatus] = useState<string>("");
const [readmeTheme, setReadmeTheme] = useState<"light" | "dark">("light");
const [readmeCols, setReadmeCols] = useState<1 | 2>(1);
const [includeStreak, setIncludeStreak] = useState(false);
const [includeHeatmap, setIncludeHeatmap] = useState(false);
const [copyState, setCopyState] = useState("");

const onSave = () => {
saveCardSettings(layout, options);
Expand All @@ -62,106 +45,6 @@ export default function DashboardSettingsClient() {
}));
};

const readmeUrl = useMemo(() => {
const username = session?.user?.login;
if (!username) {
return "";
}

const blockMap: Record<
CardBlockId,
"bio" | "stats" | "langs" | "repos" | null
> = {
avatar: null,
bio: "bio",
stats: "stats",
topLanguages: "langs",
topRepos: "repos",
};

const selected = layout.blocks
.filter((block) => block.visible)
.map((block) => blockMap[block.id])
.filter((block): block is "bio" | "stats" | "langs" | "repos" =>
Boolean(block),
);

const selectedBlocks: Array<
"bio" | "stats" | "langs" | "repos" | "streak" | "heatmap"
> = [...selected];

if (includeStreak) {
selectedBlocks.push("streak");
}

if (includeHeatmap) {
selectedBlocks.push("heatmap");
}

const uniqueBlocks = Array.from(new Set(selectedBlocks));

const layoutParts = layout.blocks
.filter((block) => block.visible && blockMap[block.id])
.map((block) => {
const target = blockMap[block.id];
if (!target) {
return null;
}
return `${block.column}:${target}`;
})
.filter((value): value is string => Boolean(value));

const hide = [];
if (options.showContributionBreakdown === false) {
hide.push("stars");
}
if (options.showActivityBreakdown === false) {
hide.push("forks");
}

const params = new URLSearchParams();
params.set("format", "png");
params.set("theme", readmeTheme);
params.set("cols", String(readmeCols));
params.set(
"blocks",
uniqueBlocks.length > 0 ? uniqueBlocks.join(",") : "bio,stats,langs",
);
if (layoutParts.length > 0) {
params.set("layout", layoutParts.join(","));
}
if (hide.length > 0) {
params.set("hide", hide.join(","));
}
params.set("width", "600");

const origin = typeof window !== "undefined" ? window.location.origin : "";
return `${origin}/api/card/${encodeURIComponent(username)}?${params.toString()}`;
}, [
session?.user?.login,
layout.blocks,
options.showContributionBreakdown,
options.showActivityBreakdown,
readmeTheme,
readmeCols,
includeStreak,
includeHeatmap,
]);

const onCopyReadmeUrl = async () => {
if (!readmeUrl) {
setCopyState("Sign in to generate URL");
return;
}

try {
await navigator.clipboard.writeText(readmeUrl);
setCopyState("Copied!");
} catch {
setCopyState("Copy failed");
}
};

return (
<div className="space-y-6">
<header>
Expand All @@ -182,97 +65,13 @@ export default function DashboardSettingsClient() {
/>
</section>

<section className="rounded-xl border border-card-border bg-card-bg p-5">
<h2 className="mb-4 text-sm font-medium text-muted">Display Options</h2>
<div className="grid gap-3 sm:grid-cols-2">
{toggles.map((toggle) => {
const checked = Boolean(options[toggle.key]);
return (
<label
key={toggle.key}
className="flex items-center justify-between rounded-md border border-card-border px-3 py-2 text-sm text-muted"
>
<span>{toggle.label}</span>
<input
type="checkbox"
checked={checked}
onChange={(event) => {
setOptions((previous) => ({
...previous,
[toggle.key]: event.target.checked,
}));
}}
/>
</label>
);
})}
</div>
</section>

<section className="rounded-xl border border-card-border bg-card-bg p-5">
<h2 className="mb-4 text-sm font-medium text-muted">README Card URL</h2>
<div className="grid gap-3 sm:grid-cols-2">
<label className="flex items-center justify-between rounded-md border border-card-border px-3 py-2 text-sm text-muted">
<span>Theme</span>
<select
value={readmeTheme}
onChange={(event) =>
setReadmeTheme(event.target.value === "dark" ? "dark" : "light")
}
className="rounded-md border border-card-border bg-background px-2 py-1 text-foreground"
>
<option value="light">light</option>
<option value="dark">dark</option>
</select>
</label>
<label className="flex items-center justify-between rounded-md border border-card-border px-3 py-2 text-sm text-muted">
<span>Columns</span>
<select
value={readmeCols}
onChange={(event) =>
setReadmeCols(event.target.value === "2" ? 2 : 1)
}
className="rounded-md border border-card-border bg-background px-2 py-1 text-foreground"
>
<option value={1}>1</option>
<option value={2}>2</option>
</select>
</label>
<label className="flex items-center justify-between rounded-md border border-card-border px-3 py-2 text-sm text-muted">
<span>Include streak</span>
<input
type="checkbox"
checked={includeStreak}
onChange={(e) => setIncludeStreak(e.target.checked)}
/>
</label>
<label className="flex items-center justify-between rounded-md border border-card-border px-3 py-2 text-sm text-muted">
<span>Include heatmap</span>
<input
type="checkbox"
checked={includeHeatmap}
onChange={(e) => setIncludeHeatmap(e.target.checked)}
/>
</label>
</div>
<DisplayOptionsSection options={options} setOptions={setOptions} />

<div className="mt-4 rounded-md border border-card-border bg-background px-3 py-2 text-xs text-muted break-all">
{readmeUrl || "Sign in to generate your README URL"}
</div>

<div className="mt-3 flex items-center gap-3">
<button
type="button"
onClick={onCopyReadmeUrl}
className="rounded-md bg-accent px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-accent-hover"
>
Copy URL
</button>
{copyState ? (
<span className="text-sm text-success">{copyState}</span>
) : null}
</div>
</section>
<ReadmeCardUrlSection
username={session?.user?.login}
layout={layout}
options={options}
/>

<div className="flex items-center gap-3">
<button
Expand Down
52 changes: 52 additions & 0 deletions src/components/DisplayOptionsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import type { CardDisplayOptions } from "@/lib/types";

const toggles: Array<{ key: keyof CardDisplayOptions; label: string }> = [
{ key: "showCompany", label: "Company" },
{ key: "showLocation", label: "Location" },
{ key: "showWebsite", label: "Website" },
{ key: "showTwitter", label: "Twitter" },
{ key: "showJoinedDate", label: "Joined date" },
{ key: "showTopics", label: "Topics" },
{ key: "showContributionBreakdown", label: "Contribution breakdown" },
{ key: "showStreaks", label: "Streaks" },
{ key: "showInterests", label: "Interests" },
{ key: "showActivityBreakdown", label: "Activity breakdown" },
];

interface Props {
options: CardDisplayOptions;
setOptions: React.Dispatch<React.SetStateAction<CardDisplayOptions>>;
}

export default function DisplayOptionsSection({ options, setOptions }: Props) {
return (
<section className="rounded-xl border border-card-border bg-card-bg p-5">
<h2 className="mb-4 text-sm font-medium text-muted">Display Options</h2>
<div className="grid gap-3 sm:grid-cols-2">
{toggles.map((toggle) => {
const checked = Boolean(options[toggle.key]);
return (
<label
key={toggle.key}
className="flex items-center justify-between rounded-md border border-card-border px-3 py-2 text-sm text-muted"
>
<span>{toggle.label}</span>
<input
type="checkbox"
checked={checked}
onChange={(event) => {
setOptions((previous) => ({
...previous,
[toggle.key]: event.target.checked,
}));
}}
/>
</label>
);
})}
</div>
</section>
);
}
Loading
Loading