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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
### ZeroBounce API key for email validation
# ZEROBOUNCE_API_KEY=

### Plain API key for customer support integration
# (Required if NEXT_PUBLIC_INCLUDE_REPORT_ISSUE=1)
# PLAIN_API_KEY=

### Loki Configuration
# LOKI_SERVICE_NAME=
# LOKI_HOST=
Expand Down Expand Up @@ -72,6 +76,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
### When enabled, both BILLING_API_URL and NEXT_PUBLIC_STRIPE_BILLING_URL must be provided
# NEXT_PUBLIC_INCLUDE_BILLING=0

### Enable report issue feature: set to 1 to enable
### When enabled, PLAIN_API_KEY must be provided
# NEXT_PUBLIC_INCLUDE_REPORT_ISSUE=0

### Set to 1 to use mock data
# NEXT_PUBLIC_MOCK_DATA=0

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
BILLING_API_URL: https://billing.e2b-test.dev
NEXT_PUBLIC_E2B_DOMAIN: e2b-test.dev
NEXT_PUBLIC_POSTHOG_KEY: test-posthog-key
NEXT_PUBLIC_PLAIN_API_KEY: test-plain-api-key
NEXT_PUBLIC_SUPABASE_URL: https://test-supabase-url.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY: test-supabase-anon-key
NEXT_PUBLIC_STRIPE_BILLING_URL: https://test-stripe-billing.example.com
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Our Dashboard is a modern, feature-rich web application built to manage and moni
- Vercel account
- Supabase account
- PostHog account (optional for analytics)
- Plain account (optional for customer support)

### Local Development Setup

Expand Down
15 changes: 15 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.13.12",
"@team-plain/typescript-sdk": "^5.11.0",
"@theguild/remark-mermaid": "^0.2.0",
"@trpc/client": "^11.7.1",
"@trpc/react-query": "^11.7.1",
Expand Down
2 changes: 2 additions & 0 deletions src/configs/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const USE_MOCK_DATA =
export const INCLUDE_DASHBOARD_FEEDBACK_SURVEY =
process.env.NEXT_PUBLIC_POSTHOG_DASHBOARD_FEEDBACK_SURVEY_ID &&
process.env.NEXT_PUBLIC_POSTHOG_KEY

