mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-28 01:09:59 -06:00
Compare commits
5 Commits
response-m
...
fix-date-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b9a884364 | ||
|
|
da4211f0b0 | ||
|
|
b21827cb32 | ||
|
|
4424a8a21d | ||
|
|
eb030f9ed6 |
@@ -5,7 +5,7 @@ permissions:
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
@@ -14,6 +14,13 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
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
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
23
.github/workflows/tolgee.yml
vendored
23
.github/workflows/tolgee.yml
vendored
@@ -3,7 +3,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
branches:
|
||||
- main
|
||||
@@ -23,18 +23,16 @@ jobs:
|
||||
- name: Get source branch name
|
||||
id: branch-name
|
||||
run: |
|
||||
# For PR merges, use the head ref from the pull request event
|
||||
SOURCE_BRANCH="${{ github.head_ref }}"
|
||||
RAW_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"
|
||||
|
||||
- name: Setup Node.js
|
||||
@@ -53,6 +51,7 @@ jobs:
|
||||
--filter-tag "draft:${SOURCE_BRANCH}" \
|
||||
--tag production \
|
||||
--untag "draft:${SOURCE_BRANCH}"
|
||||
--verbose
|
||||
|
||||
- name: Tag unused production keys as Deprecated
|
||||
run: |
|
||||
@@ -60,6 +59,7 @@ jobs:
|
||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||
--filter-not-extracted --filter-tag production \
|
||||
--tag deprecated --untag production
|
||||
--verbose
|
||||
|
||||
- name: Tag unused draft:current-branch keys as Deprecated
|
||||
run: |
|
||||
@@ -67,6 +67,7 @@ jobs:
|
||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||
--filter-not-extracted --filter-tag "draft:${SOURCE_BRANCH}" \
|
||||
--tag deprecated --untag "draft:${SOURCE_BRANCH}"
|
||||
--verbose
|
||||
|
||||
- name: Sync with backup
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -53,4 +53,5 @@ yarn-error.log*
|
||||
packages/lib/uploads
|
||||
apps/web/public/js
|
||||
packages/database/migrations
|
||||
branch.json
|
||||
branch.json
|
||||
.vercel
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
{
|
||||
"language": "zh-Hant-TW",
|
||||
"path": "./packages/lib/messages/zh-Hant-TW.json"
|
||||
},
|
||||
{
|
||||
"language": "pt-PT",
|
||||
"path": "./packages/lib/messages/pt-PT.json"
|
||||
}
|
||||
],
|
||||
"forceMode": "OVERRIDE"
|
||||
|
||||
@@ -75,6 +75,7 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean>
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error in sendEmail:", error);
|
||||
throw new InvalidInputError("Incorrect SMTP credentials");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -81,7 +81,7 @@ export const QuestionToggleTable = ({
|
||||
</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="text-sm font-semibold">{t("common.placeholder")}</th>
|
||||
<th className="text-sm font-semibold">{t("common.label")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -23,7 +23,7 @@ const nextConfig = {
|
||||
"app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"],
|
||||
},
|
||||
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,
|
||||
defaultLocale: "en-US",
|
||||
},
|
||||
|
||||
@@ -205,22 +205,20 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
|
||||
// Address Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
|
||||
await expect(
|
||||
page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)).toBeVisible();
|
||||
await page
|
||||
.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
|
||||
.getByLabel(surveys.createAndSubmit.address.placeholder.addressLine1)
|
||||
.fill("This is my Address");
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
|
||||
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
|
||||
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
|
||||
await page.getByLabel(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
|
||||
await page.getByLabel(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
|
||||
await page.locator("#questionCard-10").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Contact Info Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.contactInfo.question)).toBeVisible();
|
||||
await expect(page.getByPlaceholder(surveys.createAndSubmit.contactInfo.placeholder)).toBeVisible();
|
||||
await page.getByPlaceholder(surveys.createAndSubmit.contactInfo.placeholder).fill("John Doe");
|
||||
await expect(page.getByLabel(surveys.createAndSubmit.contactInfo.placeholder)).toBeVisible();
|
||||
await page.getByLabel(surveys.createAndSubmit.contactInfo.placeholder).fill("John Doe");
|
||||
await page.locator("#questionCard-11").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Ranking Question
|
||||
@@ -866,21 +864,17 @@ test.describe("Testing Survey with advanced logic", async () => {
|
||||
// Address Question
|
||||
await expect(page.getByText(surveys.createWithLogicAndSubmit.address.question)).toBeVisible();
|
||||
await expect(
|
||||
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||
page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||
).toBeVisible();
|
||||
await page
|
||||
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||
.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
|
||||
.fill("This is my Address");
|
||||
await expect(
|
||||
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)).toBeVisible();
|
||||
await page
|
||||
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
|
||||
.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.city)
|
||||
.fill("This is my city");
|
||||
await expect(
|
||||
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip)
|
||||
).toBeVisible();
|
||||
await page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
|
||||
await expect(page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip)).toBeVisible();
|
||||
await page.getByLabel(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
|
||||
await page.locator("#questionCard-13").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// loading spinner -> wait for it to disappear
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DevTools, Tolgee } from "@tolgee/web";
|
||||
const apiKey = process.env.NEXT_PUBLIC_TOLGEE_API_KEY;
|
||||
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";
|
||||
|
||||
@@ -20,6 +20,7 @@ export function TolgeeBase() {
|
||||
"de-DE": () => import("@formbricks/lib/messages/de-DE.json"),
|
||||
"fr-FR": () => import("@formbricks/lib/messages/fr-FR.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"),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -214,7 +214,7 @@ export const STRIPE_API_VERSION = "2024-06-20";
|
||||
export const MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT = 150 as const;
|
||||
|
||||
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
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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_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",
|
||||
"identify_users": "Benutzer identifizieren",
|
||||
"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_tooltip": "Anzahl der Aufrufe der Umfrage.",
|
||||
"includes_all": "Beinhaltet alles",
|
||||
|
||||
@@ -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 you’ll 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_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_description": "You have the email address or a userId? Append it to the URL.",
|
||||
"impressions": "Impressions",
|
||||
"impressions_tooltip": "Number of times the survey has been viewed.",
|
||||
"includes_all": "Includes all",
|
||||
|
||||
@@ -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_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",
|
||||
"identify_users": "Identifier les utilisateurs",
|
||||
"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_tooltip": "Nombre de fois que l'enquête a été consultée.",
|
||||
"includes_all": "Comprend tous",
|
||||
|
||||
@@ -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_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",
|
||||
"identify_users": "Identificar usuários",
|
||||
"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_tooltip": "Número de vezes que a pesquisa foi visualizada.",
|
||||
"includes_all": "Inclui tudo",
|
||||
|
||||
@@ -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_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",
|
||||
"identify_users": "Identificar utilizadores",
|
||||
"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_tooltip": "Número de vezes que o inquérito foi visualizado.",
|
||||
"includes_all": "Inclui tudo",
|
||||
|
||||
@@ -1750,9 +1750,7 @@
|
||||
"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_web_app": "如何在您的 Web 應用程式中嵌入問卷",
|
||||
"identify_users": "識別使用者",
|
||||
"identify_users_and_set_attributes": "識別使用者並設定屬性",
|
||||
"identify_users_description": "您有電子郵件地址或使用者 ID 嗎?將其附加到網址。",
|
||||
"impressions": "曝光數",
|
||||
"impressions_tooltip": "問卷已檢視的次數。",
|
||||
"includes_all": "包含全部",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
export const convertDateString = (dateString: string) => {
|
||||
@@ -88,6 +88,8 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
|
||||
return fr;
|
||||
case "zh-Hant-TW":
|
||||
return zhTW;
|
||||
case "pt-PT":
|
||||
return pt;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,36 @@ export function FileInput({
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
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
|
||||
useEffect(() => {
|
||||
const handleNativeFileUpload = async (
|
||||
@@ -47,7 +77,7 @@ export function FileInput({
|
||||
setIsUploading(true);
|
||||
|
||||
// Filter out files that exceed the maximum size
|
||||
const filteredFiles: typeof filesFromNative = [];
|
||||
let filteredFiles: typeof filesFromNative = [];
|
||||
const rejectedFiles: string[] = [];
|
||||
|
||||
if (maxSizeInMB) {
|
||||
@@ -67,6 +97,10 @@ export function FileInput({
|
||||
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
|
||||
if (rejectedFiles.length > 0) {
|
||||
const fileNames = rejectedFiles.join(", ");
|
||||
@@ -113,7 +147,7 @@ export function FileInput({
|
||||
};
|
||||
|
||||
const handleFileSelection = async (files: FileList) => {
|
||||
const fileArray = Array.from(files);
|
||||
let fileArray = Array.from(files);
|
||||
|
||||
if (!allowMultipleFiles && fileArray.length > 1) {
|
||||
alert("Only one file can be uploaded at a time.");
|
||||
@@ -125,8 +159,17 @@ export function FileInput({
|
||||
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
|
||||
const validFiles = Array.from(files).filter((file) => {
|
||||
const validFiles = fileArray.filter((file) => {
|
||||
const fileExtension = file.type.substring(file.type.lastIndexOf("/") + 1) as TAllowedFileExtension;
|
||||
if (allowedFileExtensions) {
|
||||
return allowedFileExtensions.includes(fileExtension);
|
||||
|
||||
7
packages/surveys/src/components/general/label.tsx
Normal file
7
packages/surveys/src/components/general/label.tsx
Normal 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>;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { Input } from "@/components/general/input";
|
||||
import { Label } from "@/components/general/label";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
@@ -56,32 +57,32 @@ export function AddressQuestion({
|
||||
{
|
||||
id: "addressLine1",
|
||||
...question.addressLine1,
|
||||
placeholder: question.addressLine1.placeholder[languageCode],
|
||||
label: question.addressLine1.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "addressLine2",
|
||||
...question.addressLine2,
|
||||
placeholder: question.addressLine2.placeholder[languageCode],
|
||||
label: question.addressLine2.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "city",
|
||||
...question.city,
|
||||
placeholder: question.city.placeholder[languageCode],
|
||||
label: question.city.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "state",
|
||||
...question.state,
|
||||
placeholder: question.state.placeholder[languageCode],
|
||||
label: question.state.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "zip",
|
||||
...question.zip,
|
||||
placeholder: question.zip.placeholder[languageCode],
|
||||
label: question.zip.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "country",
|
||||
...question.country,
|
||||
placeholder: question.country.placeholder[languageCode],
|
||||
label: question.country.placeholder[languageCode],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -155,19 +156,21 @@ export function AddressQuestion({
|
||||
|
||||
return (
|
||||
field.show && (
|
||||
<Input
|
||||
key={field.id}
|
||||
placeholder={isFieldRequired() ? `${field.placeholder}*` : field.placeholder}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
className="fb-py-3"
|
||||
type={field.id === "email" ? "email" : "text"}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
ref={index === 0 ? addressRef : null}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
<div className="fb-space-y-1">
|
||||
<Label text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<Input
|
||||
key={field.id}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
type={field.id === "email" ? "email" : "text"}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
ref={index === 0 ? addressRef : null}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label={field.label}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { Input } from "@/components/general/input";
|
||||
import { Label } from "@/components/general/label";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
@@ -56,27 +57,27 @@ export function ContactInfoQuestion({
|
||||
{
|
||||
id: "firstName",
|
||||
...question.firstName,
|
||||
placeholder: question.firstName.placeholder[languageCode],
|
||||
label: question.firstName.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "lastName",
|
||||
...question.lastName,
|
||||
placeholder: question.lastName.placeholder[languageCode],
|
||||
label: question.lastName.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
...question.email,
|
||||
placeholder: question.email.placeholder[languageCode],
|
||||
label: question.email.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
...question.phone,
|
||||
placeholder: question.phone.placeholder[languageCode],
|
||||
label: question.phone.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "company",
|
||||
...question.company,
|
||||
placeholder: question.company.placeholder[languageCode],
|
||||
label: question.company.placeholder[languageCode],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -157,19 +158,21 @@ export function ContactInfoQuestion({
|
||||
|
||||
return (
|
||||
field.show && (
|
||||
<Input
|
||||
ref={index === 0 ? contactInfoRef : null}
|
||||
key={field.id}
|
||||
placeholder={isFieldRequired() ? `${field.placeholder}*` : field.placeholder}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
className="fb-py-3"
|
||||
type={inputType}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
<div className="fb-space-y-1">
|
||||
<Label text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<Input
|
||||
ref={index === 0 ? contactInfoRef : null}
|
||||
key={field.id}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue[index] || ""}
|
||||
type={inputType}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label={field.label}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1168,7 +1168,7 @@ export const ZSurvey = z
|
||||
const multiLangIssueInPlaceholder =
|
||||
field.show &&
|
||||
validateQuestionLabels(
|
||||
`Placeholder for field ${field.label}`,
|
||||
`Label for field ${field.label}`,
|
||||
field.placeholder,
|
||||
languages,
|
||||
questionIndex,
|
||||
@@ -1202,7 +1202,7 @@ export const ZSurvey = z
|
||||
const multiLangIssueInPlaceholder =
|
||||
field.show &&
|
||||
validateQuestionLabels(
|
||||
`Placeholder for field ${field.label}`,
|
||||
`Label for field ${field.label}`,
|
||||
field.placeholder,
|
||||
languages,
|
||||
questionIndex,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
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 const ZUserObjective = z.enum([
|
||||
|
||||
Reference in New Issue
Block a user