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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ cp apps/web/.env.example apps/web/.env
bun install

# 6. Run database migrations
cd apps/web && npx prisma migrate dev && npx prisma generate && cd ../..
cd apps/web && bunx prisma migrate dev && bunx prisma generate && cd ../..

# 7. Start dev server
bun dev
Expand Down
1 change: 1 addition & 0 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const KNOWN_ROUTES = [
"dashboard",
"debug",
"extension",
"gists",
"issues",
"notifications",
"orgs",
Expand Down
8 changes: 8 additions & 0 deletions apps/web/prisma/migrations/20260228005751_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
This file was added to keep backwards compatibility with older versions without dropping the whole database. No idea how it appeared in the first place.
Deleting the folder gives the following prisma error:
- The following migration(s) are applied to the database but missing from the local migrations directory: 20260228005751_init
- You may use prisma migrate reset to drop the development database.
- All data will be lost.
If you can figure out how to delete this folder, please do.
*/
51 changes: 44 additions & 7 deletions apps/web/src/app/(app)/[owner]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
getUserOrgTopRepos,
getContributionData,
getUserEvents,
getUserGists,
getUserStarredGists,
} from "@/lib/github";
import { ogImageUrl, ogImages } from "@/lib/og/og-utils";
import { OrgDetailContent } from "@/components/orgs/org-detail-content";
Expand Down Expand Up @@ -116,23 +118,36 @@ export default async function OwnerPage({ params }: { params: Promise<{ owner: s
let contributionData: Awaited<ReturnType<typeof getContributionData>> = null;
let orgTopRepos: Awaited<ReturnType<typeof getUserOrgTopRepos>> = [];
let activityEvents: Awaited<ReturnType<typeof getUserEvents>> = [];
let gistsData: Awaited<ReturnType<typeof getUserGists>> = [];
let starredGistsData: Awaited<ReturnType<typeof getUserStarredGists>> = [];

if (!isBot) {
try {
const [reposResult, orgsResult, contributionsResult, eventsResult] =
await Promise.allSettled([
getUserPublicRepos(userData.login, 100),
getUserPublicOrgs(userData.login),
getContributionData(userData.login),
getUserEvents(userData.login, 100),
]);
const [
reposResult,
orgsResult,
contributionsResult,
eventsResult,
gistsResult,
starredGistsResult,
] = await Promise.allSettled([
getUserPublicRepos(userData.login, 100),
getUserPublicOrgs(userData.login),
getContributionData(userData.login),
getUserEvents(userData.login, 100),
getUserGists(userData.login, 100),
getUserStarredGists(100),
]);
if (reposResult.status === "fulfilled") reposData = reposResult.value;
if (orgsResult.status === "fulfilled") orgsData = orgsResult.value;
if (contributionsResult.status === "fulfilled") {
contributionData = contributionsResult.value;
}
if (eventsResult.status === "fulfilled")
activityEvents = eventsResult.value;
if (gistsResult.status === "fulfilled") gistsData = gistsResult.value;
if (starredGistsResult.status === "fulfilled")
starredGistsData = starredGistsResult.value;
if (orgsData.length > 0) {
orgTopRepos = await getUserOrgTopRepos(
orgsData.map((o) => o.login),
Expand Down Expand Up @@ -191,6 +206,28 @@ export default async function OwnerPage({ params }: { params: Promise<{ owner: s
forks_count: r.forks_count,
language: r.language,
}))}
gists={gistsData.map((gist) => ({
id: gist.id,
description: gist.description,
html_url: gist.html_url,
public: gist.public,
created_at: gist.created_at,
updated_at: gist.updated_at,
stars: gist.stars ?? 0,
files: gist.files,
comments: gist.comments,
}))}
starredGists={starredGistsData.map((gist) => ({
id: gist.id,
description: gist.description,
html_url: gist.html_url,
public: gist.public,
created_at: gist.created_at,
updated_at: gist.updated_at,
stars: gist.stars ?? 0,
files: gist.files,
comments: gist.comments,
}))}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { notFound } from "next/navigation";
import { getGist, getGistComments } from "@/lib/github";
import { GistComments } from "@/components/gist/gist-comments";

export default async function GistCommentsPage({
params,
}: {
params: Promise<{ owner: string; gistId: string }>;
}) {
const { gistId } = await params;
const [gist, comments] = await Promise.all([
getGist(gistId).catch(() => null),
getGistComments(gistId).catch(() => []),
]);

if (!gist) {
notFound();
}

return <GistComments gist={gist} comments={comments} />;
}
70 changes: 70 additions & 0 deletions apps/web/src/app/(app)/repos/[owner]/gist/[gistId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getGist } from "@/lib/github";
import { GistNav } from "@/components/gist/gist-nav";
import { GistHeader } from "@/components/gist/gist-header";
import { ogImageUrl, ogImages } from "@/lib/og/og-utils";

export async function generateMetadata({
params,
}: {
params: Promise<{ owner: string; gistId: string }>;
}): Promise<Metadata> {
const { gistId } = await params;
const gist = await getGist(gistId).catch(() => null);

if (!gist) {
return { title: "Gist Not Found" };
}

const title = gist.description || Object.values(gist.files)[0]?.filename || "Untitled Gist";
const firstFile = Object.values(gist.files)[0];

return {
title: `${title} - Gist by ${gist.owner.login}`,
description: `Gist created by ${gist.owner.login}${firstFile ? ` - ${firstFile.filename}` : ""}`,
openGraph: {
title: `${title} - Gist`,
...ogImages(ogImageUrl({ type: "owner", owner: gist.owner.login })),
},
twitter: {
card: "summary_large_image",
...ogImages(ogImageUrl({ type: "owner", owner: gist.owner.login })),
},
};
}

export default async function GistLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ owner: string; gistId: string }>;
}) {
const { owner, gistId } = await params;
const gist = await getGist(gistId).catch(() => null);

if (!gist) {
notFound();
}

const fileCount = Object.keys(gist.files).length;

return (
<div className="space-y-4 pb-4">
<header className="border-b border-border pb-4">
<GistHeader gist={gist} />
<div className="mt-4">
<GistNav
owner={owner}
gistId={gistId}
fileCount={fileCount}
revisionCount={gist.history.length}
commentCount={gist.comments}
/>
</div>
</header>
{children}
</div>
);
}
18 changes: 18 additions & 0 deletions apps/web/src/app/(app)/repos/[owner]/gist/[gistId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { notFound } from "next/navigation";
import { getGist } from "@/lib/github";
import { GistFiles } from "@/components/gist/gist-files";

export default async function GistFilesPage({
params,
}: {
params: Promise<{ owner: string; gistId: string }>;
}) {
const { gistId } = await params;
const gist = await getGist(gistId).catch(() => null);

if (!gist) {
notFound();
}

return <GistFiles gist={gist} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { notFound } from "next/navigation";
import { getGist } from "@/lib/github";
import { GistRevisions } from "@/components/gist/gist-revisions";

export default async function GistRevisionsPage({
params,
}: {
params: Promise<{ owner: string; gistId: string }>;
}) {
const { gistId } = await params;
const gist = await getGist(gistId).catch(() => null);

if (!gist) {
notFound();
}

return <GistRevisions gist={gist} />;
}
29 changes: 29 additions & 0 deletions apps/web/src/app/(app)/repos/[owner]/gist/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use server";

import { getOctokit } from "@/lib/github";
import { getErrorMessage } from "@/lib/utils";
import { revalidatePath } from "next/cache";

export async function starGist(owner: string, gistId: string) {
const octokit = await getOctokit();
if (!octokit) return { error: "Not authenticated" };
try {
await octokit.gists.star({ gist_id: gistId });
revalidatePath(`/repos/${owner}/gist/${gistId}`);
return { success: true };
} catch (e: unknown) {
return { error: getErrorMessage(e) || "Failed to star gist" };
}
}

export async function unstarGist(owner: string, gistId: string) {
const octokit = await getOctokit();
if (!octokit) return { error: "Not authenticated" };
try {
await octokit.gists.unstar({ gist_id: gistId });
revalidatePath(`/repos/${owner}/gist/${gistId}`);
return { success: true };
} catch (e: unknown) {
return { error: getErrorMessage(e) || "Failed to unstar gist" };
}
}
Loading