mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-01 08:37:31 +02:00
📢 Add Featurebase (#796)
This commit is contained in:
parent
5532723d23
commit
222947292b
8 changed files with 151 additions and 157 deletions
13
README.md
13
README.md
|
@ -89,13 +89,14 @@ Thank you to our sponsors for making this project possible.
|
||||||
|
|
||||||
[Become a sponsor →](https://github.com/sponsors/lukevella)
|
[Become a sponsor →](https://github.com/sponsors/lukevella)
|
||||||
|
|
||||||
This project is also supported by the following companies through their open-source sponsorships.
|
And thank you to these companies for sponsoring and showing support for this project.
|
||||||
|
|
||||||
<a href="https://appwrite.io"><img src="./assets/images/appwrite.svg" alt="appwrite" height="30" /></a>
|
<a href="https://appwrite.io?utm_source=rallly"><img src="./assets/images/appwrite.svg" alt="appwrite" height="24" /></a>
|
||||||
|
|
||||||
<a href="https://vercel.com/?utm_source=rallly&utm_campaign=oss"><img src="./apps/landing/public/vercel-logotype-dark.svg" alt="Powered by Vercel" height="30" /></a>
|
<a href="https://vercel.com/?utm_source=rallly&utm_campaign=oss"><img src="./apps/landing/public/vercel-logotype-dark.svg" alt="Powered by Vercel" height="24" /></a>
|
||||||
|
|
||||||
<a href="https://m.do.co/c/f91efc9c9e50"><img src="./apps/landing/public/digitalocean.svg" alt="Digital Ocean" height="30" /></a>
|
<a href="https://m.do.co/c/f91efc9c9e50"><img src="./apps/landing/public/digitalocean.svg" alt="Digital Ocean" height="24" /></a>
|
||||||
|
|
||||||
<a href="https://sentry.io"><img src="./apps/landing/public/sentry.svg" alt="Sentry" height="30" /></a>
|
<a href="https://sentry.io?utm_source=rallly"><img src="./apps/landing/public/sentry.svg" alt="Sentry" height="24" /></a>
|
||||||
<a href="https://cloudron.io"><img src="./assets/images/cloudron-logo.svg" alt="Cloudron" height="36"></a>
|
<a href="https://cloudron.io?utm_source=rallly"><img src="./assets/images/cloudron-logo.svg" alt="Cloudron" height="32"></a>
|
||||||
|
<a href="https://featurebase.app?utm_source=rallly"><img src="./assets/images/featurebase.svg" alt="Featurebase" height="30"></a>
|
||||||
|
|
|
@ -9,7 +9,7 @@ const PrivacyPolicy = () => {
|
||||||
<NextSeo title="Privacy Policy" />
|
<NextSeo title="Privacy Policy" />
|
||||||
<div className="prose my-16 mx-auto max-w-3xl rounded-lg bg-white p-8 shadow-md">
|
<div className="prose my-16 mx-auto max-w-3xl rounded-lg bg-white p-8 shadow-md">
|
||||||
<h1>Privacy Policy</h1>
|
<h1>Privacy Policy</h1>
|
||||||
<p>Last updated: 19 April 2023</p>
|
<p>Last updated: 1 August 2023</p>
|
||||||
<p>
|
<p>
|
||||||
At rallly.co, we take your privacy seriously. This privacy policy
|
At rallly.co, we take your privacy seriously. This privacy policy
|
||||||
explains how we collect, use, and disclose your personal data, and
|
explains how we collect, use, and disclose your personal data, and
|
||||||
|
@ -66,10 +66,18 @@ const PrivacyPolicy = () => {
|
||||||
<h2>Sharing of personal data</h2>
|
<h2>Sharing of personal data</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
We do not share your personal data with third parties, except for our
|
We do not share your personal data with any third parties for
|
||||||
data processor, Posthog, which is based in the United States. We have
|
marketing or commercial purposes. We may share your personal data with
|
||||||
implemented appropriate safeguards to protect your personal data when
|
third parties to provide you with our services, to comply with
|
||||||
it is transferred outside of the European Economic Area.
|
applicable laws and regulations, to respond to a subpoena, search
|
||||||
|
warrant or other lawful request for information we receive, or to
|
||||||
|
otherwise protect our rights.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
For example, we use Featurebase to make it easy for users to submit
|
||||||
|
feedback. Your name and email may be shared with Featurbase to provide
|
||||||
|
a seamless transition between the two services.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>Your rights</h2>
|
<h2>Your rights</h2>
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"changeNameDescription": "Enter a new name for this participant.",
|
"changeNameDescription": "Enter a new name for this participant.",
|
||||||
"changeNameInfo": "This will not affect any votes you have already made.",
|
"changeNameInfo": "This will not affect any votes you have already made.",
|
||||||
"close": "Close",
|
|
||||||
"comments": "Comments",
|
"comments": "Comments",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"copied": "Copied",
|
"copied": "Copied",
|
||||||
|
@ -36,11 +35,6 @@
|
||||||
"emailPlaceholder": "jessie.smith@example.com",
|
"emailPlaceholder": "jessie.smith@example.com",
|
||||||
"expiredOrInvalidLink": "This link is expired or invalid. Please request a new link.",
|
"expiredOrInvalidLink": "This link is expired or invalid. Please request a new link.",
|
||||||
"exportToCsv": "Export to CSV",
|
"exportToCsv": "Export to CSV",
|
||||||
"feedbackFormFooter": "Need help? Visit the <a>support page</a>.",
|
|
||||||
"feedbackFormLabel": "How can we improve <appname />?",
|
|
||||||
"feedbackFormPlaceholder": "Share your thoughts…",
|
|
||||||
"feedbackFormTitle": "Feedback Form",
|
|
||||||
"feedbackSent": "Thank you! Your feedback has been sent.",
|
|
||||||
"forgetMe": "Forget me",
|
"forgetMe": "Forget me",
|
||||||
"guest": "Guest",
|
"guest": "Guest",
|
||||||
"ifNeedBe": "If need be",
|
"ifNeedBe": "If need be",
|
||||||
|
@ -76,7 +70,6 @@
|
||||||
"response": "Response",
|
"response": "Response",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saveInstruction": "Select your availability and click <b>{action}</b>",
|
"saveInstruction": "Select your availability and click <b>{action}</b>",
|
||||||
"send": "Send",
|
|
||||||
"sendFeedback": "Send Feedback",
|
"sendFeedback": "Send Feedback",
|
||||||
"specifyTimes": "Specify times",
|
"specifyTimes": "Specify times",
|
||||||
"specifyTimesDescription": "Include start and end times for each option",
|
"specifyTimesDescription": "Include start and end times for each option",
|
||||||
|
@ -213,5 +206,8 @@
|
||||||
"disableCommentsDescription": "Remove the option to leave a comment on the poll",
|
"disableCommentsDescription": "Remove the option to leave a comment on the poll",
|
||||||
"planCustomizablePollSettings": "Customizable poll settings",
|
"planCustomizablePollSettings": "Customizable poll settings",
|
||||||
"clockPreferences": "Clock Preferences",
|
"clockPreferences": "Clock Preferences",
|
||||||
"clockPreferencesDescription": "Set your preferred time zone and time format."
|
"clockPreferencesDescription": "Set your preferred time zone and time format.",
|
||||||
|
"featureRequest": "Request a Feature",
|
||||||
|
"bugReport": "Report an Issue",
|
||||||
|
"getSupport": "Get Support"
|
||||||
}
|
}
|
||||||
|
|
44
apps/web/src/components/featurebase.tsx
Normal file
44
apps/web/src/components/featurebase.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import Script from "next/script";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useUser } from "@/components/user-provider";
|
||||||
|
|
||||||
|
export const FeaturebaseProvider = ({ children }: React.PropsWithChildren) => {
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (user.isGuest) return;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const win = window as any;
|
||||||
|
|
||||||
|
if (typeof win.Featurebase !== "function") {
|
||||||
|
win.Featurebase = function () {
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
(win.Featurebase.q = win.Featurebase.q || []).push(arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
win.Featurebase(
|
||||||
|
"identify",
|
||||||
|
{
|
||||||
|
organization: "rallly",
|
||||||
|
|
||||||
|
// Required. Replace with your customers data.
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
(err: Error) => {
|
||||||
|
// Callback function. Called when identify completed.
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [user]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Script src="https://do.featurebase.app/js/sdk.js" id="featurebase-sdk" />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,107 +1,75 @@
|
||||||
import { trpc } from "@rallly/backend";
|
import {
|
||||||
import { CheckCircleIcon, MegaphoneIcon } from "@rallly/icons";
|
BugIcon,
|
||||||
import { Button } from "@rallly/ui/button";
|
LifeBuoyIcon,
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
LightbulbIcon,
|
||||||
|
MegaphoneIcon,
|
||||||
|
SmileIcon,
|
||||||
|
} from "@rallly/icons";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@rallly/ui/dropdown-menu";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import { Logo } from "@/components/logo";
|
import { FeaturebaseProvider } from "@/components/featurebase";
|
||||||
import { useModalState } from "@/components/modal/use-modal";
|
|
||||||
|
|
||||||
const FeedbackForm = (props: { onClose: () => void }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const sendFeedback = trpc.feedback.send.useMutation();
|
|
||||||
const { handleSubmit, register, formState } = useForm<{ content: string }>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(async (data) => {
|
|
||||||
await sendFeedback.mutateAsync(data);
|
|
||||||
})}
|
|
||||||
className="shadow-huge animate-popIn fixed bottom-8 right-8 z-20 w-[460px] max-w-full origin-bottom-right space-y-2 overflow-hidden rounded-md border bg-white p-3"
|
|
||||||
>
|
|
||||||
{formState.isSubmitted ? (
|
|
||||||
<div className="absolute inset-0 flex h-full w-full items-center justify-center bg-white">
|
|
||||||
<div className="space-y-3 text-center">
|
|
||||||
<CheckCircleIcon className="inline-block h-14 text-green-500" />
|
|
||||||
<div>{t("feedbackSent")}</div>
|
|
||||||
<div>
|
|
||||||
<button onClick={props.onClose} className="text-link">
|
|
||||||
{t("close")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="font-semibold text-gray-800">
|
|
||||||
{t("feedbackFormTitle")}
|
|
||||||
</div>
|
|
||||||
<fieldset>
|
|
||||||
<label className="mb-2" htmlFor="feedback">
|
|
||||||
<Trans
|
|
||||||
t={t}
|
|
||||||
i18nKey="feedbackFormLabel"
|
|
||||||
components={{ appname: <Logo /> }}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="feedback"
|
|
||||||
autoFocus={true}
|
|
||||||
placeholder={t("feedbackFormPlaceholder")}
|
|
||||||
rows={4}
|
|
||||||
className="w-full border bg-gray-50 p-2 text-gray-800"
|
|
||||||
{...register("content", { required: true })}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
<div className="flex items-center justify-between gap-4">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button onClick={props.onClose}>{t("cancel")}</Button>
|
|
||||||
<Button
|
|
||||||
loading={formState.isSubmitting}
|
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
{t("send")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">
|
|
||||||
<Trans
|
|
||||||
t={t}
|
|
||||||
i18nKey="feedbackFormFooter"
|
|
||||||
components={{
|
|
||||||
a: (
|
|
||||||
<Link href="https://support.rallly.co" className="text-link" />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const FeedbackButton = () => {
|
const FeedbackButton = () => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isVisible, show, close] = useModalState();
|
|
||||||
if (isVisible) {
|
|
||||||
return <FeedbackForm onClose={close} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<FeaturebaseProvider>
|
||||||
<TooltipTrigger asChild>
|
<DropdownMenu modal={false}>
|
||||||
<button
|
<DropdownMenuTrigger className="shadow-huge fixed bottom-8 right-6 hidden h-12 w-12 items-center justify-center rounded-full bg-gray-800 hover:bg-gray-700 active:shadow-none sm:inline-flex">
|
||||||
onClick={show}
|
<MegaphoneIcon className="h-5 text-white" />
|
||||||
className="shadow-huge fixed bottom-8 right-8 hidden h-14 w-14 items-center justify-center rounded-full bg-gray-800 sm:inline-flex"
|
</DropdownMenuTrigger>
|
||||||
>
|
<DropdownMenuContent sideOffset={10} align="end">
|
||||||
<MegaphoneIcon className="h-7 text-white" />
|
<DropdownMenuLabel>
|
||||||
</button>
|
<Trans i18nKey="menu" />
|
||||||
</TooltipTrigger>
|
</DropdownMenuLabel>
|
||||||
<TooltipContent side="left">
|
<DropdownMenuSeparator />
|
||||||
<p>{t("sendFeedback")}</p>
|
<DropdownMenuItem asChild>
|
||||||
</TooltipContent>
|
<Link
|
||||||
</Tooltip>
|
href={`https://rallly.featurebase.app/?b=feedback`}
|
||||||
|
target={"_blank"}
|
||||||
|
>
|
||||||
|
<SmileIcon className="mr-2 h-4 w-4" />
|
||||||
|
<Trans i18nKey="sendFeedback" defaults="Send Feedback" />
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={`https://rallly.featurebase.app/?b=feature-request`}
|
||||||
|
target={"_blank"}
|
||||||
|
>
|
||||||
|
<LightbulbIcon className="mr-2 h-4 w-4" />
|
||||||
|
<Trans
|
||||||
|
i18nKey={"featureRequest"}
|
||||||
|
defaults={"Request a Feature"}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={`https://rallly.featurebase.app/?b=bug-reports`}
|
||||||
|
target={"_blank"}
|
||||||
|
>
|
||||||
|
<BugIcon className="mr-2 h-4 w-4" />
|
||||||
|
<Trans i18nKey={"bugReport"} defaults={"Report an Issue"} />
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link href={`https://support.rallly.co`} target={"_blank"}>
|
||||||
|
<LifeBuoyIcon className="mr-2 h-4 w-4" />
|
||||||
|
<Trans i18nKey={"getSupport"} defaults={"Get Support"} />
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FeaturebaseProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
16
assets/images/featurebase.svg
Normal file
16
assets/images/featurebase.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.5 KiB |
|
@ -1,37 +0,0 @@
|
||||||
import { prisma } from "@rallly/database";
|
|
||||||
import { sendRawEmail } from "@rallly/emails";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { publicProcedure, router } from "../trpc";
|
|
||||||
|
|
||||||
export const feedback = router({
|
|
||||||
send: publicProcedure
|
|
||||||
.input(z.object({ content: z.string() }))
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
let replyTo: string | undefined;
|
|
||||||
let name = "Guest";
|
|
||||||
|
|
||||||
if (!ctx.user.isGuest) {
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id: ctx.user.id },
|
|
||||||
select: { email: true, name: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
replyTo = user.email;
|
|
||||||
name = user.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendRawEmail({
|
|
||||||
to: process.env.NEXT_PUBLIC_FEEDBACK_EMAIL,
|
|
||||||
from: {
|
|
||||||
name: "Rallly Feedback Form",
|
|
||||||
address: process.env.SUPPORT_EMAIL ?? "",
|
|
||||||
},
|
|
||||||
subject: "Feedback",
|
|
||||||
replyTo,
|
|
||||||
text: `${name} says:\n\n${input.content}`,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { mergeRouters, router } from "../trpc";
|
import { mergeRouters, router } from "../trpc";
|
||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { feedback } from "./feedback";
|
|
||||||
import { polls } from "./polls";
|
import { polls } from "./polls";
|
||||||
import { user } from "./user";
|
import { user } from "./user";
|
||||||
import { userPreferences } from "./user-preferences";
|
import { userPreferences } from "./user-preferences";
|
||||||
|
@ -12,7 +11,6 @@ export const appRouter = mergeRouters(
|
||||||
auth,
|
auth,
|
||||||
polls,
|
polls,
|
||||||
user,
|
user,
|
||||||
feedback,
|
|
||||||
userPreferences,
|
userPreferences,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue