Compare commits

..

3 Commits

Author SHA1 Message Date
ShubhamPalriwala
dc72b712d6 fix: upgrade node to 20 2024-01-15 19:12:33 +05:30
ShubhamPalriwala
0bdae282e5 init: buildjet in actions to compare 2024-01-15 19:07:56 +05:30
ShubhamPalriwala
28bc87b8e5 init: buildjet 2024-01-15 19:02:42 +05:30
33 changed files with 592 additions and 690 deletions

View File

@@ -4,17 +4,17 @@ on:
jobs:
build:
name: Build Formbricks-web
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 30
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup Node.js 18.x
- name: Setup Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- name: Install pnpm
uses: pnpm/action-setup@v2

View File

@@ -4,7 +4,7 @@ on:
jobs:
build:
name: Run E2E Tests
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 60
steps:

View File

@@ -1,6 +1,9 @@
name: PR Update
on:
push:
branches:
- shubham/integrate-buildjet
pull_request_target:
branches:
- main
@@ -35,7 +38,7 @@ jobs:
required:
needs: [lint, test, build, e2e-test]
if: always()
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: fail if conditional jobs failed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')

View File

@@ -4,7 +4,7 @@ on:
jobs:
build:
name: Tests
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
timeout-minutes: 15
env:
@@ -15,10 +15,10 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup Node.js 18.x
- name: Setup Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- name: Install pnpm
uses: pnpm/action-setup@v2

View File

