add seventy seven
This commit is contained in:
@@ -29,6 +29,9 @@ ENV NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
|
||||
ARG CLERK_SECRET_KEY
|
||||
ENV CLERK_SECRET_KEY=$CLERK_SECRET_KEY
|
||||
|
||||
ARG SEVENTY_SEVEN_API_KEY
|
||||
ENV SEVENTY_SEVEN_API_KEY=$SEVENTY_SEVEN_API_KEY
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
@@ -74,6 +74,7 @@ export default function LayoutMenu({ dashboards }: LayoutMenuProps) {
|
||||
if (hasProjectId) {
|
||||
user?.update({
|
||||
unsafeMetadata: {
|
||||
...user.unsafeMetadata,
|
||||
projectId: params.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@openpanel/db';
|
||||
|
||||
import { LayoutSidebar } from './layout-sidebar';
|
||||
import SideEffects from './side-effects';
|
||||
|
||||
interface AppLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -48,6 +49,7 @@ export default async function AppLayout({
|
||||
{...{ organizationSlug, projectId, organizations, dashboards }}
|
||||
/>
|
||||
<div className="transition-all lg:pl-72">{children}</div>
|
||||
<SideEffects />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { pushModal, useOnPushModal } from '@/modals';
|
||||
import { useUser } from '@clerk/nextjs';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
|
||||
import { trackEvent } from '@openpanel/nextjs';
|
||||
|
||||
export default function SideEffects() {
|
||||
const { user } = useUser();
|
||||
const accountAgeInDays = differenceInDays(
|
||||
new Date(),
|
||||
user?.createdAt || new Date()
|
||||
);
|
||||
useOnPushModal('Testimonial', (open) => {
|
||||
if (!open) {
|
||||
user?.update({
|
||||
unsafeMetadata: {
|
||||
...user.unsafeMetadata,
|
||||
testimonial: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const showTestimonial =
|
||||
user && !user.unsafeMetadata.testimonial && accountAgeInDays > 7;
|
||||
|
||||
useEffect(() => {
|
||||
if (showTestimonial) {
|
||||
pushModal('Testimonial');
|
||||
trackEvent('testimonials_shown');
|
||||
}
|
||||
}, [showTestimonial]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import makeStore from '@/redux';
|
||||
import { api } from '@/trpc/client';
|
||||
import { ClerkProvider, useAuth } from '@clerk/nextjs';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { httpLink, unstable_httpBatchStreamLink } from '@trpc/client';
|
||||
import { unstable_httpBatchStreamLink } from '@trpc/client';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { Toaster } from 'sonner';
|
||||
|
||||
24
apps/dashboard/src/components/ui/textarea.tsx
Normal file
24
apps/dashboard/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/utils/cn"
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
78
apps/dashboard/src/modals/Testimonial.tsx
Normal file
78
apps/dashboard/src/modals/Testimonial.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { api } from '@/trpc/client';
|
||||
import { useUser } from '@clerk/nextjs';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { trackEvent } from '@openpanel/nextjs';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent } from './Modal/Container';
|
||||
|
||||
const validator = z.object({
|
||||
body: z.string().min(3),
|
||||
});
|
||||
|
||||
type IForm = z.infer<typeof validator>;
|
||||
|
||||
const Testimonial = () => {
|
||||
const mutation = api.ticket.create.useMutation();
|
||||
const params = useAppParams();
|
||||
const form = useForm<IForm>({
|
||||
resolver: zodResolver(validator),
|
||||
});
|
||||
return (
|
||||
<ModalContent className="p-0">
|
||||
<div className="w-full rounded-t-lg border-b border-border bg-secondary p-4">
|
||||
<h1 className="mb-2 text-2xl font-bold">Review time 🫶</h1>
|
||||
<p className="mb-2">
|
||||
Thank you so much for using Openpanel — it truly means a great deal to
|
||||
me! If you're enjoying your experience, I'd be thrilled if
|
||||
you could leave a quick review. 😇
|
||||
</p>
|
||||
<p>
|
||||
If you have any feedback or suggestions, I'd love to hear them as
|
||||
well! 🚀
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
className="p-4"
|
||||
onSubmit={form.handleSubmit(async ({ body }) => {
|
||||
try {
|
||||
await mutation.mutateAsync({
|
||||
subject: 'New testimonial',
|
||||
body,
|
||||
meta: {
|
||||
...params,
|
||||
},
|
||||
});
|
||||
toast.success('Thanks for your feedback 🚀');
|
||||
trackEvent('testimonials_sent');
|
||||
popModal();
|
||||
} catch (e) {
|
||||
toast.error('Something went wrong. Please try again later.');
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Textarea
|
||||
placeholder="Type your review here."
|
||||
{...form.register('body')}
|
||||
/>
|
||||
<div className="mt-4 flex justify-between gap-2">
|
||||
<Button type="button" variant="secondary" onClick={() => popModal()}>
|
||||
Maybe later
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
Send it
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Testimonial;
|
||||
@@ -65,6 +65,9 @@ const modals = {
|
||||
OverviewChartDetails: dynamic(() => import('./OverviewChartDetails'), {
|
||||
loading: Loading,
|
||||
}),
|
||||
Testimonial: dynamic(() => import('./Testimonial'), {
|
||||
loading: Loading,
|
||||
}),
|
||||
};
|
||||
|
||||
export const {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@openpanel/constants": "workspace:*",
|
||||
"@openpanel/db": "workspace:*",
|
||||
"@openpanel/validation": "workspace:*",
|
||||
"@seventy-seven/sdk": "0.0.0-beta.2",
|
||||
"@trpc/server": "^10.45.1",
|
||||
"date-fns": "^3.3.1",
|
||||
"mathjs": "^12.3.2",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { projectRouter } from './routers/project';
|
||||
import { referenceRouter } from './routers/reference';
|
||||
import { reportRouter } from './routers/report';
|
||||
import { shareRouter } from './routers/share';
|
||||
import { ticketRouter } from './routers/ticket';
|
||||
import { userRouter } from './routers/user';
|
||||
import { createTRPCRouter } from './trpc';
|
||||
|
||||
@@ -30,6 +31,7 @@ export const appRouter = createTRPCRouter({
|
||||
share: shareRouter,
|
||||
onboarding: onboardingRouter,
|
||||
reference: referenceRouter,
|
||||
ticket: ticketRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
34
packages/trpc/src/routers/ticket.ts
Normal file
34
packages/trpc/src/routers/ticket.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { clerkClient } from '@clerk/fastify';
|
||||
import { SeventySevenClient } from '@seventy-seven/sdk';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||
|
||||
const API_KEY = process.env.SEVENTY_SEVEN_API_KEY!;
|
||||
const client = new SeventySevenClient(API_KEY);
|
||||
|
||||
export const ticketRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
subject: z.string(),
|
||||
body: z.string(),
|
||||
meta: z.record(z.string(), z.unknown()),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!API_KEY) {
|
||||
throw new Error('Ticket system not configured');
|
||||
}
|
||||
|
||||
const user = await clerkClient.users.getUser(ctx.session.userId);
|
||||
|
||||
return client.createTicket({
|
||||
subject: input.subject,
|
||||
body: input.body,
|
||||
meta: input.meta,
|
||||
senderEmail: user.primaryEmailAddress?.emailAddress || 'none',
|
||||
senderFullName: user.fullName || 'none',
|
||||
});
|
||||
}),
|
||||
});
|
||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -1243,6 +1243,9 @@ importers:
|
||||
'@openpanel/validation':
|
||||
specifier: workspace:*
|
||||
version: link:../validation
|
||||
'@seventy-seven/sdk':
|
||||
specifier: 0.0.0-beta.2
|
||||
version: 0.0.0-beta.2
|
||||
'@trpc/server':
|
||||
specifier: ^10.45.1
|
||||
version: 10.45.1
|
||||
@@ -6979,6 +6982,10 @@ packages:
|
||||
join-component: 1.1.0
|
||||
dev: false
|
||||
|
||||
/@seventy-seven/sdk@0.0.0-beta.2:
|
||||
resolution: {integrity: sha512-aqRk1zvfEiq0yn20UwGvcQ1PGOLrGo3fSKZmhgKOesh8HFnimrduJ4uGJjpJUSmvvKafL/rC7wXCrMuwtSiYQg==}
|
||||
dev: false
|
||||
|
||||
/@sideway/address@4.1.5:
|
||||
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user