Compare commits

..

5 Commits

Author SHA1 Message Date
Matti Nannt
0b9a884364 fix: smtp missing logs (#4917) 2025-03-11 11:13:22 +00:00
Dhruwang Jariwala
da4211f0b0 fix: Actions for contributors (#4905) 2025-03-10 16:04:17 +00:00
Dhruwang Jariwala
b21827cb32 fix: file input should not allow duplicate files (#4900) 2025-03-10 12:04:03 +00:00
Dhruwang Jariwala
4424a8a21d feat: pt-PT translations (#4874) 2025-03-10 09:22:26 +00:00
Dhruwang Jariwala
eb030f9ed6 fix: Address/Contact question accessibility (#4884) 2025-03-10 09:21:44 +00:00
24 changed files with 352 additions and 94 deletions

View File

@@ -5,7 +5,7 @@ permissions:
on: on:
workflow_dispatch: workflow_dispatch:
pull_request: pull_request_target:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
jobs: jobs:
@@ -14,6 +14,13 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4

View File

@@ -3,7 +3,7 @@ permissions:
contents: read contents: read
on: on:
pull_request: pull_request_target:
types: [closed] types: [closed]
branches: branches:
- main - main
@@ -23,18 +23,16 @@ jobs:
- name: Get source branch name - name: Get source branch name
id: branch-name id: branch-name
run: | run: |
# For PR merges, use the head ref from the pull request event RAW_BRANCH="${{ github.head_ref }}"
SOURCE_BRANCH="${{ github.head_ref }}" SOURCE_BRANCH=$(echo "$RAW_BRANCH" | sed 's/[^a-zA-Z0-9._\/-]//g')
# Only remove username prefix if needed
if [[ "$SOURCE_BRANCH" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]+/ ]]; then
PREFIX=${SOURCE_BRANCH%%/*}
if [[ ! "$PREFIX" =~ ^(feature|fix|bugfix|hotfix|release|chore|docs|test|refactor|style|perf|build|ci|revert)$ ]]; then
SOURCE_BRANCH=${SOURCE_BRANCH#*/}
fi
fi
echo "SOURCE_BRANCH=$SOURCE_BRANCH" >> $GITHUB_ENV # Safely add to environment variables using GitHub's recommended method
# This prevents environment variable injection attacks
echo "SOURCE_BRANCH<<EOF" >> $GITHUB_ENV
echo "$SOURCE_BRANCH" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "Detected source branch: $SOURCE_BRANCH" echo "Detected source branch: $SOURCE_BRANCH"
- name: Setup Node.js - name: Setup Node.js
@@ -53,6 +51,7 @@ jobs:
--filter-tag "draft:${SOURCE_BRANCH}" \ --filter-tag "draft:${SOURCE_BRANCH}" \
--tag production \ --tag production \
--untag "draft:${SOURCE_BRANCH}" --untag "draft:${SOURCE_BRANCH}"
--verbose
- name: Tag unused production keys as Deprecated - name: Tag unused production keys as Deprecated
run: | run: |
@@ -60,6 +59,7 @@ jobs:
--api-key ${{ secrets.TOLGEE_API_KEY }} \ --api-key ${{ secrets.TOLGEE_API_KEY }} \
--filter-not-extracted --filter-tag production \ --filter-not-extracted --filter-tag production \
--tag deprecated --untag production --tag deprecated --untag production
--verbose
- name: Tag unused draft:current-branch keys as Deprecated - name: Tag unused draft:current-branch keys as Deprecated
run: | run: |
@@ -67,6 +67,7 @@ jobs:
--api-key ${{ secrets.TOLGEE_API_KEY }} \ --api-key ${{ secrets.TOLGEE_API_KEY }} \
--filter-not-extracted --filter-tag "draft:${SOURCE_BRANCH}" \ --filter-not-extracted --filter-tag "draft:${SOURCE_BRANCH}" \
--tag deprecated --untag "draft:${SOURCE_BRANCH}" --tag deprecated --untag "draft:${SOURCE_BRANCH}"
--verbose
- name: Sync with backup - name: Sync with backup
run: | run: |

1
.gitignore vendored
View File

@@ -54,3 +54,4 @@ packages/lib/uploads
apps/web/public/js apps/web/public/js
packages/database/migrations packages/database/migrations
branch.json branch.json
.vercel

View File

@@ -27,6 +27,10 @@
{ {
"language": "zh-Hant-TW", "language": "zh-Hant-TW",
"path": "./packages/lib/messages/zh-Hant-TW.json" "path": "./packages/lib/messages/zh-Hant-TW.json"
},
{
"language": "pt-PT",
"path": "./packages/lib/messages/pt-PT.json"
} }
], ],
"forceMode": "OVERRIDE" "forceMode": "OVERRIDE"

View File

@@ -75,6 +75,7 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
return true; return true;
} catch (error) { } catch (error) {
console.error("Error in sendEmail:", error);
throw new InvalidInputError("Incorrect SMTP credentials"); throw new InvalidInputError("Incorrect SMTP credentials");
} }
}; };

