mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 11:29:22 -05:00
Merge branch 'main' of https://github.com/formbricks/formbricks into main
This commit is contained in:
@@ -14,10 +14,10 @@
|
||||
"@formbricks/pmf": "workspace:*",
|
||||
"@formbricks/react": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@heroicons/react": "^2.0.14",
|
||||
"@heroicons/react": "^2.0.16",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"clsx": "^1.2.1",
|
||||
"next": "latest",
|
||||
"next": "13.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-toastify": "^9.1.1"
|
||||
@@ -25,13 +25,13 @@
|
||||
"devDependencies": {
|
||||
"@formbricks/tailwind-config": "workspace:*",
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/node": "^18.14.1",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"postcss": "^8.4.21",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.9.4"
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ function Header({ navigation }: any) {
|
||||
className={clsx(
|
||||
"sticky top-0 z-50 flex flex-wrap items-center justify-between bg-slate-100 px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500 dark:shadow-none sm:px-6 lg:px-8",
|
||||
isScrolled
|
||||
? "[@supports(backdrop-filter:blur(0))]:bg-slate-100/75 dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75 bg-slate-100/90 backdrop-blur dark:bg-slate-900/90"
|
||||
? "bg-slate-100/90 backdrop-blur dark:bg-slate-900/90 [@supports(backdrop-filter:blur(0))]:bg-slate-100/75 dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75"
|
||||
: "dark:bg-transparent"
|
||||
)}>
|
||||
<div className="mr-6 flex lg:hidden">
|
||||
|
||||
@@ -10,28 +10,28 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docsearch/react": "^3.3.2",
|
||||
"@docsearch/react": "^3.3.3",
|
||||
"@formbricks/engine-react": "workspace:*",
|
||||
"@formbricks/feedback": "workspace:*",
|
||||
"@formbricks/pmf": "workspace:*",
|
||||
"@formbricks/react": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@headlessui/react": "^1.7.8",
|
||||
"@heroicons/react": "^2.0.14",
|
||||
"@headlessui/react": "^1.7.11",
|
||||
"@heroicons/react": "^2.0.16",
|
||||
"@mapbox/rehype-prism": "^0.8.0",
|
||||
"@mdx-js/loader": "^2.2.1",
|
||||
"@mdx-js/react": "^2.2.1",
|
||||
"@next/mdx": "^13.1.6",
|
||||
"@mdx-js/loader": "^2.3.0",
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@next/mdx": "^13.2.1",
|
||||
"add": "^2.0.6",
|
||||
"clsx": "^1.2.1",
|
||||
"lottie-web": "^5.10.2",
|
||||
"next": "13.1.6",
|
||||
"next-plausible": "^3.7.1",
|
||||
"next-sitemap": "^3.1.48",
|
||||
"next": "13.2.1",
|
||||
"next-plausible": "^3.7.2",
|
||||
"next-sitemap": "^3.1.52",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.43.0",
|
||||
"react-hook-form": "^7.43.2",
|
||||
"react-responsive-embed": "^2.1.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.31.3"
|
||||
@@ -39,14 +39,14 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/react": "18.0.27",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@types/node": "18.14.1",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "8.33.0",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"postcss": "^8.4.21",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "4.9.4"
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "4.9.5"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-13
@@ -14,40 +14,40 @@
|
||||
"@formbricks/engine-react": "workspace:*",
|
||||
"@formbricks/feedback": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@headlessui/react": "^1.7.8",
|
||||
"@heroicons/react": "^2.0.14",
|
||||
"@sentry/nextjs": "^7.34.0",
|
||||
"@vercel/analytics": "^0.1.8",
|
||||
"@headlessui/react": "^1.7.11",
|
||||
"@heroicons/react": "^2.0.16",
|
||||
"@sentry/nextjs": "^7.38.0",
|
||||
"@vercel/analytics": "^0.1.10",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"next": "^13.1.6",
|
||||
"next-auth": "^4.19.0",
|
||||
"next": "^13.2.1",
|
||||
"next-auth": "^4.19.2",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"nodemailer": "^6.9.1",
|
||||
"platform": "^1.3.6",
|
||||
"posthog-js": "^1.45.1",
|
||||
"posthog-js": "^1.46.2",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-loader-spinner": "^5.3.4",
|
||||
"react-toastify": "^9.1.1",
|
||||
"stripe": "^11.8.0",
|
||||
"stripe": "^11.12.0",
|
||||
"swr": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/database": "workspace:*",
|
||||
"@formbricks/tailwind-config": "workspace:*",
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/node": "^18.14.1",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"postcss": "^8.4.21",
|
||||
"typescript": "^4.9.4"
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function NewFormModal({ open, setOpen, organisationId }: FormOnbo
|
||||
name: "Product-Market Fit Survey",
|
||||
description: "Leverage the Superhuman PMF engine.",
|
||||
icon: PMFIcon,
|
||||
needsUpgrade: process.env.NEXT_PUBLIC_IS_FORMBRICKS_CLOUD && organisation?.plan === "free",
|
||||
needsUpgrade: process.env.NEXT_PUBLIC_IS_FORMBRICKS_CLOUD === "1" && organisation?.plan === "free",
|
||||
},
|
||||
],
|
||||
[organisation]
|
||||
|
||||
@@ -9,6 +9,7 @@ import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
import Tagging from "../shared/Tagging";
|
||||
|
||||
export default function PMFTimeline({ submissions }) {
|
||||
const router = useRouter();
|
||||
@@ -130,6 +131,7 @@ export default function PMFTimeline({ submissions }) {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Tagging submission={submission} />
|
||||
<div className=" bg-gray-50 p-4 sm:p-6">
|
||||
<div className="flex w-full justify-between gap-4">
|
||||
<div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
import Tagging from "../shared/Tagging";
|
||||
|
||||
export default function FeedbackTimeline({ submissions }) {
|
||||
const router = useRouter();
|
||||
@@ -103,6 +104,8 @@ export default function FeedbackTimeline({ submissions }) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Tagging submission={submission} />
|
||||
|
||||
<div className=" bg-slate-50 p-4 sm:p-6">
|
||||
<div className="flex w-full justify-between gap-4">
|
||||
<div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-toastify";
|
||||
import Tagging from "../shared/Tagging";
|
||||
|
||||
export default function PMFTimeline({ submissions }) {
|
||||
const router = useRouter();
|
||||
@@ -103,7 +104,6 @@ export default function PMFTimeline({ submissions }) {
|
||||
Somewhat disappointed
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<div className="text-sm text-slate-400">
|
||||
<time dateTime={convertDateTimeString(submission.createdAt)}>
|
||||
{new Date().getTime() - new Date(submission.createdAt).getTime() >
|
||||
@@ -132,6 +132,8 @@ export default function PMFTimeline({ submissions }) {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Tagging submission={submission} />
|
||||
|
||||
<div className=" bg-slate-50 p-4 sm:p-6">
|
||||
<div className="flex w-full justify-between gap-4">
|
||||
<div>
|
||||
@@ -154,10 +156,12 @@ export default function PMFTimeline({ submissions }) {
|
||||
{parseUserAgent(submission.meta.userAgent)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-thin text-slate-500">Page</p>
|
||||
<p className="text-sm text-slate-500">{submission.data.pageUrl}</p>
|
||||
</div>
|
||||
{submission.data.pageUrl && (
|
||||
<div>
|
||||
<p className="text-sm font-thin text-slate-500">Page</p>
|
||||
<p className="text-sm text-slate-500">{submission.data.pageUrl}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex w-full justify-end">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { camelToTitle, filterUniqueById } from "@/lib/utils";
|
||||
import { RectangleGroupIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { BsPin, BsPinFill } from "react-icons/bs";
|
||||
|
||||
interface Filter {
|
||||
@@ -39,6 +39,19 @@ export default function FilterNavigation({
|
||||
|
||||
const { form, isLoadingForm, isErrorForm } = useForm(formId?.toString(), organisationId?.toString());
|
||||
|
||||
// get all the tags from the submissions
|
||||
const tags = useMemo(() => {
|
||||
const tags = [];
|
||||
for (const submission of submissions) {
|
||||
for (const tag of submission.tags) {
|
||||
if (!tags.includes(tag)) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}, [submissions]);
|
||||
|
||||
// filter submissions based on selected filters
|
||||
useEffect(() => {
|
||||
if (form) {
|
||||
@@ -55,8 +68,32 @@ export default function FilterNavigation({
|
||||
setNumTotalSubmissions([...submissions].filter((s) => !s.archived).length);
|
||||
}
|
||||
|
||||
continue;
|
||||
} else if (filter.type === "tags") {
|
||||
const isAllActive = filter.options.find((option) => option.value === "all")?.active;
|
||||
// no filter is all is selected, if not keep on filtering
|
||||
if (!isAllActive) {
|
||||
// filter for all other types
|
||||
let listOfValidFilteredSubmissions = [];
|
||||
|
||||
for (const option of filter.options) {
|
||||
if (option.active || option.pinned) {
|
||||
listOfValidFilteredSubmissions.push(
|
||||
newFilteredSubmissions.filter((submission) => {
|
||||
if (submission.tags) {
|
||||
return submission.tags.includes(option.value);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
// add pinned submissions to the top
|
||||
const flattenedListOfValidFilteredSubmissions = listOfValidFilteredSubmissions.flat();
|
||||
newFilteredSubmissions = filterUniqueById(flattenedListOfValidFilteredSubmissions);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const isAllActive = filter.options.find((option) => option.value === "all")?.active;
|
||||
// no filter is all is selected, if not keep on filtering
|
||||
if (!isAllActive) {
|
||||
@@ -178,9 +215,18 @@ export default function FilterNavigation({
|
||||
{ value: "archived", label: "Archived", active: false },
|
||||
],
|
||||
});
|
||||
// add tag selection to filters
|
||||
filters.push({
|
||||
name: "tags",
|
||||
label: "Tags",
|
||||
type: "tags",
|
||||
options: [{ value: "all", label: "All", active: true, pinned: false }].concat([
|
||||
...tags.map((tag) => ({ value: tag, label: tag, active: false, pinned: false })),
|
||||
]),
|
||||
});
|
||||
setFilters(filters);
|
||||
}
|
||||
}, [form, limitFields]);
|
||||
}, [form, limitFields, tags]);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <LoadingSpinner />;
|
||||
@@ -198,24 +244,24 @@ export default function FilterNavigation({
|
||||
<h4 className="text-slate-600">{camelToTitle(filter.name)}</h4>
|
||||
</div>
|
||||
{filter.options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
chooseOptionFilter(filter.name, option.value, option.active);
|
||||
}}
|
||||
className={clsx(
|
||||
option.active || option.pinned
|
||||
? "bg-slate-200 text-slate-900"
|
||||
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900",
|
||||
"group my-1 flex w-full items-center rounded-md px-3 py-1.5 text-sm font-medium"
|
||||
)}
|
||||
aria-current={option.active ? "page" : undefined}>
|
||||
<div className={clsx("-ml-1 mr-3 h-2 w-2 flex-shrink-0 rounded-full")} />
|
||||
<span className="truncate">{option.label}</span>
|
||||
<div key={option.value} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
chooseOptionFilter(filter.name, option.value, option.active);
|
||||
}}
|
||||
className={clsx(
|
||||
option.active || option.pinned
|
||||
? "bg-slate-200 text-slate-900"
|
||||
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900",
|
||||
"group my-1 flex w-full items-center rounded-md px-3 py-1.5 text-sm font-medium transition-all duration-200"
|
||||
)}
|
||||
aria-current={option.active ? "page" : undefined}>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</button>
|
||||
{!["all", "inbox", "archived"].includes(option.value) && (option.active || option.pinned) && (
|
||||
<button
|
||||
className="ml-auto"
|
||||
className="absolute right-2 top-1 rounded px-2 py-1 transition-all duration-100 hover:bg-slate-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
pinOptionFilter(filter.name, option.value, !option.pinned);
|
||||
@@ -227,7 +273,7 @@ export default function FilterNavigation({
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RectangleStackIcon } from "@heroicons/react/24/solid";
|
||||
export function SubmissionCounter({ numFilteredSubmissions, numTotalSubmissions }) {
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-slate-200 px-4 py-2">
|
||||
<div className="flex items-center text-base font-semibold text-slate-500">
|
||||
<div className="flex flex-wrap items-center text-base font-semibold text-slate-500">
|
||||
<RectangleStackIcon className="mr-2 h-5 w-5 text-slate-300" /> {numFilteredSubmissions} responses
|
||||
{numFilteredSubmissions !== numTotalSubmissions && (
|
||||
<div className="ml-2 text-sm font-medium text-slate-400">(out of {numTotalSubmissions})</div>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { persistSubmission, useSubmissions } from "@/lib/submissions";
|
||||
import { onlyUnique } from "@/lib/utils";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
// add interface props for tags
|
||||
interface TaggingProps {
|
||||
submission: any;
|
||||
}
|
||||
|
||||
export default function Tagging({ submission }: TaggingProps) {
|
||||
const router = useRouter();
|
||||
const [isEditingTag, setIsEditingTag] = useState("");
|
||||
const [currentTag, setCurrentTag] = useState("");
|
||||
|
||||
const { submissions, mutateSubmissions } = useSubmissions(
|
||||
router.query.organisationId?.toString(),
|
||||
router.query.formId?.toString()
|
||||
);
|
||||
|
||||
// update submissions
|
||||
const updateSubmissionsLocally = (updatedSubmission) => {
|
||||
const updatedSubmissions = JSON.parse(JSON.stringify(submissions));
|
||||
const submissionIdx = updatedSubmissions.findIndex((s) => s.id === updatedSubmission.id);
|
||||
updatedSubmissions[submissionIdx] = updatedSubmission;
|
||||
mutateSubmissions(updatedSubmissions, false);
|
||||
};
|
||||
|
||||
// add tag to submission
|
||||
const addTag = async (submission, tag) => {
|
||||
if (!tag) return;
|
||||
if (submission.tags.includes(tag)) {
|
||||
setIsEditingTag("");
|
||||
setCurrentTag("");
|
||||
return;
|
||||
}
|
||||
const updatedSubmission = JSON.parse(JSON.stringify(submission));
|
||||
updatedSubmission.tags = [...updatedSubmission.tags, tag];
|
||||
updateSubmissionsLocally(updatedSubmission);
|
||||
await persistSubmission(updatedSubmission, router.query.organisationId?.toString());
|
||||
setIsEditingTag("");
|
||||
setCurrentTag("");
|
||||
};
|
||||
|
||||
//remove tag from submission
|
||||
const removeTag = async (submission, tag) => {
|
||||
const updatedSubmission = JSON.parse(JSON.stringify(submission));
|
||||
updatedSubmission.tags = updatedSubmission.tags.filter((t) => t !== tag);
|
||||
updateSubmissionsLocally(updatedSubmission);
|
||||
await persistSubmission(updatedSubmission, router.query.organisationId?.toString());
|
||||
};
|
||||
|
||||
// get all the tags from the submissions
|
||||
const existingTags = useMemo(() => {
|
||||
const tags = [];
|
||||
for (const submission of submissions) {
|
||||
for (const tag of submission.tags) {
|
||||
if (!tags.includes(tag)) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}, [submissions]);
|
||||
|
||||
const notYetAssignedTags = useMemo(
|
||||
() => existingTags.filter((tag) => !submission.tags.includes(tag)),
|
||||
[existingTags, submission.tags]
|
||||
);
|
||||
|
||||
const filteredTags = useMemo(
|
||||
() =>
|
||||
currentTag === ""
|
||||
? notYetAssignedTags
|
||||
: notYetAssignedTags.filter((notYetAssignedTag) => {
|
||||
return notYetAssignedTag.toLowerCase().includes(currentTag.toLowerCase());
|
||||
}),
|
||||
[currentTag, notYetAssignedTags]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border-t border-slate-100 px-6 py-4">
|
||||
<ul className="flex flex-wrap space-x-2">
|
||||
{submission.tags.map((tag: string) => (
|
||||
<li
|
||||
key={tag}
|
||||
className="my-1 inline-flex items-center rounded-full bg-slate-600 py-0.5 pl-2.5 pr-1 text-sm font-medium text-slate-100">
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTag(submission, tag)}
|
||||
className="ml-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full text-slate-400 hover:bg-slate-500 hover:text-slate-200 focus:bg-slate-500 focus:text-white focus:outline-none">
|
||||
<span className="sr-only">Remove large option</span>
|
||||
<svg className="h-2 w-2" stroke="currentColor" fill="none" viewBox="0 0 8 8">
|
||||
<path strokeLinecap="round" strokeWidth="1.5" d="M1 1l6 6m0-6L1 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li>
|
||||
{isEditingTag && submission.id === isEditingTag ? (
|
||||
<Combobox
|
||||
as="div"
|
||||
value={filteredTags.length > 0 && currentTag ? filteredTags[0] : currentTag}
|
||||
onChange={(value) => {
|
||||
addTag(submission, value);
|
||||
}}>
|
||||
<div className="relative">
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className="h-8 w-40 rounded-full border border-slate-300 bg-slate-50 text-sm text-slate-400 outline-none hover:text-slate-600 focus:border-2 focus:border-slate-300 focus:text-slate-600"
|
||||
autoFocus={true}
|
||||
value={currentTag}
|
||||
onChange={(event) => {
|
||||
setCurrentTag(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsEditingTag("");
|
||||
setCurrentTag("");
|
||||
}}
|
||||
className="absolute top-1/2 right-1.5 inline-flex h-4 w-4 -translate-y-1/2 items-center justify-center rounded-full text-slate-400 hover:bg-slate-200 focus:bg-slate-500 focus:text-white focus:outline-none">
|
||||
<span className="sr-only">Remove large option</span>
|
||||
<svg className="h-2 w-2" stroke="currentColor" fill="none" viewBox="0 0 8 8">
|
||||
<path strokeLinecap="round" strokeWidth="1.5" d="M1 1l6 6m0-6L1 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Combobox.Options className="absolute z-10 mt-1 max-h-28 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{[...filteredTags, currentTag].filter(onlyUnique).map((tag) => (
|
||||
<Combobox.Option
|
||||
key={tag}
|
||||
value={tag}
|
||||
className={({ active }) =>
|
||||
clsx(
|
||||
"relative cursor-default select-none py-2 pl-3 pr-9",
|
||||
active ? "bg-slate-500 text-white" : "text-gray-900"
|
||||
)
|
||||
}>
|
||||
{({ selected }) => (
|
||||
<span className={clsx("block truncate", selected && "font-semibold")}>{tag}</span>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</Combobox>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-dashed border-slate-300 py-1 px-3 text-sm text-slate-400 hover:text-slate-600"
|
||||
onClick={() => {
|
||||
setIsEditingTag(submission.id);
|
||||
}}>
|
||||
add tag <PlusIcon className="inline h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,13 +3,12 @@
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export default function LayoutWrapperCustomForm({ children }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const pathname = router.pathname;
|
||||
const navigation = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
||||
@@ -182,6 +182,8 @@ const benefitingUsers = [
|
||||
"People who want to protect their finances and personal information from fraud and theft",
|
||||
];
|
||||
|
||||
const tags = ["helpful", "useful", "interesting", "funny", "inspiring", "motivating", "thought-provoking"];
|
||||
|
||||
export const getPmfSubmissions = () => {
|
||||
const submissions = [];
|
||||
for (let i = 0; i < 142; i++) {
|
||||
@@ -205,6 +207,7 @@ export const getPmfSubmissions = () => {
|
||||
userAgent:
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
|
||||
},
|
||||
tags: [getRandomItem(tags)],
|
||||
});
|
||||
}
|
||||
return submissions;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
import { formatDistance } from "date-fns";
|
||||
import intlFormat from "date-fns/intlFormat";
|
||||
import { formatDistance, formatDistanceStrict, formatDistanceToNow } from "date-fns";
|
||||
import platform from "platform";
|
||||
import { demoEndpoints } from "./demo";
|
||||
|
||||
@@ -8,7 +8,6 @@ export const fetcher = async (url) => {
|
||||
if (url in demoEndpoints) {
|
||||
const { file } = demoEndpoints[url];
|
||||
const { getData } = await import(`../demo-data/${file}`);
|
||||
console.log(url, getData);
|
||||
return getData(url);
|
||||
}
|
||||
const res = await fetch(url);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { ResetPasswordForm } from "@/components/auth/ResetPasswordForm";
|
||||
import LayoutAuth from "@/components/layout/LayoutAuth";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<LayoutAuth title="Reset password">
|
||||
<ResetPasswordForm token={searchParams.get("token")} />
|
||||
<ResetPasswordForm token={router.query.token} />
|
||||
</LayoutAuth>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
import LayoutAuth from "@/components/layout/LayoutAuth";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { SigninForm } from "@/components/auth/SigninForm";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function SignInPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<LayoutAuth title="Sign in">
|
||||
<SigninForm callbackUrl={searchParams.get("callbackUrl")} error={searchParams.get("error")} />
|
||||
<SigninForm callbackUrl={router.query.callbackUr} error={router.query.error} />
|
||||
{process.env.NEXT_PUBLIC_SIGNUP_DISABLED !== "1" && (
|
||||
<div>
|
||||
<Link
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import LayoutAuth from "@/components/layout/LayoutAuth";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { RequestVerificationEmail } from "@/components/auth/RequestVerificationEmail";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function VerficationPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<LayoutAuth title="Email verification required">
|
||||
{searchParams.get("email") ? (
|
||||
{router.query.email ? (
|
||||
<>
|
||||
<h1 className="leading-2 mb-4 text-center font-bold">Please verify your email address</h1>
|
||||
<p className="text-center">
|
||||
We have sent you an email to the address{" "}
|
||||
<span className="italic">{searchParams.get("email")}</span>. Please click the link in the email to
|
||||
activate your account.
|
||||
We have sent you an email to the address <span className="italic">{router.query.email}</span>.
|
||||
Please click the link in the email to activate your account.
|
||||
</p>
|
||||
<hr className="my-4" />
|
||||
<p className="text-center text-xs">
|
||||
@@ -23,7 +22,7 @@ export default function VerficationPage() {
|
||||
Click the button below to request a new email.
|
||||
</p>
|
||||
<div className="mt-5">
|
||||
<RequestVerificationEmail email={searchParams.get("email")} />
|
||||
<RequestVerificationEmail email={router.query.email.toString()} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { SignIn } from "@/components/auth/SignIn";
|
||||
import LayoutAuth from "@/components/layout/LayoutAuth";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function Verify() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<LayoutAuth title="Verify">
|
||||
<p className="text-center">{!searchParams.get("token") ? "No Token provided" : "Verifying..."}</p>
|
||||
<SignIn token={searchParams.get("token")} />
|
||||
<p className="text-center">{!router.query.token ? "No Token provided" : "Verifying..."}</p>
|
||||
<SignIn token={router.query.token} />
|
||||
</LayoutAuth>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,3 +27,9 @@
|
||||
background-color: #cbd5e1;
|
||||
border: 3px solid #cbd5e1;
|
||||
}
|
||||
|
||||
[type="text"]:focus {
|
||||
--tw-ring-color: none;
|
||||
--tw-ring-offset-color: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Submission" ADD COLUMN "tags" TEXT[];
|
||||
@@ -80,6 +80,7 @@ model Submission {
|
||||
customerOrganisationId String?
|
||||
data Json @default("{}")
|
||||
meta Json @default("{}")
|
||||
tags String[]
|
||||
}
|
||||
|
||||
enum Plan {
|
||||
|
||||
Generated
+519
-229
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user