Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
7202731
WIP: SSO and feature request board implementation
madster456 Jul 14, 2025
8f4e0e9
GitButler Workspace Commit
gitbutler-client Jul 15, 2025
dde4fc1
Implement user ID tracking for feature requests and fix linter warnings
madster456 Jul 15, 2025
969c193
Merge origin/dev into gitbutler/workspace
madster456 Jul 15, 2025
2b65e17
Update pnpm-lock.yaml after merge
madster456 Jul 15, 2025
1de8099
Remove mock data from changelog widget
madster456 Jul 15, 2025
210a90a
fix typescript errors with scrollbarWidth property
madster456 Jul 15, 2025
6295775
Merge dev into feature/stack-companion
N2D4 Jul 16, 2025
75d2763
remove old envvars from dashboard, setup new envvar in backend
madster456 Jul 17, 2025
4cff413
added relative back to header, removed commented out import
madster456 Jul 17, 2025
58b2430
feature-request-board updates for backend functionality
madster456 Jul 17, 2025
5474b51
imports keep reorganizing on save..... very annoying cannot find the …
madster456 Jul 17, 2025
27a271c
new routes on backend for feature-requests
madster456 Jul 17, 2025
d05e112
remove old route on dashboard, now on backend
madster456 Jul 17, 2025
d46cbbb
images from featurebase were blocked
madster456 Jul 17, 2025
b435187
Update apps/backend/.env
madster456 Jul 18, 2025
2161240
fix linter errors
madster456 Jul 18, 2025
c5435c6
remove dangerouslySetInnerHTML, and defined types for feature-request…
madster456 Jul 22, 2025
b6314d3
merge dev into feature/stack-companion
madster456 Jul 23, 2025
434d604
Merge branch 'dev' into feature/stack-companion
madster456 Jul 23, 2025
e3c0f2e
Merge branch 'dev' into pr/madster456/769
N2D4 Jul 31, 2025
a720ea2
Merge dev into feature/stack-companion
N2D4 Aug 1, 2025
1820b78
Merge dev into feature/stack-companion
N2D4 Aug 2, 2025
31bc972
Merge dev into feature/stack-companion
N2D4 Aug 3, 2025
5dfbf26
Merge dev into feature/stack-companion
N2D4 Aug 5, 2025
0fefd51
add STACK_ to featurebase env vars
madster456 Aug 5, 2025
ec4c4e8
add relative back to content body on sidebar-layout
madster456 Aug 5, 2025
962d888
add support tab to stack-companion, remove feedback button from top n…
madster456 Aug 5, 2025
c920dfa
shared version-check now
madster456 Aug 5, 2025
c620548
clean up props
madster456 Aug 5, 2025
eb748e7
fix viewport scrolling
madster456 Aug 5, 2025
45daff5
update icon in feature-request-board
madster456 Aug 5, 2025
e388f02
add color back in
madster456 Aug 5, 2025
449d196
active items now close stack-companion when clicked
madster456 Aug 5, 2025
ec2c4c5
fix tooltip not showing, and adjust timing
madster456 Aug 5, 2025
2334d77
since no interactive docs yet, access docs is now clickable to our ac…
madster456 Aug 5, 2025
9a604b9
implement featurebase-utils for managming users
madster456 Aug 5, 2025
7257ad4
throw error
madster456 Aug 6, 2025
24aa9d2
Not using primary emails as identifier in feature-requests route anymore
madster456 Aug 6, 2025
ae36e43
no longer using .then and .catch, rather using runAsynchronously
madster456 Aug 6, 2025
2ea4be1
using DOMParser for HTML on feature-request-board
madster456 Aug 6, 2025
9a1f05d
remove un-used comments, verified redirect to SSO works
madster456 Aug 6, 2025
3406e75
better error handling and user feedback on success/error
madster456 Aug 6, 2025
8549fb0
Merge dev into feature/stack-companion
N2D4 Aug 7, 2025
b5568b6
remove blocked userId from top-level field
madster456 Aug 7, 2025
b1b514f
no longer exporting StackAuthUser from featurebase utils, and updated…
madster456 Aug 7, 2025
85693b5
cleanup JSON body
madster456 Aug 7, 2025
2519267
naming conventions
madster456 Aug 7, 2025
7f7ac5d
add StackAssertionError in two places
madster456 Aug 7, 2025
7aafa7c
update authorName default to Stack Auth User
madster456 Aug 7, 2025
59ad738
update import to use stack-shared featurebase
madster456 Aug 7, 2025
d1d0d6c
remove duplicate code, use stack-shared featurebase
madster456 Aug 7, 2025
64d75fa
move featurebase-utils to stack-shared
madster456 Aug 7, 2025
f283d99
update user display name default to Stack Auth User
madster456 Aug 7, 2025
5b04efc
remove comments and format
madster456 Aug 7, 2025
a4d9fff
remove externalLink icon
madster456 Aug 7, 2025
0c6afd0
give more space for feature request titles, moving badge down
madster456 Aug 7, 2025
46cd29a
Added Stack Auth Logo at top of stack companion
madster456 Aug 7, 2025
50491c5
100ms load for tooltip
madster456 Aug 7, 2025
95d5f0d
no delay on tooltip hover. This is cleaner
madster456 Aug 7, 2025
b7b8b0e
sort by upvote count now
madster456 Aug 7, 2025
e16945d
new submit feature button to show/hide form for submission
madster456 Aug 7, 2025
f90a133
Merge dev into feature/stack-companion
N2D4 Aug 8, 2025
2f9ecb2
Merge branch 'dev' into feature/stack-companion
N2D4 Aug 8, 2025
8e931bd
Update user email to be sent with request instead of userID
madster456 Aug 11, 2025
7fc50ed
user with no email is now created with default email
madster456 Aug 11, 2025
e90950a
Merge branch 'dev' into feature/stack-companion
madster456 Aug 12, 2025
318e9b6
fix linter errors, add StackAssertionError
madster456 Aug 12, 2025
c5859c4
Merge dev into feature/stack-companion
N2D4 Aug 12, 2025
ef773d7
proper error handling
madster456 Aug 12, 2025
78834a5
clean up shared functions, proper error handling
madster456 Aug 12, 2025
674e0ed
remove unused jsonwebtoken in package.json
madster456 Aug 12, 2025
1cb8ea0
Merge remote-tracking branch 'origin/dev' into feature/stack-companion
madster456 Aug 12, 2025
7a3c42c
updated pnpm-lock
madster456 Aug 12, 2025
31e64f3
remove semicolon
madster456 Aug 12, 2025
0592243
better error handling
madster456 Aug 12, 2025
8baf5e1
update error handling in feedback form
madster456 Aug 12, 2025
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
1 change: 1 addition & 0 deletions apps/backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@ OTEL_EXPORTER_OTLP_ENDPOINT=# enter the OpenTelemetry endpoint here. Optional, d
STACK_INTEGRATION_CLIENTS_CONFIG=# a list of oidc-provider clients for integrations. If not provided, disables integrations
STACK_FREESTYLE_API_KEY=# enter your freestyle.sh api key
STACK_OPENAI_API_KEY=# enter your openai api key
STACK_FEATUREBASE_API_KEY=# enter your featurebase api key
STACK_STRIPE_SECRET_KEY=# enter your stripe api key
STACK_STRIPE_WEBHOOK_SECRET=# enter your stripe webhook secret
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { getOrCreateFeaturebaseUser } from "@stackframe/stack-shared/dist/utils/featurebase";

const STACK_FEATUREBASE_API_KEY = getEnvVariable("STACK_FEATUREBASE_API_KEY");

// POST /api/latest/internal/feature-requests/[featureRequestId]/upvote
export const POST = createSmartRouteHandler({
metadata: {
summary: "Toggle upvote on feature request",
description: "Toggle upvote on a feature request for the current user",
tags: ["Internal"],
},
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: yupObject({
id: yupString().oneOf(["internal"]).defined(),
})
}).defined(),
params: yupObject({
featureRequestId: yupString().defined(),
}).defined(),
body: yupObject({}),
method: yupString().oneOf(["POST"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
success: yupBoolean().defined(),
upvoted: yupBoolean().optional(),
}).defined(),
}),
handler: async ({ auth, params }) => {
// Get or create Featurebase user for consistent email handling
const featurebaseUser = await getOrCreateFeaturebaseUser({
id: auth.user.id,
primaryEmail: auth.user.primary_email,
displayName: auth.user.display_name,
profileImageUrl: auth.user.profile_image_url,
});

const response = await fetch('https://do.featurebase.app/v2/posts/upvoters', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': STACK_FEATUREBASE_API_KEY,
},
body: JSON.stringify({
id: params.featureRequestId,
email: featurebaseUser.email,
}),
});