View File

@@ -81,7 +81,7 @@ export const QuestionToggleTable = ({
</th> </th>
<th className="w-1/6 text-sm font-semibold">{t("common.show")}</th> <th className="w-1/6 text-sm font-semibold">{t("common.show")}</th>
<th className="w-1/6 text-sm font-semibold">{t("environments.surveys.edit.required")}</th> <th className="w-1/6 text-sm font-semibold">{t("environments.surveys.edit.required")}</th>
<th className="text-sm font-semibold">{t("common.placeholder")}</th> <th className="text-sm font-semibold">{t("common.label")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -23,7 +23,7 @@ const nextConfig = {
"app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"], "app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"],
}, },
i18n: { i18n: {
locales: ["en-US", "de-DE", "fr-FR", "pt-BR", "zh-Hant-TW"], locales: ["en-US", "de-DE", "fr-FR", "pt-BR", "zh-Hant-TW", "pt-PT"],
localeDetection: false, localeDetection: false,
defaultLocale: "en-US", defaultLocale: "en-US",
}, },

View File

@@ -205,22 +205,20 @@ test.describe("Survey Create & Submit Response without logic", async () => {
// Address Question // Address Question
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible(); await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
await expect( await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)).toBeVisible();
page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
).toBeVisible();
await page await page
.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1) .getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)
.fill("This is my Address"); .fill("This is my Address");
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city)).toBeVisible(); await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city).fill("This is my city"); await page.getByLabel(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible(); await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip).fill("12345"); await page.getByLabel(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
await page.locator("#questionCard-10").getByRole("button", { name: "Next" }).click(); await page.locator("#questionCard-10").getByRole("button", { name: "Next" }).click();
// Contact Info Question // Contact Info Question
await expect(page.getByText(surveys.createAndSubmit.contactInfo.question)).toBeVisible(); await expect(page.getByText(surveys.createAndSubmit.contactInfo.question)).toBeVisible();
await expect(page.getByPlaceholder(surveys.createAndSubmit.contactInfo.placeholder)).toBeVisible(); await expect(page.getByLabel(surveys.createAndSubmit.contactInfo.placeholder)).toBeVisible();
await page.getByPlaceholder(surveys.createAndSubmit.contactInfo.placeholder).fill("John Doe"); await page.getByLabel(surveys.createAndSubmit.contactInfo.placeholder).fill("John Doe");
await page.locator("#questionCard-11").getByRole("button", { name: "Next" }).click(); await page.locator("#questionCard-11").getByRole("button", { name: "Next" }).click();
// Ranking Question // Ranking Question
@@ -866,21 +864,17 @@ test.describe("Testing Survey with advanced logic", async () => {
// Address Question // Address Question
await expect(page.getByText(surveys.createWithLogicAndSubmit.address.question)).toBeVisible(); await expect(page.getByText(surveys.createWithLogicAndSubmit.address.question)).toBeVisible();
await expect( await expect(
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1) page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
).toBeVisible(); ).toBeVisible();
await page await page
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1) .getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
.fill("This is my Address"); .fill("This is my Address");
await expect( await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)).toBeVisible();
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
).toBeVisible();
await page await page
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city) .getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)
.fill("This is my city"); .fill("This is my city");
await expect( await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip)).toBeVisible();
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip) await page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
).toBeVisible();
await page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
await page.locator("#questionCard-13").getByRole("button", { name: "Next" }).click(); await page.locator("#questionCard-13").getByRole("button", { name: "Next" }).click();
// loading spinner -> wait for it to disappear // loading spinner -> wait for it to disappear

View File