export const INCLUDE_REPORT_ISSUE = process.env.NEXT_PUBLIC_INCLUDE_REPORT_ISSUE === '1'
4 changes: 2 additions & 2 deletions src/features/dashboard/navbar/dashboard-survey-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ function DashboardSurveyPopover({ trigger }: DashboardSurveyPopoverProps) {

<PopoverContent
className="w-[400px]"
collisionPadding={20}
sideOffset={25}
collisionPadding={12}
sideOffset={12}
>
{dashboardFeedbackSurvey && (
<SurveyContent
Expand Down
130 changes: 130 additions & 0 deletions src/features/dashboard/navbar/report-issue-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client'

import { reportIssueAction } from '@/server/support/support-actions'
import { Button } from '@/ui/primitives/button'
import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/ui/primitives/card'
import { Input } from '@/ui/primitives/input'
import { Popover, PopoverContent, PopoverTrigger } from '@/ui/primitives/popover'
import { Textarea } from '@/ui/primitives/textarea'
import { usePostHog } from 'posthog-js/react'
import { useState } from 'react'
import { toast } from 'sonner'

interface ReportIssuePopoverProps {
trigger: React.ReactNode
}

export default function ReportIssuePopover({
trigger,
}: ReportIssuePopoverProps) {
const posthog = usePostHog()
const [isOpen, setIsOpen] = useState(false)
const [sandboxId, setSandboxId] = useState('')
const [description, setDescription] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [wasSubmitted, setWasSubmitted] = useState(false)

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()

if (!sandboxId.trim() || !description.trim()) {
toast.error('Please fill in all fields')
return
}

setIsSubmitting(true)

try {
const result = await reportIssueAction({
sandboxId: sandboxId.trim(),
description: description.trim(),
})

if (result?.data?.success) {
posthog.capture('issue_reported', {
sandbox_id: sandboxId.trim(),
thread_id: result.data.threadId,
})
setWasSubmitted(true)
toast.success('Issue reported successfully. Our team will review it shortly.')
setIsOpen(false)
// reset state
setSandboxId('')
setDescription('')
setTimeout(() => {
setWasSubmitted(false)
}, 100)
} else {
toast.error('Failed to report issue. Please try again.')
}
} catch (error) {
console.error('Error reporting issue:', error)
toast.error('Failed to report issue. Please try again.')
} finally {
setIsSubmitting(false)
}
}

return (
<Popover
open={isOpen}
onOpenChange={(open) => {
if (open) {
posthog.capture('issue_report_shown')
}
if (!open && !wasSubmitted) {
posthog.capture('issue_report_dismissed')
}
setIsOpen(open)
}}
>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>

<PopoverContent
className="w-[400px]"
collisionPadding={12}
sideOffset={12}
>
<CardHeader>
<CardTitle>Report Issue</CardTitle>
<CardDescription>
Our team will get back to you shortly
</CardDescription>
</CardHeader>
<CardContent className="pt-1">
<form onSubmit={handleSubmit} className="flex flex-col gap-1">
<Input
id="sandboxId"
placeholder="Enter sandbox ID"
aria-label="Sandbox ID"
value={sandboxId}
onChange={(e) => setSandboxId(e.target.value)}
disabled={isSubmitting}
/>
<Textarea
id="description"
placeholder="Describe the issue you're experiencing..."
aria-label="Issue description"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="min-h-28"
disabled={isSubmitting}
/>
<Button
type="submit"
className="w-full mt-2"
disabled={isSubmitting || !sandboxId.trim() || !description.trim()}
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</Button>
</form>
</CardContent>
</PopoverContent>
</Popover>
)
}
91 changes: 64 additions & 27 deletions src/features/dashboard/sidebar/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { INCLUDE_DASHBOARD_FEEDBACK_SURVEY } from '@/configs/flags'
import { INCLUDE_DASHBOARD_FEEDBACK_SURVEY, INCLUDE_REPORT_ISSUE } from '@/configs/flags'
import { GITHUB_URL } from '@/configs/urls'
import { cn } from '@/lib/utils'
import ExternalIcon from '@/ui/external-icon'
Expand All @@ -12,9 +12,10 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@/ui/primitives/sidebar'
import { Book, Github, MessageSquarePlus } from 'lucide-react'
import { Book, Bug, Github, MessageSquarePlus } from 'lucide-react'
import Link from 'next/link'
import DashboardSurveyPopover from '../navbar/dashboard-survey-popover'
import ReportIssuePopover from '../navbar/report-issue-popover'
import TeamBlockageAlert from './blocked-banner'

export default function DashboardSidebarFooter() {
Expand Down Expand Up @@ -60,31 +61,67 @@ export default function DashboardSidebarFooter() {
</SidebarGroup>
</SidebarFooter>

{INCLUDE_DASHBOARD_FEEDBACK_SURVEY && (
<SidebarMenu className="p-0 gap-0">
<SidebarMenuItem
key="survey"
className={cn(
'transition-all border-t group-data-[collapsible=icon]:pl-2',
SIDEBAR_TRANSITION_CLASSNAMES
)}
>
<DashboardSurveyPopover
trigger={
<SidebarMenuButton
tooltip="Survey"
variant="ghost"
className={cn(
'hover:bg-bg-hover transition-all w-full min-h-protected-statusbar justify-center group-data-[collapsible=icon]:justify-start',
SIDEBAR_TRANSITION_CLASSNAMES
)}
>
<MessageSquarePlus className="group-data-[collapsible=icon]:!size-5" />
Feedback
</SidebarMenuButton>
}
/>
</SidebarMenuItem>
{(INCLUDE_DASHBOARD_FEEDBACK_SURVEY || INCLUDE_REPORT_ISSUE) && (
<SidebarMenu
className={cn(
'flex-row gap-0 border-t group-data-[collapsible=icon]:flex-col',
SIDEBAR_TRANSITION_CLASSNAMES
)}
>
{INCLUDE_DASHBOARD_FEEDBACK_SURVEY && (
<SidebarMenuItem
key="survey"
className={cn(
'flex-1 transition-all group-data-[collapsible=icon]:pl-2',
SIDEBAR_TRANSITION_CLASSNAMES
)}
>
<DashboardSurveyPopover
trigger={
<SidebarMenuButton
tooltip="Feedback"
variant="ghost"
className={cn(
'hover:bg-bg-hover transition-all w-full min-h-protected-statusbar justify-center group-data-[collapsible=icon]:justify-start',
SIDEBAR_TRANSITION_CLASSNAMES
)}
>
<MessageSquarePlus className="hidden group-data-[collapsible=icon]:block group-data-[collapsible=icon]:!size-5" />
Feedback
</SidebarMenuButton>
}
/>
</SidebarMenuItem>
)}
{INCLUDE_DASHBOARD_FEEDBACK_SURVEY && INCLUDE_REPORT_ISSUE && (
/* separator */
<div className="border-r h-full hidden group-data-[collapsible=icon]:hidden group-data-[state=expanded]:block" />
)}
{INCLUDE_REPORT_ISSUE && (
<SidebarMenuItem
key="report-issue"
className={cn(
'flex-1 transition-all group-data-[collapsible=icon]:pl-2',
SIDEBAR_TRANSITION_CLASSNAMES
)}
>
<ReportIssuePopover
trigger={
<SidebarMenuButton
tooltip="Report Issue"
variant="ghost"
className={cn(
'hover:bg-bg-hover transition-all w-full min-h-protected-statusbar justify-center group-data-[collapsible=icon]:justify-start',
SIDEBAR_TRANSITION_CLASSNAMES
)}
>
<Bug className="hidden group-data-[collapsible=icon]:block group-data-[collapsible=icon]:!size-5" />
Report Issue
</SidebarMenuButton>
}
/>
</SidebarMenuItem>
)}
</SidebarMenu>
)}
</>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const serverSchema = z.object({

BILLING_API_URL: z.url().optional(),
ZEROBOUNCE_API_KEY: z.string().optional(),
PLAIN_API_KEY: z.string().min(1).optional(),

OTEL_SERVICE_NAME: z.string().optional(),
OTEL_EXPORTER_OTLP_ENDPOINT: z.url().optional(),
Expand Down Expand Up @@ -48,6 +49,7 @@ export const clientSchema = z.object({

NEXT_PUBLIC_INCLUDE_BILLING: z.string().optional(),
NEXT_PUBLIC_INCLUDE_ARGUS: z.string().optional(),
NEXT_PUBLIC_INCLUDE_REPORT_ISSUE: z.string().optional(),
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(),
NEXT_PUBLIC_SCAN: z.string().optional(),
NEXT_PUBLIC_MOCK_DATA: z.string().optional(),
Expand Down
Loading
Loading