add seventy seven

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-05-21 22:16:51 +02:00
parent 9558de3385
commit c90848765a
12 changed files with 194 additions and 1 deletions

View File

@@ -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"

View File

@@ -74,6 +74,7 @@ export default function LayoutMenu({ dashboards }: LayoutMenuProps) {
if (hasProjectId) {
user?.update({
unsafeMetadata: {
...user.unsafeMetadata,
projectId: params.projectId,
},
});

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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';

View 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 }

View 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&apos;re enjoying your experience, I&apos;d be thrilled if
you could leave a quick review. 😇
</p>
<p>
If you have any feedback or suggestions, I&apos;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;

View File

@@ -65,6 +65,9 @@ const modals = {
OverviewChartDetails: dynamic(() => import('./OverviewChartDetails'), {
loading: Loading,
}),
Testimonial: dynamic(() => import('./Testimonial'), {
loading: Loading,
}),
};
export const {

View File

@@ -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",

View File

@@ -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

View 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
View File

@@ -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: