diff --git a/components/Drawer.tsx b/components/Drawer.tsx index 49cd9eb..59af893 100644 --- a/components/Drawer.tsx +++ b/components/Drawer.tsx @@ -1,6 +1,6 @@ import React, { ReactNode, useEffect } from "react"; -import ClickAwayHandler from "@/components/ClickAwayHandler"; import { Drawer as D } from "vaul"; +import clsx from "clsx"; type Props = { toggleDrawer: Function; @@ -32,27 +32,24 @@ export default function Drawer({ return ( dismissible && setTimeout(() => toggleDrawer(), 350)} + onClose={() => dismissible && setDrawerIsOpen(false)} + onAnimationEnd={(isOpen) => !isOpen && toggleDrawer()} dismissible={dismissible} > - dismissible && setDrawerIsOpen(false)} - > - -
-
- {children} -
- - + +
+
+ {children} +
+ ); @@ -60,27 +57,23 @@ export default function Drawer({ return ( dismissible && setTimeout(() => toggleDrawer(), 350)} + onClose={() => dismissible && setDrawerIsOpen(false)} + onAnimationEnd={(isOpen) => !isOpen && toggleDrawer()} dismissible={dismissible} direction="right" > - dismissible && setDrawerIsOpen(false)} - className="z-30" - > - -
- {children} -
-
-
+ +
+ {children} +
+
); diff --git a/components/Modal.tsx b/components/Modal.tsx index 7b7b528..dbffb25 100644 --- a/components/Modal.tsx +++ b/components/Modal.tsx @@ -32,28 +32,25 @@ export default function Modal({ return ( dismissible && setTimeout(() => toggleModal(), 350)} + onClose={() => dismissible && setDrawerIsOpen(false)} + onAnimationEnd={(isOpen) => !isOpen && toggleModal()} dismissible={dismissible} > - dismissible && setDrawerIsOpen(false)} - > - + +
-
+ className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-neutral mb-5" + data-testid="mobile-modal-slider" + /> - {children} -
- - + {children} +
+ ); diff --git a/components/ModalContent/SurveyModal.tsx b/components/ModalContent/SurveyModal.tsx new file mode 100644 index 0000000..5b6a2ba --- /dev/null +++ b/components/ModalContent/SurveyModal.tsx @@ -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 ( + +

{t("quick_survey")}

+ +
+ +
+

{t("how_did_you_discover_linkwarden")}

+ + + + {referer === "other" && ( + { + 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" + /> + )} + + +
+
+ ); +} diff --git a/lib/api/controllers/users/userId/updateUserById.ts b/lib/api/controllers/users/userId/updateUserById.ts index 6dd09a8..8bc5e0f 100644 --- a/lib/api/controllers/users/userId/updateUserById.ts +++ b/lib/api/controllers/users/userId/updateUserById.ts @@ -208,6 +208,8 @@ export default async function updateUserById( archiveAsWaybackMachine: data.archiveAsWaybackMachine, linksRouteTo: data.linksRouteTo, preventDuplicateLinks: data.preventDuplicateLinks, + referredBy: + !user?.referredBy && data.referredBy ? data.referredBy : undefined, password: isInvited || (data.newPassword && data.newPassword !== "") ? newHashedPassword diff --git a/lib/shared/schemaValidation.ts b/lib/shared/schemaValidation.ts index 30398c0..bc6d445 100644 --- a/lib/shared/schemaValidation.ts +++ b/lib/shared/schemaValidation.ts @@ -81,6 +81,7 @@ export const UpdateUserSchema = () => { collectionOrder: z.array(z.number()).optional(), linksRouteTo: z.nativeEnum(LinksRouteTo).optional(), whitelistedUsers: z.array(z.string().max(50)).optional(), + referredBy: z.string().max(100).optional(), }); }; diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index d58ddc9..8b8310b 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -1,5 +1,5 @@ import MainLayout from "@/layouts/MainLayout"; -import { useEffect, useMemo, useState } from "react"; +import { BaseSyntheticEvent, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import React from "react"; import { toast } from "react-hot-toast"; @@ -16,7 +16,8 @@ import { useTags } from "@/hooks/store/tags"; import { useDashboardData } from "@/hooks/store/dashboardData"; import Links from "@/components/LinkViews/Links"; 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() { const { t } = useTranslation(); @@ -26,6 +27,7 @@ export default function Dashboard() { ...dashboardData } = useDashboardData(); const { data: tags = [] } = useTags(); + const { data: account = [] } = useUser(); const [numberOfLinks, setNumberOfLinks] = useState(0); @@ -41,6 +43,19 @@ export default function Dashboard() { ); }, [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(() => { if (window.innerWidth > 1900) { return 10; @@ -101,6 +116,42 @@ export default function Dashboard() { (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 (
@@ -343,6 +394,14 @@ export default function Dashboard() { )}
+ {showSurveyModal && ( + { + setShowsSurveyModal(false); + }} + /> + )} {newLinkModal && setNewLinkModal(false)} />} ); diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index 3a15b20..851a7c7 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -101,12 +101,6 @@ export default function Account() { password: password ? password : undefined, }, { - onSuccess: (data) => { - if (data.response.email !== user.email) { - toast.success(t("email_change_request")); - setEmailChangeVerificationModal(false); - } - }, onSettled: (data, error) => { setSubmitLoader(false); toast.dismiss(load); diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx index 4c490f0..bcd1db2 100644 --- a/pages/subscribe.tsx +++ b/pages/subscribe.tsx @@ -21,7 +21,6 @@ export default function Subscribe() { const { data: user = {} } = useUser(); useEffect(() => { - console.log("user", user); if ( session.status === "authenticated" && user.id && diff --git a/prisma/migrations/20241107123356_add_field/migration.sql b/prisma/migrations/20241107123356_add_field/migration.sql new file mode 100644 index 0000000..bd75a6c --- /dev/null +++ b/prisma/migrations/20241107123356_add_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "referredBy" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1341843..89738ef 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -55,6 +55,7 @@ model User { archiveAsPDF Boolean @default(true) archiveAsWaybackMachine Boolean @default(false) isPrivate Boolean @default(false) + referredBy String? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 33f9cfa..39303d4 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -417,5 +417,14 @@ "remove_user": "Remove User", "continue_to_dashboard": "Continue to Dashboard", "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.)" } \ No newline at end of file