added survey

This commit is contained in:
daniel31x13 2024-11-07 11:09:36 -05:00
parent cbf93dcf06
commit 6eac8423f8
11 changed files with 185 additions and 60 deletions

View File

@ -1,6 +1,6 @@
import React, { ReactNode, useEffect } from "react"; import React, { ReactNode, useEffect } from "react";
import ClickAwayHandler from "@/components/ClickAwayHandler";
import { Drawer as D } from "vaul"; import { Drawer as D } from "vaul";
import clsx from "clsx";
type Props = { type Props = {
toggleDrawer: Function; toggleDrawer: Function;
@ -32,27 +32,24 @@ export default function Drawer({
return ( return (
<D.Root <D.Root
open={drawerIsOpen} open={drawerIsOpen}
onClose={() => dismissible && setTimeout(() => toggleDrawer(), 350)} onClose={() => dismissible && setDrawerIsOpen(false)}
onAnimationEnd={(isOpen) => !isOpen && toggleDrawer()}
dismissible={dismissible} dismissible={dismissible}
> >
<D.Portal> <D.Portal>
<D.Overlay className="fixed inset-0 bg-black/40" /> <D.Overlay className="fixed inset-0 bg-black/40" />
<ClickAwayHandler
onClickOutside={() => dismissible && setDrawerIsOpen(false)}
>
<D.Content className="flex flex-col rounded-t-2xl mt-24 fixed bottom-0 left-0 right-0 z-30 h-[90%]"> <D.Content className="flex flex-col rounded-t-2xl mt-24 fixed bottom-0 left-0 right-0 z-30 h-[90%]">
<div <div
className="p-4 bg-base-100 rounded-t-2xl flex-1 border-neutral-content border-t overflow-y-auto" className={clsx(
"p-4 bg-base-100 rounded-t-2xl flex-1 border-neutral-content border-t overflow-y-auto",
className
)}
data-testid="mobile-modal-container" data-testid="mobile-modal-container"
> >
<div <div data-testid="mobile-modal-slider" />
className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-neutral mb-5 relative z-20"
data-testid="mobile-modal-slider"
/>
{children} {children}
</div> </div>
</D.Content> </D.Content>
</ClickAwayHandler>
</D.Portal> </D.Portal>
</D.Root> </D.Root>
); );
@ -60,27 +57,23 @@ export default function Drawer({
return ( return (
<D.Root <D.Root
open={drawerIsOpen} open={drawerIsOpen}
onClose={() => dismissible && setTimeout(() => toggleDrawer(), 350)} onClose={() => dismissible && setDrawerIsOpen(false)}
onAnimationEnd={(isOpen) => !isOpen && toggleDrawer()}
dismissible={dismissible} dismissible={dismissible}
direction="right" direction="right"
> >
<D.Portal> <D.Portal>
<D.Overlay className="fixed inset-0 bg-black/10 z-20" /> <D.Overlay className="fixed inset-0 bg-black/10 z-20" />
<ClickAwayHandler
onClickOutside={() => dismissible && setDrawerIsOpen(false)}
className="z-30"
>
<D.Content className="bg-white flex flex-col h-full w-2/5 min-w-[30rem] mt-24 fixed bottom-0 right-0 z-40 !select-auto"> <D.Content className="bg-white flex flex-col h-full w-2/5 min-w-[30rem] mt-24 fixed bottom-0 right-0 z-40 !select-auto">
<div <div
className={ className={clsx(
"p-4 bg-base-100 flex-1 border-neutral-content border-l overflow-y-auto " + "p-4 bg-base-100 flex-1 border-neutral-content border-l overflow-y-auto",
className className
} )}
> >
{children} {children}
</div> </div>
</D.Content> </D.Content>
</ClickAwayHandler>
</D.Portal> </D.Portal>
</D.Root> </D.Root>
); );

View File

@ -32,14 +32,12 @@ export default function Modal({
return ( return (
<Drawer.Root <Drawer.Root
open={drawerIsOpen} open={drawerIsOpen}
onClose={() => dismissible && setTimeout(() => toggleModal(), 350)} onClose={() => dismissible && setDrawerIsOpen(false)}
onAnimationEnd={(isOpen) => !isOpen && toggleModal()}
dismissible={dismissible} dismissible={dismissible}
> >
<Drawer.Portal> <Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" /> <Drawer.Overlay className="fixed inset-0 bg-black/40" />
<ClickAwayHandler
onClickOutside={() => dismissible && setDrawerIsOpen(false)}
>
<Drawer.Content className="flex flex-col rounded-t-2xl h-[90%] mt-24 fixed bottom-0 left-0 right-0 z-30"> <Drawer.Content className="flex flex-col rounded-t-2xl h-[90%] mt-24 fixed bottom-0 left-0 right-0 z-30">
<div <div
className="p-4 bg-base-100 rounded-t-2xl flex-1 border-neutral-content border-t overflow-y-auto" className="p-4 bg-base-100 rounded-t-2xl flex-1 border-neutral-content border-t overflow-y-auto"
@ -53,7 +51,6 @@ export default function Modal({
{children} {children}
</div> </div>
</Drawer.Content> </Drawer.Content>
</ClickAwayHandler>
</Drawer.Portal> </Drawer.Portal>
</Drawer.Root> </Drawer.Root>
); );

View File

@ -0,0 +1,68 @@
import React, { useState } from "react";
import Modal from "../Modal";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
submit: Function;
};
export default function SurveyModal({ onClose, submit }: Props) {
const { t } = useTranslation();
const [referer, setReferrer] = useState("rather_not_say");
const [other, setOther] = useState("");
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">{t("quick_survey")}</p>
<div className="divider mb-3 mt-1"></div>
<div className="flex flex-col gap-4">
<p>{t("how_did_you_discover_linkwarden")}</p>
<select
onChange={(e) => {
setReferrer(e.target.value);
setOther("");
}}
className="select border border-neutral-content focus:outline-none focus:border-primary duration-100 w-full bg-base-200 rounded-[0.375rem] min-h-0 h-[2.625rem] leading-4 p-2"
>
<option value="rather_not_say">{t("rather_not_say")}</option>
<option value="search_engine">{t("search_engine")}</option>
<option value="people_recommendation">
{t("people_recommendation")}
</option>
<option value="reddit">{t("reddit")}</option>
<option value="github">{t("github")}</option>
<option value="twitter">{t("twitter")}</option>
<option value="mastodon">{t("mastodon")}</option>
<option value="lemmy">{t("lemmy")}</option>
<option value="other">{t("other")}</option>
</select>
{referer === "other" && (
<input
type="text"
placeholder={t("please_specify")}
onChange={(e) => {
setOther(e.target.value);
}}
value={other}
className="input border border-neutral-content focus:border-primary focus:outline-none duration-100 w-full bg-base-200 rounded-[0.375rem] min-h-0 h-[2.625rem] leading-4 p-2"
/>
)}
<Button
className="ml-auto mt-3"
intent="accent"
onClick={() => submit(referer, other)}
>
<i className="bi-check2 text-xl" />
{t("submit")}
</Button>
</div>
</Modal>
);
}

View File

@ -208,6 +208,8 @@ export default async function updateUserById(
archiveAsWaybackMachine: data.archiveAsWaybackMachine, archiveAsWaybackMachine: data.archiveAsWaybackMachine,
linksRouteTo: data.linksRouteTo, linksRouteTo: data.linksRouteTo,
preventDuplicateLinks: data.preventDuplicateLinks, preventDuplicateLinks: data.preventDuplicateLinks,
referredBy:
!user?.referredBy && data.referredBy ? data.referredBy : undefined,
password: password:
isInvited || (data.newPassword && data.newPassword !== "") isInvited || (data.newPassword && data.newPassword !== "")
? newHashedPassword ? newHashedPassword

View File

@ -81,6 +81,7 @@ export const UpdateUserSchema = () => {
collectionOrder: z.array(z.number()).optional(), collectionOrder: z.array(z.number()).optional(),
linksRouteTo: z.nativeEnum(LinksRouteTo).optional(), linksRouteTo: z.nativeEnum(LinksRouteTo).optional(),
whitelistedUsers: z.array(z.string().max(50)).optional(), whitelistedUsers: z.array(z.string().max(50)).optional(),
referredBy: z.string().max(100).optional(),
}); });
}; };

View File

@ -1,5 +1,5 @@
import MainLayout from "@/layouts/MainLayout"; import MainLayout from "@/layouts/MainLayout";
import { useEffect, useMemo, useState } from "react"; import { BaseSyntheticEvent, useEffect, useMemo, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
@ -16,7 +16,8 @@ import { useTags } from "@/hooks/store/tags";
import { useDashboardData } from "@/hooks/store/dashboardData"; import { useDashboardData } from "@/hooks/store/dashboardData";
import Links from "@/components/LinkViews/Links"; import Links from "@/components/LinkViews/Links";
import useLocalSettingsStore from "@/store/localSettings"; import useLocalSettingsStore from "@/store/localSettings";
import Divider from "@/components/ui/Divider"; import { useUpdateUser, useUser } from "@/hooks/store/user";
import SurveyModal from "@/components/ModalContent/SurveyModal";
export default function Dashboard() { export default function Dashboard() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -26,6 +27,7 @@ export default function Dashboard() {
...dashboardData ...dashboardData
} = useDashboardData(); } = useDashboardData();
const { data: tags = [] } = useTags(); const { data: tags = [] } = useTags();
const { data: account = [] } = useUser();
const [numberOfLinks, setNumberOfLinks] = useState(0); const [numberOfLinks, setNumberOfLinks] = useState(0);
@ -41,6 +43,19 @@ export default function Dashboard() {
); );
}, [collections]); }, [collections]);
useEffect(() => {
if (
process.env.NEXT_PUBLIC_STRIPE === "true" &&
account.id &&
account.referredBy === null &&
// if user is using Linkwarden for more than 3 days
new Date().getTime() - new Date(account.createdAt).getTime() >
3 * 24 * 60 * 60 * 1000
) {
setShowsSurveyModal(true);
}
}, [account]);
const numberOfLinksToShow = useMemo(() => { const numberOfLinksToShow = useMemo(() => {
if (window.innerWidth > 1900) { if (window.innerWidth > 1900) {
return 10; return 10;
@ -101,6 +116,42 @@ export default function Dashboard() {
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
); );
const [showSurveyModal, setShowsSurveyModal] = useState(false);
const { data: user } = useUser();
const updateUser = useUpdateUser();
const [submitLoader, setSubmitLoader] = useState(false);
const submitSurvey = async (referer: string, other?: string) => {
if (submitLoader) return;
setSubmitLoader(true);
const load = toast.loading(t("applying"));
await updateUser.mutateAsync(
{
...user,
referredBy: referer === "other" ? "Other: " + other : referer,
},
{
onSettled: (data, error) => {
console.log(data, error);
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
toast.success(t("thanks_for_feedback"));
setShowsSurveyModal(false);
}
},
}
);
};
return ( return (
<MainLayout> <MainLayout>
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5"> <div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
@ -343,6 +394,14 @@ export default function Dashboard() {
)} )}
</div> </div>
</div> </div>
{showSurveyModal && (
<SurveyModal
submit={submitSurvey}
onClose={() => {
setShowsSurveyModal(false);
}}
/>
)}
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />} {newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
</MainLayout> </MainLayout>
); );

View File

@ -101,12 +101,6 @@ export default function Account() {
password: password ? password : undefined, password: password ? password : undefined,
}, },
{ {
onSuccess: (data) => {
if (data.response.email !== user.email) {
toast.success(t("email_change_request"));
setEmailChangeVerificationModal(false);
}
},
onSettled: (data, error) => { onSettled: (data, error) => {
setSubmitLoader(false); setSubmitLoader(false);
toast.dismiss(load); toast.dismiss(load);

View File

@ -21,7 +21,6 @@ export default function Subscribe() {
const { data: user = {} } = useUser(); const { data: user = {} } = useUser();
useEffect(() => { useEffect(() => {
console.log("user", user);
if ( if (
session.status === "authenticated" && session.status === "authenticated" &&
user.id && user.id &&

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "referredBy" TEXT;

View File

@ -55,6 +55,7 @@ model User {
archiveAsPDF Boolean @default(true) archiveAsPDF Boolean @default(true)
archiveAsWaybackMachine Boolean @default(false) archiveAsWaybackMachine Boolean @default(false)
isPrivate Boolean @default(false) isPrivate Boolean @default(false)
referredBy String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
} }

View File

@ -417,5 +417,14 @@
"remove_user": "Remove User", "remove_user": "Remove User",
"continue_to_dashboard": "Continue to Dashboard", "continue_to_dashboard": "Continue to Dashboard",
"confirm_user_removal_desc": "They will need to have a subscription to access Linkwarden again.", "confirm_user_removal_desc": "They will need to have a subscription to access Linkwarden again.",
"click_out_to_apply": "Click outside to apply" "click_out_to_apply": "Click outside to apply",
"submit": "Submit",
"thanks_for_feedback": "Thanks for your feedback!",
"quick_survey": "Quick Survey",
"how_did_you_discover_linkwarden": "How did you discover Linkwarden?",
"rather_not_say": "Rather not say",
"search_engine": "Search Engine (Google, Bing, etc.)",
"reddit": "Reddit",
"lemmy": "Lemmy",
"people_recommendation": "Recommendation (Friend, Family, etc.)"
} }