@@ -4,7 +4,7 @@ import { DevTools, Tolgee } from "@tolgee/web";
const apiKey = process.env.NEXT_PUBLIC_TOLGEE_API_KEY; const apiKey = process.env.NEXT_PUBLIC_TOLGEE_API_KEY;
const apiUrl = process.env.NEXT_PUBLIC_TOLGEE_API_URL; const apiUrl = process.env.NEXT_PUBLIC_TOLGEE_API_URL;
export const ALL_LANGUAGES = ["en-US", "de-DE", "fr-FR", "pt-BR", "zh-Hant-TW"]; export const ALL_LANGUAGES = ["en-US", "de-DE", "fr-FR", "pt-BR", "pt-PT", "zh-Hant-TW"];
export const DEFAULT_LANGUAGE = "en-US"; export const DEFAULT_LANGUAGE = "en-US";
@@ -20,6 +20,7 @@ export function TolgeeBase() {
"de-DE": () => import("@formbricks/lib/messages/de-DE.json"), "de-DE": () => import("@formbricks/lib/messages/de-DE.json"),
"fr-FR": () => import("@formbricks/lib/messages/fr-FR.json"), "fr-FR": () => import("@formbricks/lib/messages/fr-FR.json"),
"pt-BR": () => import("@formbricks/lib/messages/pt-BR.json"), "pt-BR": () => import("@formbricks/lib/messages/pt-BR.json"),
"pt-PT": () => import("@formbricks/lib/messages/pt-PT.json"),
"zh-Hant-TW": () => import("@formbricks/lib/messages/zh-Hant-TW.json"), "zh-Hant-TW": () => import("@formbricks/lib/messages/zh-Hant-TW.json"),
}, },
}); });

View File

@@ -214,7 +214,7 @@ export const STRIPE_API_VERSION = "2024-06-20";
export const MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT = 150 as const; export const MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT = 150 as const;
export const DEFAULT_LOCALE = "en-US"; export const DEFAULT_LOCALE = "en-US";
export const AVAILABLE_LOCALES: TUserLocale[] = ["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW"]; export const AVAILABLE_LOCALES: TUserLocale[] = ["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW", "pt-PT"];
// Billing constants // Billing constants

File diff suppressed because it is too large Load Diff

View File

@@ -1750,9 +1750,7 @@
"how_to_create_a_panel_step_4_description": "Sobald alles eingerichtet ist, kannst Du deine Studie starten. Innerhalb weniger Stunden wirst Du die ersten Antworten erhalten.", "how_to_create_a_panel_step_4_description": "Sobald alles eingerichtet ist, kannst Du deine Studie starten. Innerhalb weniger Stunden wirst Du die ersten Antworten erhalten.",
"how_to_embed_a_survey_on_your_react_native_app": "Wie man eine Umfrage in deine React Native App einbettet", "how_to_embed_a_survey_on_your_react_native_app": "Wie man eine Umfrage in deine React Native App einbettet",
"how_to_embed_a_survey_on_your_web_app": "Wie man eine Umfrage in seine App einbettet", "how_to_embed_a_survey_on_your_web_app": "Wie man eine Umfrage in seine App einbettet",
"identify_users": "Benutzer identifizieren",
"identify_users_and_set_attributes": "Benutzer identifizieren und Attribute festlegen", "identify_users_and_set_attributes": "Benutzer identifizieren und Attribute festlegen",
"identify_users_description": "Hast Du die E-Mail-Adresse oder eine Benutzer-ID? Füge sie der URL hinzu.",
"impressions": "Eindrücke", "impressions": "Eindrücke",
"impressions_tooltip": "Anzahl der Aufrufe der Umfrage.", "impressions_tooltip": "Anzahl der Aufrufe der Umfrage.",
"includes_all": "Beinhaltet alles", "includes_all": "Beinhaltet alles",

View File

@@ -1750,9 +1750,7 @@
"how_to_create_a_panel_step_4_description": "Once everything is setup, you can launch your study. Within a few hours youll receive the first responses.", "how_to_create_a_panel_step_4_description": "Once everything is setup, you can launch your study. Within a few hours youll receive the first responses.",
"how_to_embed_a_survey_on_your_react_native_app": "How to embed a survey on your React Native app", "how_to_embed_a_survey_on_your_react_native_app": "How to embed a survey on your React Native app",
"how_to_embed_a_survey_on_your_web_app": "How to embed a survey on your web app", "how_to_embed_a_survey_on_your_web_app": "How to embed a survey on your web app",
"identify_users": "Identify users",
"identify_users_and_set_attributes": "identify users and set attributes", "identify_users_and_set_attributes": "identify users and set attributes",
"identify_users_description": "You have the email address or a userId? Append it to the URL.",
"impressions": "Impressions", "impressions": "Impressions",
"impressions_tooltip": "Number of times the survey has been viewed.", "impressions_tooltip": "Number of times the survey has been viewed.",
"includes_all": "Includes all", "includes_all": "Includes all",

View File

@@ -1750,9 +1750,7 @@
"how_to_create_a_panel_step_4_description": "Une fois que tout est configuré, vous pouvez lancer votre étude. Dans quelques heures, vous recevrez les premières réponses.", "how_to_create_a_panel_step_4_description": "Une fois que tout est configuré, vous pouvez lancer votre étude. Dans quelques heures, vous recevrez les premières réponses.",
"how_to_embed_a_survey_on_your_react_native_app": "Comment intégrer un sondage dans votre application React Native", "how_to_embed_a_survey_on_your_react_native_app": "Comment intégrer un sondage dans votre application React Native",
"how_to_embed_a_survey_on_your_web_app": "Comment intégrer une enquête dans votre application web", "how_to_embed_a_survey_on_your_web_app": "Comment intégrer une enquête dans votre application web",
"identify_users": "Identifier les utilisateurs",
"identify_users_and_set_attributes": "identifier les utilisateurs et définir des attributs", "identify_users_and_set_attributes": "identifier les utilisateurs et définir des attributs",
"identify_users_description": "Avez-vous l'adresse e-mail ou un identifiant utilisateur ? Ajoutez-le à l'URL.",
"impressions": "Impressions", "impressions": "Impressions",
"impressions_tooltip": "Nombre de fois que l'enquête a été consultée.", "impressions_tooltip": "Nombre de fois que l'enquête a été consultée.",
"includes_all": "Comprend tous", "includes_all": "Comprend tous",

View File

@@ -1750,9 +1750,7 @@
"how_to_create_a_panel_step_4_description": "Depois que tudo estiver configurado, você pode iniciar seu estudo. Em algumas horas, você vai receber as primeiras respostas.", "how_to_create_a_panel_step_4_description": "Depois que tudo estiver configurado, você pode iniciar seu estudo. Em algumas horas, você vai receber as primeiras respostas.",
"how_to_embed_a_survey_on_your_react_native_app": "Como incorporar uma pesquisa no seu app React Native", "how_to_embed_a_survey_on_your_react_native_app": "Como incorporar uma pesquisa no seu app React Native",
"how_to_embed_a_survey_on_your_web_app": "Como incorporar uma pesquisa no seu app web", "how_to_embed_a_survey_on_your_web_app": "Como incorporar uma pesquisa no seu app web",
"identify_users": "Identificar usuários",
"identify_users_and_set_attributes": "identificar usuários e definir atributos", "identify_users_and_set_attributes": "identificar usuários e definir atributos",
"identify_users_description": "Você tem o endereço de e-mail ou um userId? Adiciona isso ao URL.",
"impressions": "Impressões", "impressions": "Impressões",
"impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.", "impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.",
"includes_all": "Inclui tudo", "includes_all": "Inclui tudo",

View File

@@ -1750,9 +1750,7 @@
"how_to_create_a_panel_step_4_description": "Depois de tudo configurado, pode lançar o seu estudo. Dentro de algumas horas, receberá as primeiras respostas.", "how_to_create_a_panel_step_4_description": "Depois de tudo configurado, pode lançar o seu estudo. Dentro de algumas horas, receberá as primeiras respostas.",
"how_to_embed_a_survey_on_your_react_native_app": "Como incorporar um questionário na sua aplicação React Native", "how_to_embed_a_survey_on_your_react_native_app": "Como incorporar um questionário na sua aplicação React Native",
"how_to_embed_a_survey_on_your_web_app": "Como incorporar um questionário na sua aplicação web", "how_to_embed_a_survey_on_your_web_app": "Como incorporar um questionário na sua aplicação web",
"identify_users": "Identificar utilizadores",
"identify_users_and_set_attributes": "identificar utilizadores e definir atributos", "identify_users_and_set_attributes": "identificar utilizadores e definir atributos",
"identify_users_description": "Tem o endereço de email ou um userId? Adicione-o ao URL.",
"impressions": "Impressões", "impressions": "Impressões",
"impressions_tooltip": "Número de vezes que o inquérito foi visualizado.", "impressions_tooltip": "Número de vezes que o inquérito foi visualizado.",
"includes_all": "Inclui tudo", "includes_all": "Inclui tudo",