@@ -1,70 +0,0 @@
import Image from "next/image";
import ShareLink from "./share-link.webp";
import ViewResponse from "./view-response.webp";
export const metadata = {
title: "Source Tracking",
description: "Track the source of your users in an easy & compliant way!",
};
#### Link Surveys
# Source Tracking
Understand the source a survey respondent comes from when responding to your survey - all while keeping data privacy standards high!
Check out this video to learn more about source tracking in link surveys:
{/* Replace link below with our new link on Source Tracking */}
<iframe width="700" height="450" src="https://www.youtube.com/embed/CytWhuyEMVI?si=t-SFB2A1l1RZDdAC" title="YouTube video player: Formbricks" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe>
## Purpose
Source tracking for link surveys is essential when you:
- Want to analyze the origin of your survey respondents.
- Aim to ensure compliance with tracking and data collection regulations.
## Code Example
<Col>
<CodeGroup title="Example Source as Google">
```sh
https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?source=Google
```
</CodeGroup>
</Col>
## How it Works
To track the source of users in your link surveys effectively, follow these steps:
1. **Generate Survey URL**: Create a Link Survey and get the sharable link. Append `?source=YourSouce` to the link to reference it with your campaigns and sources.
<Col>
<CodeGroup title="Example Source as Google">
```sh
https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?source=Google
```
</CodeGroup>
</Col>
2. **Collect Data**: When users access the survey through these links, the URL parameters will capture the source information from which they were shared.
3. **View Responses**: Use the collected source data to analyze where your survey respondents are coming from. You can hover over the user icon in the responses tab to see the source of the user.
<Image
src={ViewResponse}
alt="View Source in Response"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
4. **Analyse Data**: Download all the responses as a CSV/Excel and get access to the source information. This can provide valuable insights into your audience.
Source tracking allows you to make informed decisions based on the origin of your survey participants, helping you tailor your surveys and marketing strategies accordingly.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -218,7 +218,6 @@ export const navigation: Array<NavGroup> = [
{ title: "Data Prefilling", href: "/docs/link-surveys/data-prefilling" },
{ title: "Identify Users", href: "/docs/link-surveys/user-identification" },
{ title: "Single Use Links", href: "/docs/link-surveys/single-use-links" },
{ title: "Source Tracking", href: "/docs/link-surveys/source-tracking" },
],
},
{

View File

@@ -84,7 +84,6 @@ export default function ActionClassesTable({
<AddNoCodeActionModal
environmentId={environmentId}
open={isAddActionModalOpen}
actionClasses={actionClasses}
setOpen={setAddActionModalOpen}
isViewer={isViewer}
/>

View File

@@ -24,8 +24,7 @@ interface AddNoCodeActionModalProps {
environmentId: string;
open: boolean;
setOpen: (v: boolean) => void;
actionClasses: TActionClass[];
setActionClasses?;
setActionClassArray?;
isViewer: boolean;
}
@@ -46,8 +45,7 @@ export default function AddNoCodeActionModal({
environmentId,
open,
setOpen,
actionClasses,
setActionClasses,
setActionClassArray,
isViewer,
}: AddNoCodeActionModalProps) {
const { register, control, handleSubmit, watch, reset } = useForm();
@@ -58,7 +56,6 @@ export default function AddNoCodeActionModal({
const [testUrl, setTestUrl] = useState("");
const [isMatch, setIsMatch] = useState("");
const [type, setType] = useState("noCode");
const actionClassNames = actionClasses.map((actionClass) => actionClass.name);
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
const { pageUrl, innerHtml, cssSelector } = noCodeConfig;
@@ -95,12 +92,7 @@ export default function AddNoCodeActionModal({
throw new Error("You are not authorised to perform this action.");
}
setIsCreatingAction(true);
if (!data.name || data.name?.trim() === "") {
throw new Error("Please give your action a name");
}
if (data.name && actionClassNames.includes(data.name)) {
throw new Error(`Action with name ${data.name} already exist`);
}
if (data.name === "") throw new Error("Please give your action a name");
if (type === "noCode") {
if (!isPageUrl && !isCssSelector && !isInnerHtml)
throw new Error("Please select at least one selector");
@@ -127,8 +119,11 @@ export default function AddNoCodeActionModal({
}
const newActionClass: TActionClass = await createActionClassAction(updatedAction);
if (setActionClasses) {
setActionClasses((prevActionClasses: TActionClass[]) => [...prevActionClasses, newActionClass]);
if (setActionClassArray) {
setActionClassArray((prevActionClassArray: TActionClass[]) => [
...prevActionClassArray,
newActionClass,
]);
}
reset();
resetAllStates(false);
@@ -183,7 +178,7 @@ export default function AddNoCodeActionModal({
<div className="grid w-full grid-cols-2 gap-x-4">
<div className="col-span-1">
<Label>What did your user do?</Label>
<Input placeholder="E.g. Clicked Download" {...register("name")} />
<Input placeholder="E.g. Clicked Download" {...register("name", { required: true })} />
</div>
<div className="col-span-1">
<Label>Description</Label>

View File

@@ -5,7 +5,6 @@ import { DownloadIcon, FileIcon } from "lucide-react";
import Link from "next/link";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
import { timeSince } from "@formbricks/lib/time";
import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
import { TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
@@ -82,31 +81,29 @@ export default function FileUploadSummary({ questionSummary, environmentId }: Fi
{Array.isArray(response.value) &&
(response.value.length > 0 ? (
response.value.map((fileUrl, index) => {
const fileName = getOriginalFileNameFromUrl(fileUrl);
return (
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
<a
href={fileUrl as string}
key={index}
download={fileName}
target="_blank"
rel="noopener noreferrer">
<div className="absolute right-0 top-0 m-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
<DownloadIcon className="h-6 text-slate-500" />
</div>
response.value.map((fileUrl, index) => (
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
<a
href={fileUrl as string}
key={index}
download
target="_blank"
rel="noopener noreferrer">
<div className="absolute right-0 top-0 m-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
<DownloadIcon className="h-6 text-slate-500" />
</div>
</a>
<div className="flex flex-col items-center justify-center p-2">
<FileIcon className="h-6 text-slate-500" />
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{fileName}</p>
</div>
</a>
<div className="flex flex-col items-center justify-center p-2">
<FileIcon className="h-6 text-slate-500" />
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
{fileUrl.split("/").pop()}
</p>
</div>
);
})
</div>
))
) : (
<div className="flex w-full flex-col items-center justify-center p-2">
<p className="mt-2 text-sm font-semibold text-slate-500 dark:text-slate-400">skipped</p>

View File

@@ -47,7 +47,7 @@ export default function SettingsView({
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
environmentId={environment.id}
propActionClasses={actionClasses}
actionClasses={actionClasses}
membershipRole={membershipRole}
/>

View File

@@ -27,7 +27,7 @@ interface WhenToSendCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey) => void;
environmentId: string;
propActionClasses: TActionClass[];
actionClasses: TActionClass[];
membershipRole?: TMembershipRole;
}
@@ -35,13 +35,13 @@ export default function WhenToSendCard({
environmentId,
localSurvey,
setLocalSurvey,
propActionClasses,
actionClasses,
membershipRole,
}: WhenToSendCardProps) {
const [open, setOpen] = useState(localSurvey.type === "web" ? true : false);
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const [actionClasses, setActionClasses] = useState<TActionClass[]>(propActionClasses);
const [actionClassArray, setActionClassArray] = useState<TActionClass[]>(actionClasses);
const { isViewer } = getAccessFlags(membershipRole);
const autoClose = localSurvey.autoClose !== null;
@@ -55,7 +55,7 @@ export default function WhenToSendCard({
const setTriggerEvent = useCallback(
(idx: number, actionClassName: string) => {
const updatedSurvey = { ...localSurvey };
const newActionClass = actionClasses!.find((actionClass) => {
const newActionClass = actionClassArray!.find((actionClass) => {
return actionClass.name === actionClassName;
});
if (!newActionClass) {
@@ -64,7 +64,7 @@ export default function WhenToSendCard({
updatedSurvey.triggers[idx] = newActionClass.name;
setLocalSurvey(updatedSurvey);
},
[actionClasses, localSurvey, setLocalSurvey]
[actionClassArray, localSurvey, setLocalSurvey]
);
const removeTriggerEvent = (idx: number) => {
@@ -101,7 +101,7 @@ export default function WhenToSendCard({
useEffect(() => {
if (isAddEventModalOpen) return;
if (activeIndex !== null) {
const newActionClass = actionClasses[actionClasses.length - 1].name;
const newActionClass = actionClassArray[actionClassArray.length - 1].name;
const currentActionClass = localSurvey.triggers[activeIndex];
if (newActionClass !== currentActionClass) {
@@ -110,7 +110,7 @@ export default function WhenToSendCard({
setActiveIndex(null);
}
}, [actionClasses, activeIndex, setTriggerEvent, isAddEventModalOpen, localSurvey.triggers]);
}, [actionClassArray, activeIndex, setTriggerEvent, isAddEventModalOpen, localSurvey.triggers]);
useEffect(() => {
if (localSurvey.type === "link") {
@@ -200,7 +200,7 @@ export default function WhenToSendCard({
Add Action
</button>
<SelectSeparator />
{actionClasses.map((actionClass) => (
{actionClassArray.map((actionClass) => (
<SelectItem
value={actionClass.name}
key={actionClass.name}
@@ -279,8 +279,7 @@ export default function WhenToSendCard({
environmentId={environmentId}
open={isAddEventModalOpen}
setOpen={setAddEventModalOpen}
actionClasses={actionClasses}
setActionClasses={setActionClasses}
setActionClassArray={setActionClassArray}
isViewer={isViewer}
/>
</>

View File

@@ -1,7 +1,6 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { sendToPipeline } from "@/app/lib/pipelines";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import { UAParser } from "ua-parser-js";
@@ -23,7 +22,6 @@ export async function POST(request: Request): Promise<NextResponse> {
responseInput.personId = null;
}
const agent = UAParser(request.headers.get("user-agent"));
const country = headers().get("CF-IPCountry") || headers().get("X-Vercel-IP-Country") || undefined;
const inputValidation = ZResponseLegacyInput.safeParse(responseInput);
if (!inputValidation.success) {
@@ -62,7 +60,6 @@ export async function POST(request: Request): Promise<NextResponse> {
device: agent?.device.type,
os: agent?.os.name,
},
country: country,
};
// check if personId is anonymous

View File

@@ -1,7 +1,6 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { sendToPipeline } from "@/app/lib/pipelines";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import { UAParser } from "ua-parser-js";
@@ -46,7 +45,6 @@ export async function POST(request: Request, context: Context): Promise<NextResp
}
const agent = UAParser(request.headers.get("user-agent"));
const country = headers().get("CF-IPCountry") || headers().get("X-Vercel-IP-Country") || undefined;
const inputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
if (!inputValidation.success) {
@@ -85,7 +83,6 @@ export async function POST(request: Request, context: Context): Promise<NextResp
device: agent?.device.type,
os: agent?.os.name,
},
country: country,
};
response = await createResponse({

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/web",
"version": "1.4.2",
"version": "1.4.1",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",
@@ -26,23 +26,23 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@react-email/components": "^0.0.12",
"@sentry/nextjs": "^7.93.0",
"@sentry/nextjs": "^7.92.0",
"@vercel/og": "^0.6.2",
"@vercel/speed-insights": "^1.0.3",
"@vercel/speed-insights": "^1.0.2",
"bcryptjs": "^2.4.3",
"dotenv": "^16.3.1",
"encoding": "^0.1.13",
"framer-motion": "10.18.0",
"framer-motion": "10.17.12",
"googleapis": "^130.0.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"lru-cache": "^10.1.0",
"lucide-react": "^0.309.0",
"lucide-react": "^0.308.0",
"mime": "^4.0.1",
"next": "14.0.4",
"nodemailer": "^6.9.8",
"otplib": "^12.0.1",
"posthog-js": "^1.98.2",
"posthog-js": "^1.97.0",
"prismjs": "^1.29.0",
"qrcode": "^1.5.3",
"react": "18.2.0",
@@ -51,7 +51,7 @@
"react-email": "^1.10.0",
"react-hook-form": "^7.49.3",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.0.1",
"react-icons": "^4.12.0",
"ua-parser-js": "^1.0.37",
"webpack": "^5.89.0",
"xlsx": "^0.18.5"

View File

@@ -42,7 +42,7 @@ export class StorageAPI {
const json = await response.json();
const { data } = json;
const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
const { signedUrl, fileUrl, signingData, presignedFields } = data;
let requestHeaders: Record<string, string> = {};
@@ -51,7 +51,7 @@ export class StorageAPI {
requestHeaders = {
"X-File-Type": file.type,
"X-File-Name": encodeURIComponent(updatedFileName),
"X-File-Name": encodeURIComponent(file.name),
"X-Survey-ID": surveyId ?? "",
"X-Signature": signature,
"X-Timestamp": String(timestamp),

View File

@@ -25,7 +25,7 @@
"predev": "pnpm generate"
},
"dependencies": {
"@prisma/client": "^5.8.0",
"@prisma/client": "^5.7.1",
"@prisma/extension-accelerate": "^0.6.2",
"dotenv-cli": "^7.3.0"
},
@@ -33,7 +33,7 @@
"@formbricks/tsconfig": "workspace:*",
"@formbricks/types": "workspace:*",
"eslint-config-formbricks": "workspace:*",
"prisma": "^5.8.0",
"prisma": "^5.7.1",
"prisma-dbml-generator": "^0.10.0",
"prisma-json-types-generator": "^3.0.3",
"zod": "^3.22.4",

View File

@@ -18,6 +18,6 @@
},
"dependencies": {
"@formbricks/lib": "workspace:*",
"stripe": "^14.12.0"
"stripe": "^14.11.0"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@formbricks/js",
"license": "MIT",
"version": "1.4.2",
"version": "1.4.1",
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
"homepage": "https://formbricks.com",
"repository": {

View File

@@ -14,10 +14,10 @@
"test": "jest -ci --coverage --no-cache"
},
"dependencies": {
"@aws-sdk/s3-presigned-post": "3.490.0",
"@aws-sdk/client-s3": "3.490.0",
"@aws-sdk/s3-request-presigner": "3.490.0",
"@t3-oss/env-nextjs": "^0.7.3",
"@aws-sdk/s3-presigned-post": "3.485.0",
"@aws-sdk/client-s3": "3.485.0",
"@aws-sdk/s3-request-presigner": "3.485.0",
"@t3-oss/env-nextjs": "^0.7.1",
"mime": "4.0.1",
"@formbricks/api": "*",
"@formbricks/database": "*",
@@ -30,7 +30,7 @@
"nanoid": "^5.0.4",
"next-auth": "^4.24.5",
"nodemailer": "^6.9.8",
"posthog-node": "^3.5.0",
"posthog-node": "^3.4.0",
"server-only": "^0.0.1",
"tailwind-merge": "^2.2.0"
},

View File

@@ -7,7 +7,6 @@ import {
} from "@aws-sdk/client-s3";
import { PresignedPostOptions, createPresignedPost } from "@aws-sdk/s3-presigned-post";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { randomUUID } from "crypto";
import { access, mkdir, readFile, rmdir, unlink, writeFile } from "fs/promises";
import mime from "mime";
import { unstable_cache } from "next/cache";
@@ -62,7 +61,6 @@ type TGetSignedUrlResponse =
| { signedUrl: string; fileUrl: string; presignedFields: Object }
| {
signedUrl: string;
updatedFileName: string;
fileUrl: string;
signingData: {
signature: string;
@@ -173,21 +171,10 @@ export const getUploadSignedUrl = async (
accessType: TAccessType,
plan: "free" | "pro" = "free"
): Promise<TGetSignedUrlResponse> => {
// add a unique id to the file name
const fileExtension = fileName.split(".").pop();
const fileNameWithoutExtension = fileName.split(".").slice(0, -1).join(".");
if (!fileExtension) {
throw new Error("File extension not found");
}
const updatedFileName = `${fileNameWithoutExtension}--fid--${randomUUID()}.${fileExtension}`;
// handle the local storage case first
if (!IS_S3_CONFIGURED) {
try {
const { signature, timestamp, uuid } = generateLocalSignedUrl(updatedFileName, environmentId, fileType);
const { signature, timestamp, uuid } = generateLocalSignedUrl(fileName, environmentId, fileType);
return {
signedUrl:
@@ -199,8 +186,7 @@ export const getUploadSignedUrl = async (
timestamp,
uuid,
},
updatedFileName,
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${updatedFileName}`).href,
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${fileName}`).href,
};
} catch (err) {
throw err;
@@ -209,7 +195,7 @@ export const getUploadSignedUrl = async (
try {
const { presignedFields, signedUrl } = await getS3UploadSignedUrl(
updatedFileName,
fileName,
fileType,
accessType,
environmentId,
@@ -220,7 +206,7 @@ export const getUploadSignedUrl = async (
return {
signedUrl,
presignedFields,
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${updatedFileName}`).href,
fileUrl: new URL(`${WEBAPP_URL}/storage/${environmentId}/${accessType}/${fileName}`).href,
};
} catch (err) {
throw err;

View File

@@ -1,14 +0,0 @@
export const getOriginalFileNameFromUrl = (fileURL: string) => {
const fileNameFromURL = new URL(fileURL).pathname.split("/").pop();
const fileExt = fileNameFromURL?.split(".").pop();
const originalFileName = fileNameFromURL?.split("--fid--")[0];
const fileId = fileNameFromURL?.split("--fid--")[1];
if (!fileId) {
const fileName = originalFileName ? decodeURIComponent(originalFileName || "") : "";
return fileName;
}
const fileName = originalFileName ? decodeURIComponent(`${originalFileName}.${fileExt}` || "") : "";
return fileName;
};

View File

@@ -8,7 +8,7 @@
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"prettier": "^3.2.1",
"prettier": "^3.1.1",
"prettier-plugin-tailwindcss": "^0.5.11"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@formbricks/surveys",
"license": "MIT",
"version": "1.4.2",
"version": "1.4.1",
"description": "Formbricks-surveys is a helper library to embed surveys into your application",
"homepage": "https://formbricks.com",
"repository": {

View File

@@ -2,7 +2,6 @@ import { useMemo } from "preact/hooks";
import { JSXInternal } from "preact/src/jsx";
import { useState } from "react";
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
import { TAllowedFileExtension } from "@formbricks/types/common";
import { TUploadFileConfig } from "@formbricks/types/storage";
@@ -205,47 +204,45 @@ export default function FileInput({
<div className="items-left relative mt-3 flex w-full cursor-pointer flex-col justify-center rounded-lg border-2 border-dashed border-slate-300 bg-slate-50 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-700 dark:hover:border-slate-500 dark:hover:bg-slate-800">
<div>
{fileUrls &&
fileUrls?.map((file, index) => {
const fileName = getOriginalFileNameFromUrl(file);
return (
<div key={index} className="relative m-2 rounded-md bg-slate-200">
<div className="absolute right-0 top-0 m-2">
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-slate-100 hover:bg-slate-50">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 26 26"
strokeWidth={1}
stroke="currentColor"
className="h-5 text-slate-700 hover:text-slate-900"
onClick={(e) => handleDeleteFile(index, e)}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10 10m0-10L9 19" />
</svg>
</div>
</div>
<div className="flex flex-col items-center justify-center p-2">
fileUrls?.map((file, index) => (
<div key={index} className="relative m-2 rounded-md bg-slate-200">
<div className="absolute right-0 top-0 m-2">
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-slate-100 hover:bg-slate-50">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
viewBox="0 0 26 26"
strokeWidth={1}
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-file"
className="h-6 text-slate-500">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
<polyline points="14 2 14 8 20 8" />
className="h-5 text-slate-700 hover:text-slate-900"
onClick={(e) => handleDeleteFile(index, e)}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10 10m0-10L9 19" />
</svg>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">{fileName}</p>
</div>
</div>
);
})}
<div className="flex flex-col items-center justify-center p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-file"
className="h-6 text-slate-500">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
<polyline points="14 2 14 8 20 8" />
</svg>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">
{decodeURIComponent(file).split("/").pop()}
</p>
</div>
</div>
))}
</div>
<div>

View File

@@ -7,7 +7,7 @@
"clean": "rimraf node_modules dist turbo"
},
"devDependencies": {
"@types/node": "20.11.0",
"@types/node": "20.10.7",
"@types/react": "18.2.47",
"@types/react-dom": "18.2.18",
"typescript": "^5.3.3"

View File

@@ -45,7 +45,6 @@ export const ZResponseMeta = z.object({
device: z.string().optional(),
})
.optional(),
country: z.string().optional(),
});
export type TResponseMeta = z.infer<typeof ZResponseMeta>;
@@ -87,7 +86,6 @@ export const ZResponseInput = z.object({
os: z.string().optional(),
})
.optional(),
country: z.string().optional(),
})
.optional(),
});

View File

@@ -46,7 +46,7 @@ const uploadFile = async (
const json = await response.json();
const { data } = json;
const { signedUrl, fileUrl, signingData, presignedFields, updatedFileName } = data;
const { signedUrl, fileUrl, signingData, presignedFields } = data;
let requestHeaders: Record<string, string> = {};
@@ -55,7 +55,7 @@ const uploadFile = async (
requestHeaders = {
"X-File-Type": file.type,
"X-File-Name": encodeURIComponent(updatedFileName),
"X-File-Name": encodeURIComponent(file.name),
"X-Environment-ID": environmentId ?? "",
"X-Signature": signature,
"X-Timestamp": String(timestamp),

View File

@@ -2,49 +2,11 @@
import { FileIcon } from "lucide-react";
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
interface FileUploadResponseProps {
selected: string | number | string[];
}
export const FileUploadResponse = ({ selected }: FileUploadResponseProps) => {
const SingleFileResponse = () => {
const selectedFile = selected as string;
const fileName = getOriginalFileNameFromUrl(selectedFile);
return (
<div className="relative m-2 rounded-lg bg-slate-300">
<a href={selected as string} download={fileName}>
<div className="absolute right-0 top-0 m-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100 bg-opacity-50 hover:bg-slate-200/50">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="h-6 w-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
</div>
</div>
</a>
<div className="flex flex-col items-center justify-center p-2">
<FileIcon className="h-6 text-slate-500" />
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
{selected && typeof selected === "string" && decodeURIComponent(selected).split("/").pop()}
</p>
</div>
</div>
);
};
return (
<>
{selected === "selected" ? (
@@ -52,40 +14,65 @@ export const FileUploadResponse = ({ selected }: FileUploadResponseProps) => {
) : (
<div className="col-span-2 grid md:grid-cols-2 lg:grid-cols-4">
{Array.isArray(selected) ? (
selected.map((fileUrl, index) => {
const fileName = getOriginalFileNameFromUrl(fileUrl);
return (
<div className="relative m-2 ml-0 rounded-lg bg-slate-200">
<a href={fileUrl as string} key={index} download={fileName}>
<div className="absolute right-0 top-0 m-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="h-6 w-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
</div>
selected.map((fileUrl, index) => (
<div className="relative m-2 ml-0 rounded-lg bg-slate-200">
<a href={fileUrl as string} key={index} download>
<div className="absolute right-0 top-0 m-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="h-6 w-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
</div>
</a>
</div>
</a>
<div className="flex flex-col items-center justify-center p-2">
<FileIcon className="h-6 text-slate-500" />
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{fileName}</p>
<div className="flex flex-col items-center justify-center p-2">
<FileIcon className="h-6 text-slate-500" />
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
{decodeURIComponent(fileUrl).split("/").pop()}
</p>
</div>
</div>
))
) : (
<div className="relative m-2 rounded-lg bg-slate-300">
<a href={selected as string} download>
<div className="absolute right-0 top-0 m-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-100 bg-opacity-50 hover:bg-slate-200/50">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="h-6 w-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
</div>
</div>
);
})
) : (
<SingleFileResponse />
</a>
<div className="flex flex-col items-center justify-center p-2">
<FileIcon className="h-6 text-slate-500" />
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
{selected && typeof selected === "string" && decodeURIComponent(selected).split("/").pop()}
</p>
</div>
</div>
)}
</div>
)}

View File

@@ -209,7 +209,6 @@ export default function SingleResponseCard({
</p>
)}
{response.meta?.source && <p>Source: {response.meta.source}</p>}
{response.meta?.country && <p>Country: {response.meta.country}</p>}
</div>
)}
</>

View File

@@ -41,7 +41,7 @@
"clsx": "^2.1.0",
"cmdk": "^0.2.0",
"lexical": "^0.12.6",
"lucide-react": "^0.309.0",
"lucide-react": "^0.308.0",
"react-colorful": "^5.6.1",
"react-confetti": "^6.1.0",
"react-day-picker": "^8.10.0",

821
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff