added survey
This commit is contained in:
parent
cbf93dcf06
commit
6eac8423f8
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 &&
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "referredBy" TEXT;
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.)"
|
||||||
}
|
}
|
Ŝarĝante…
Reference in New Issue