View File

@@ -1750,9 +1750,7 @@
"how_to_create_a_panel_step_4_description": "設定完成後,您可以啟動您的研究。在幾個小時內,您就會收到第一個回應。", "how_to_create_a_panel_step_4_description": "設定完成後,您可以啟動您的研究。在幾個小時內,您就會收到第一個回應。",
"how_to_embed_a_survey_on_your_react_native_app": "如何在您的 React Native 應用程式中嵌入問卷", "how_to_embed_a_survey_on_your_react_native_app": "如何在您的 React Native 應用程式中嵌入問卷",
"how_to_embed_a_survey_on_your_web_app": "如何在您的 Web 應用程式中嵌入問卷", "how_to_embed_a_survey_on_your_web_app": "如何在您的 Web 應用程式中嵌入問卷",
"identify_users": "識別使用者",
"identify_users_and_set_attributes": "識別使用者並設定屬性", "identify_users_and_set_attributes": "識別使用者並設定屬性",
"identify_users_description": "您有電子郵件地址或使用者 ID 嗎?將其附加到網址。",
"impressions": "曝光數", "impressions": "曝光數",
"impressions_tooltip": "問卷已檢視的次數。", "impressions_tooltip": "問卷已檢視的次數。",
"includes_all": "包含全部", "includes_all": "包含全部",

View File

@@ -1,5 +1,5 @@
import { formatDistance, intlFormat } from "date-fns"; import { formatDistance, intlFormat } from "date-fns";
import { de, enUS, fr, ptBR, zhTW } from "date-fns/locale"; import { de, enUS, fr, pt, ptBR, zhTW } from "date-fns/locale";
import { TUserLocale } from "@formbricks/types/user"; import { TUserLocale } from "@formbricks/types/user";
export const convertDateString = (dateString: string) => { export const convertDateString = (dateString: string) => {
@@ -88,6 +88,8 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
return fr; return fr;
case "zh-Hant-TW": case "zh-Hant-TW":
return zhTW; return zhTW;
case "pt-PT":
return pt;
} }
}; };

View File