let data;
try {
data = await response.json();
} catch (error) {
if (error instanceof StackAssertionError) {
throw error;
}
throw new StackAssertionError("Failed to parse Featurebase upvote response", { cause: error });
}

if (!response.ok) {
throw new StackAssertionError(`Featurebase upvote API error: ${data.error || 'Failed to toggle upvote'}`, { data });
}

return {
statusCode: 200,
bodyType: "json" as const,
body: { success: true, upvoted: data.upvoted },
};
},
});
190 changes: 190 additions & 0 deletions apps/backend/src/app/api/latest/internal/feature-requests/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, yupArray, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { getOrCreateFeaturebaseUser } from "@stackframe/stack-shared/dist/utils/featurebase";

const STACK_FEATUREBASE_API_KEY = getEnvVariable("STACK_FEATUREBASE_API_KEY");

// GET /api/latest/internal/feature-requests
export const GET = createSmartRouteHandler({
metadata: {
summary: "Get feature requests",
description: "Fetch all feature requests with upvote status for the current user",
tags: ["Internal"],
},
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: yupObject({
id: yupString().oneOf(["internal"]).defined(),
}).defined(),
}).defined(),
query: yupObject({}),
method: yupString().oneOf(["GET"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
posts: yupArray(yupObject({
id: yupString().defined(),
title: yupString().defined(),
content: yupString().nullable(),
upvotes: yupNumber().defined(),
date: yupString().defined(),
postStatus: yupObject({
name: yupString().defined(),
color: yupString().defined(),
}).noUnknown(false).nullable(),
userHasUpvoted: yupBoolean().defined(),
}).noUnknown(false)).defined(),
}).defined(),
}),
handler: async ({ auth }) => {
// Get or create Featurebase user for consistent email handling
const featurebaseUser = await getOrCreateFeaturebaseUser({
id: auth.user.id,
primaryEmail: auth.user.primary_email,
displayName: auth.user.display_name,
profileImageUrl: auth.user.profile_image_url,
});

// Fetch all posts with sorting
const response = await fetch('https://do.featurebase.app/v2/posts?limit=50&sortBy=upvotes:desc', {
method: 'GET',
headers: {
'X-API-Key': STACK_FEATUREBASE_API_KEY,
},
});

const data = await response.json();

if (!response.ok) {
throw new Error(`Featurebase API error: ${data.error || 'Failed to fetch feature requests'}`);
}

const posts = data.results || [];

// Check upvote status for each post for the current user using Featurebase email
const postsWithUpvoteStatus = await Promise.all(
posts.map(async (post: any) => {
let userHasUpvoted = false;

const upvoteResponse = await fetch(`https://do.featurebase.app/v2/posts/upvoters?submissionId=${post.id}`, {
method: 'GET',
headers: {
'X-API-Key': STACK_FEATUREBASE_API_KEY,
},
});

if (upvoteResponse.ok) {
const upvoteData = await upvoteResponse.json();
const upvoters = upvoteData.results || [];
userHasUpvoted = upvoters.some((upvoter: any) =>
upvoter.userId === featurebaseUser.userId
);
}

return {
id: post.id,
title: post.title,
content: post.content,
upvotes: post.upvotes || 0,
date: post.date,
postStatus: post.postStatus,
userHasUpvoted,
};
})
);

return {
statusCode: 200,
bodyType: "json" as const,
body: { posts: postsWithUpvoteStatus },
};
},
});

// POST /api/latest/internal/feature-requests
export const POST = createSmartRouteHandler({
metadata: {
summary: "Create feature request",
description: "Create a new feature request",
tags: ["Internal"],
},
request: yupObject({
auth: yupObject({
type: adaptSchema,
user: adaptSchema.defined(),
project: yupObject({
id: yupString().oneOf(["internal"]).defined(),
}).defined(),
}).defined(),
body: yupObject({
title: yupString().defined(),
content: yupString().optional(),
category: yupString().optional(),
tags: yupArray(yupString()).optional(),
commentsAllowed: yupBoolean().optional(),
customInputValues: yupObject().noUnknown(false).optional().nullable(),
}).defined(),
method: yupString().oneOf(["POST"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
success: yupBoolean().defined(),
id: yupString().optional(),
}).defined(),
}),
handler: async ({ auth, body }) => {
// Get or create Featurebase user for consistent email handling
const featurebaseUser = await getOrCreateFeaturebaseUser({
id: auth.user.id,
primaryEmail: auth.user.primary_email,
displayName: auth.user.display_name,
profileImageUrl: auth.user.profile_image_url,
});

const featurebaseRequestBody = {
title: body.title,
content: body.content || '',
category: body.category || 'feature-requests',
tags: body.tags || ['feature_request', 'dashboard'],
commentsAllowed: body.commentsAllowed ?? true,
email: featurebaseUser.email,
authorName: auth.user.display_name || 'Stack Auth User',
customInputValues: {
// Using the actual field IDs from Featurebase
"6872f858cc9682d29cf2e4c0": 'dashboard_companion', // source field
"6872f88041fa77a4dd9dab29": featurebaseUser.userId, // userId field
"6872f890143fc108288d8f5a": 'stack-auth', // projectId field
...body.customInputValues,
}
};

const response = await fetch('https://do.featurebase.app/v2/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': STACK_FEATUREBASE_API_KEY,
},
body: JSON.stringify(featurebaseRequestBody),
});

const data = await response.json();

if (!response.ok) {
throw new StackAssertionError(`Featurebase API error: ${data.error || 'Failed to create feature request'}`, { data });
}

return {
statusCode: 200,
bodyType: "json" as const,
body: { success: true, id: data.id },
};
},
});
13 changes: 12 additions & 1 deletion apps/dashboard/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ const nextConfig = {

poweredByHeader: false,

images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.featurebase-attachments.com',
port: '',
pathname: '/**',
},
],
},

async rewrites() {
return [
{
Expand All @@ -78,7 +89,7 @@ const nextConfig = {
},
];
},
skipTrailingSlashRedirect: true,
skipTrailingSlashRedirect: true,

async headers() {
return [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use client';

import { FeedbackDialog } from "@/components/feedback-dialog";
import { Link } from "@/components/link";
import { Logo } from "@/components/logo";
import { ProjectSwitcher } from "@/components/project-switcher";
import { StackCompanion } from "@/components/stack-companion";
import ThemeToggle from "@/components/theme-toggle";
import { getPublicEnvVar } from '@/lib/env';
import { cn, devFeaturesEnabledForProject } from "@/lib/utils";
Expand All @@ -14,7 +14,7 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,

Sheet,
SheetContent,
SheetTitle,
Expand Down Expand Up @@ -489,14 +489,19 @@ function HeaderBreadcrumb({

export default function SidebarLayout(props: { projectId: string, children?: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [companionExpanded, setCompanionExpanded] = useState(false);
const { resolvedTheme, setTheme } = useTheme();

return (
<div className="w-full flex">
{/* Left Sidebar */}
<div className="flex-col border-r min-w-[240px] h-screen sticky top-0 hidden md:flex backdrop-blur-md bg-white/20 dark:bg-black/20 z-[10]">
<SidebarContent projectId={props.projectId} />
</div>

{/* Main Content Area */}
<div className="flex flex-col flex-grow w-0">
{/* Header */}
<div className="h-14 border-b flex items-center justify-between sticky top-0 backdrop-blur-md bg-white/20 dark:bg-black/20 z-10 px-4 md:px-6">
<div className="hidden md:flex">
<HeaderBreadcrumb projectId={props.projectId} />
Expand All @@ -522,20 +527,24 @@ export default function SidebarLayout(props: { projectId: string, children?: Rea
</div>
</div>

<div className="flex gap-4">
<FeedbackDialog
trigger={<Button variant="outline" size='sm'>Feedback</Button>}
/>
<div className="flex gap-4 relative">
{getPublicEnvVar("NEXT_PUBLIC_STACK_EMULATOR_ENABLED") === "true" ?
<ThemeToggle /> :
<UserButton colorModeToggle={() => setTheme(resolvedTheme === 'light' ? 'dark' : 'light')} />
}
</div>
</div>

{/* Content Body - Normal scrolling */}
<div className="flex-grow relative">
{props.children}
</div>
</div>

{/* Stack Companion - Sticky positioned like left sidebar */}
<div className="h-screen sticky top-0 backdrop-blur-md bg-white/20 dark:bg-black/20 z-[10]">
<StackCompanion onExpandedChange={setCompanionExpanded} />
</div>
</div>
);
}
Loading