@@ -36,6 +36,36 @@ export function FileInput({
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [parent] = useAutoAnimate(); const [parent] = useAutoAnimate();
// Helper function to filter duplicate files
const filterDuplicateFiles = <T extends { name: string }>(
files: T[],
checkAgainstSelected: boolean = true
): {
filteredFiles: T[];
duplicateFiles: T[];
} => {
const existingFileNames = fileUrls ? fileUrls.map(getOriginalFileNameFromUrl) : [];
const duplicateFiles = files.filter(
(file) =>
existingFileNames.includes(file.name) ||
(checkAgainstSelected && selectedFiles.some((selectedFile) => selectedFile.name === file.name))
);
const filteredFiles = files.filter(
(file) =>
!existingFileNames.includes(file.name) &&
(!checkAgainstSelected || !selectedFiles.some((selectedFile) => selectedFile.name === file.name))
);
if (duplicateFiles.length > 0) {
const duplicateNames = duplicateFiles.map((file) => file.name).join(", ");
alert(`The following files are already uploaded: ${duplicateNames}. Duplicate files are not allowed.`);
}
return { filteredFiles, duplicateFiles };
};
// Listen for the native file-upload event dispatched via window.formbricksSurveys.onFilePick // Listen for the native file-upload event dispatched via window.formbricksSurveys.onFilePick
useEffect(() => { useEffect(() => {
const handleNativeFileUpload = async ( const handleNativeFileUpload = async (
@@ -47,7 +77,7 @@ export function FileInput({
setIsUploading(true); setIsUploading(true);
// Filter out files that exceed the maximum size // Filter out files that exceed the maximum size
const filteredFiles: typeof filesFromNative = []; let filteredFiles: typeof filesFromNative = [];
const rejectedFiles: string[] = []; const rejectedFiles: string[] = [];
if (maxSizeInMB) { if (maxSizeInMB) {
@@ -67,6 +97,10 @@ export function FileInput({
filteredFiles.push(...filesFromNative); filteredFiles.push(...filesFromNative);
} }
// Check for duplicate files - native uploads don't need to check against selectedFiles
const { filteredFiles: nonDuplicateFiles } = filterDuplicateFiles(filteredFiles, false);
filteredFiles = nonDuplicateFiles;
// Display alert for rejected files // Display alert for rejected files
if (rejectedFiles.length > 0) { if (rejectedFiles.length > 0) {
const fileNames = rejectedFiles.join(", "); const fileNames = rejectedFiles.join(", ");
@@ -113,7 +147,7 @@ export function FileInput({
}; };
const handleFileSelection = async (files: FileList) => { const handleFileSelection = async (files: FileList) => {
const fileArray = Array.from(files); let fileArray = Array.from(files);
if (!allowMultipleFiles && fileArray.length > 1) { if (!allowMultipleFiles && fileArray.length > 1) {
alert("Only one file can be uploaded at a time."); alert("Only one file can be uploaded at a time.");
@@ -125,8 +159,17 @@ export function FileInput({
return; return;
} }
// Check for duplicate files
const { filteredFiles: nonDuplicateFiles } = filterDuplicateFiles(fileArray);
if (nonDuplicateFiles.length === 0) {
return; // No non-duplicate files to process
}
fileArray = nonDuplicateFiles;
// filter out files that are not allowed // filter out files that are not allowed
const validFiles = Array.from(files).filter((file) => { const validFiles = fileArray.filter((file) => {
const fileExtension = file.type.substring(file.type.lastIndexOf("/") + 1) as TAllowedFileExtension; const fileExtension = file.type.substring(file.type.lastIndexOf("/") + 1) as TAllowedFileExtension;
if (allowedFileExtensions) { if (allowedFileExtensions) {
return allowedFileExtensions.includes(fileExtension); return allowedFileExtensions.includes(fileExtension);

View File

@@ -0,0 +1,7 @@
interface LabelProps {
text: string;
}
export function Label({ text }: LabelProps) {
return <label className="fb-text-subheading fb-font-normal fb-text-sm">{text}</label>;
}

View File

@@ -2,6 +2,7 @@ import { BackButton } from "@/components/buttons/back-button";
import { SubmitButton } from "@/components/buttons/submit-button"; import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline"; import { Headline } from "@/components/general/headline";
import { Input } from "@/components/general/input"; import { Input } from "@/components/general/input";
import { Label } from "@/components/general/label";
import { QuestionMedia } from "@/components/general/question-media"; import { QuestionMedia } from "@/components/general/question-media";
import { Subheader } from "@/components/general/subheader"; import { Subheader } from "@/components/general/subheader";
import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
@@ -56,32 +57,32 @@ export function AddressQuestion({
{ {
id: "addressLine1", id: "addressLine1",
...question.addressLine1, ...question.addressLine1,
placeholder: question.addressLine1.placeholder[languageCode], label: question.addressLine1.placeholder[languageCode],
}, },
{ {
id: "addressLine2", id: "addressLine2",
...question.addressLine2, ...question.addressLine2,
placeholder: question.addressLine2.placeholder[languageCode], label: question.addressLine2.placeholder[languageCode],
}, },
{ {
id: "city", id: "city",
...question.city, ...question.city,
placeholder: question.city.placeholder[languageCode], label: question.city.placeholder[languageCode],
}, },
{ {
id: "state", id: "state",
...question.state, ...question.state,
placeholder: question.state.placeholder[languageCode], label: question.state.placeholder[languageCode],
}, },
{ {
id: "zip", id: "zip",
...question.zip, ...question.zip,
placeholder: question.zip.placeholder[languageCode], label: question.zip.placeholder[languageCode],
}, },
{ {
id: "country", id: "country",
...question.country, ...question.country,
placeholder: question.country.placeholder[languageCode], label: question.country.placeholder[languageCode],
}, },
]; ];
@@ -155,19 +156,21 @@ export function AddressQuestion({
return ( return (
field.show && ( field.show && (
<Input <div className="fb-space-y-1">
key={field.id} <Label text={isFieldRequired() ? `${field.label}*` : field.label} />
placeholder={isFieldRequired() ? `${field.placeholder}*` : field.placeholder} <Input
required={isFieldRequired()} key={field.id}
value={safeValue[index] || ""} required={isFieldRequired()}
className="fb-py-3" value={safeValue[index] || ""}
type={field.id === "email" ? "email" : "text"} type={field.id === "email" ? "email" : "text"}
onChange={(e) => { onChange={(e) => {
handleChange(field.id, e.currentTarget.value); handleChange(field.id, e.currentTarget.value);
}} }}
ref={index === 0 ? addressRef : null} ref={index === 0 ? addressRef : null}
tabIndex={isCurrent ? 0 : -1} tabIndex={isCurrent ? 0 : -1}
/> aria-label={field.label}
/>
</div>
) )
); );
})} })}

View File

@@ -2,6 +2,7 @@ import { BackButton } from "@/components/buttons/back-button";
import { SubmitButton } from "@/components/buttons/submit-button"; import { SubmitButton } from "@/components/buttons/submit-button";
import { Headline } from "@/components/general/headline"; import { Headline } from "@/components/general/headline";
import { Input } from "@/components/general/input"; import { Input } from "@/components/general/input";
import { Label } from "@/components/general/label";
import { QuestionMedia } from "@/components/general/question-media"; import { QuestionMedia } from "@/components/general/question-media";
import { Subheader } from "@/components/general/subheader"; import { Subheader } from "@/components/general/subheader";
import { ScrollableContainer } from "@/components/wrappers/scrollable-container"; import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
@@ -56,27 +57,27 @@ export function ContactInfoQuestion({
{ {
id: "firstName", id: "firstName",
...question.firstName, ...question.firstName,
placeholder: question.firstName.placeholder[languageCode], label: question.firstName.placeholder[languageCode],
}, },
{ {
id: "lastName", id: "lastName",
...question.lastName, ...question.lastName,
placeholder: question.lastName.placeholder[languageCode], label: question.lastName.placeholder[languageCode],
}, },
{ {
id: "email", id: "email",
...question.email, ...question.email,
placeholder: question.email.placeholder[languageCode], label: question.email.placeholder[languageCode],
}, },
{ {
id: "phone", id: "phone",
...question.phone, ...question.phone,
placeholder: question.phone.placeholder[languageCode], label: question.phone.placeholder[languageCode],
}, },
{ {
id: "company", id: "company",
...question.company, ...question.company,
placeholder: question.company.placeholder[languageCode], label: question.company.placeholder[languageCode],
}, },
]; ];
@@ -157,19 +158,21 @@ export function ContactInfoQuestion({
return ( return (
field.show && ( field.show && (
<Input <div className="fb-space-y-1">
ref={index === 0 ? contactInfoRef : null} <Label text={isFieldRequired() ? `${field.label}*` : field.label} />
key={field.id} <Input
placeholder={isFieldRequired() ? `${field.placeholder}*` : field.placeholder} ref={index === 0 ? contactInfoRef : null}
required={isFieldRequired()} key={field.id}
value={safeValue[index] || ""} required={isFieldRequired()}
className="fb-py-3" value={safeValue[index] || ""}
type={inputType} type={inputType}
onChange={(e) => { onChange={(e) => {
handleChange(field.id, e.currentTarget.value); handleChange(field.id, e.currentTarget.value);
}} }}
tabIndex={isCurrent ? 0 : -1} tabIndex={isCurrent ? 0 : -1}
/> aria-label={field.label}
/>
</div>
) )
); );
})} })}

View File

@@ -1168,7 +1168,7 @@ export const ZSurvey = z
const multiLangIssueInPlaceholder = const multiLangIssueInPlaceholder =
field.show && field.show &&
validateQuestionLabels( validateQuestionLabels(
`Placeholder for field ${field.label}`, `Label for field ${field.label}`,
field.placeholder, field.placeholder,
languages, languages,
questionIndex, questionIndex,
@@ -1202,7 +1202,7 @@ export const ZSurvey = z
const multiLangIssueInPlaceholder = const multiLangIssueInPlaceholder =
field.show && field.show &&
validateQuestionLabels( validateQuestionLabels(
`Placeholder for field ${field.label}`, `Label for field ${field.label}`,
field.placeholder, field.placeholder,
languages, languages,
questionIndex, questionIndex,

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
const ZRole = z.enum(["project_manager", "engineer", "founder", "marketing_specialist", "other"]); const ZRole = z.enum(["project_manager", "engineer", "founder", "marketing_specialist", "other"]);
export const ZUserLocale = z.enum(["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW"]); export const ZUserLocale = z.enum(["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW", "pt-PT"]);
export type TUserLocale = z.infer<typeof ZUserLocale>; export type TUserLocale = z.infer<typeof ZUserLocale>;
export const ZUserObjective = z.enum([ export const ZUserObjective = z.enum([