mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-29 03:13:19 -05:00
Merge branch 'main' of https://github.com/formbricks/formbricks into Naidu-4444/main
This commit is contained in:
@@ -309,7 +309,6 @@
|
||||
"project_not_found": "Projekt nicht gefunden",
|
||||
"project_permission_not_found": "Projekt-Berechtigung nicht gefunden",
|
||||
"projects": "Projekte",
|
||||
"projects_limit_reached": "Projektlimit erreicht",
|
||||
"question": "Frage",
|
||||
"question_id": "Frage-ID",
|
||||
"questions": "Fragen",
|
||||
|
||||
@@ -309,7 +309,6 @@
|
||||
"project_not_found": "Project not found",
|
||||
"project_permission_not_found": "Project permission not found",
|
||||
"projects": "Projects",
|
||||
"projects_limit_reached": "Projects limit reached",
|
||||
"question": "Question",
|
||||
"question_id": "Question ID",
|
||||
"questions": "Questions",
|
||||
|
||||
@@ -309,7 +309,6 @@
|
||||
"project_not_found": "Projet non trouvé",
|
||||
"project_permission_not_found": "Autorisation de projet non trouvée",
|
||||
"projects": "Projets",
|
||||
"projects_limit_reached": "Limite de projets atteinte",
|
||||
"question": "Question",
|
||||
"question_id": "ID de la question",
|
||||
"questions": "Questions",
|
||||
|
||||
@@ -309,7 +309,6 @@
|
||||
"project_not_found": "Projeto não encontrado",
|
||||
"project_permission_not_found": "Permissão do projeto não encontrada",
|
||||
"projects": "Projetos",
|
||||
"projects_limit_reached": "Limites de projetos atingidos",
|
||||
"question": "Pergunta",
|
||||
"question_id": "ID da Pergunta",
|
||||
"questions": "Perguntas",
|
||||
|
||||
@@ -309,7 +309,6 @@
|
||||
"project_not_found": "Projeto não encontrado",
|
||||
"project_permission_not_found": "Permissão do projeto não encontrada",
|
||||
"projects": "Projetos",
|
||||
"projects_limit_reached": "Limite de projetos atingido",
|
||||
"question": "Pergunta",
|
||||
"question_id": "ID da pergunta",
|
||||
"questions": "Perguntas",
|
||||
|
||||
@@ -309,7 +309,6 @@
|
||||
"project_not_found": "找不到專案",
|
||||
"project_permission_not_found": "找不到專案權限",
|
||||
"projects": "專案",
|
||||
"projects_limit_reached": "已達到專案上限",
|
||||
"question": "問題",
|
||||
"question_id": "問題 ID",
|
||||
"questions": "問題",
|
||||
|
||||
@@ -6,13 +6,70 @@ import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi";
|
||||
const bulkContactEndpoint: ZodOpenApiOperationObject = {
|
||||
operationId: "uploadBulkContacts",
|
||||
summary: "Upload Bulk Contacts",
|
||||
description: "Uploads contacts in bulk",
|
||||
description:
|
||||
"Uploads contacts in bulk. Each contact in the payload must have an 'email' attribute present in their attributes array. The email attribute is mandatory and must be a valid email format. Without a valid email, the contact will be skipped during processing.",
|
||||
requestBody: {
|
||||
required: true,
|
||||
description: "The contacts to upload",
|
||||
description:
|
||||
"The contacts to upload. Each contact must include an 'email' attribute in their attributes array. The email is used as the unique identifier for the contact.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ZContactBulkUploadRequest,
|
||||
example: {
|
||||
environmentId: "env_01h2xce9q8p3w4x5y6z7a8b9c0",
|
||||
contacts: [
|
||||
{
|
||||
attributes: [
|
||||
{
|
||||
attributeKey: {
|
||||
key: "email",
|
||||
name: "Email Address",
|
||||
},
|
||||
value: "john.doe@example.com",
|
||||
},
|
||||
{
|
||||
attributeKey: {
|
||||
key: "firstName",
|
||||
name: "First Name",
|
||||
},
|
||||
value: "John",
|
||||
},
|
||||
{
|
||||
attributeKey: {
|
||||
key: "lastName",
|
||||
name: "Last Name",
|
||||
},
|
||||
value: "Doe",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
attributes: [
|
||||
{
|
||||
attributeKey: {
|
||||
key: "email",
|
||||
name: "Email Address",
|
||||
},
|
||||
value: "jane.smith@example.com",
|
||||
},
|
||||
{
|
||||
attributeKey: {
|
||||
key: "firstName",
|
||||
name: "First Name",
|
||||
},
|
||||
value: "Jane",
|
||||
},
|
||||
{
|
||||
attributeKey: {
|
||||
key: "lastName",
|
||||
name: "Last Name",
|
||||
},
|
||||
value: "Smith",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,11 +4,19 @@ import { logger } from "@formbricks/logger";
|
||||
|
||||
// Define the v1 (now v2) client endpoints to be merged
|
||||
const v1ClientEndpoints = {
|
||||
"/responses/{responseId}": {
|
||||
"/client/{environmentId}/responses/{responseId}": {
|
||||
put: {
|
||||
security: [],
|
||||
description:
|
||||
"Update an existing response for example when you want to mark a response as finished or you want to change an existing response's value.",
|
||||
parameters: [
|
||||
{
|
||||
in: "path",
|
||||
name: "environmentId",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
description: "The ID of the environment.",
|
||||
},
|
||||
{
|
||||
in: "path",
|
||||
name: "responseId",
|
||||
@@ -57,14 +65,15 @@ const v1ClientEndpoints = {
|
||||
tags: ["Client API > Response"],
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.formbricks.com/api/v2/client",
|
||||
url: "https://app.formbricks.com/api/v2",
|
||||
description: "Formbricks Client",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"/{environmentId}/responses": {
|
||||
"/client/{environmentId}/responses": {
|
||||
post: {
|
||||
security: [],
|
||||
description:
|
||||
"Create a response for a survey and its fields with the user's responses. The userId & meta here is optional",
|
||||
requestBody: {
|
||||
@@ -89,14 +98,15 @@ const v1ClientEndpoints = {
|
||||
tags: ["Client API > Response"],
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.formbricks.com/api/v2/client",
|
||||
url: "https://app.formbricks.com/api/v2",
|
||||
description: "Formbricks Client",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"/{environmentId}/contacts/{userId}/attributes": {
|
||||
"/client/{environmentId}/contacts/{userId}/attributes": {
|
||||
put: {
|
||||
security: [],
|
||||
description:
|
||||
"Update a contact's attributes in Formbricks to keep them in sync with your app or when you want to set a custom attribute in Formbricks.",
|
||||
parameters: [
|
||||
@@ -138,14 +148,15 @@ const v1ClientEndpoints = {
|
||||
tags: ["Client API > Contacts"],
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.formbricks.com/api/v2/client",
|
||||
url: "https://app.formbricks.com/api/v2",
|
||||
description: "Formbricks Client",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"/{environmentId}/identify/contacts/{userId}": {
|
||||
"/client/{environmentId}/identify/contacts/{userId}": {
|
||||
get: {
|
||||
security: [],
|
||||
description:
|
||||
"Retrieves a contact's state including their segments, displays, responses and other tracking information. If the contact doesn't exist, it will be created.",
|
||||
parameters: [
|
||||
@@ -167,14 +178,15 @@ const v1ClientEndpoints = {
|
||||
tags: ["Client API > Contacts"],
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.formbricks.com/api/v2/client",
|
||||
url: "https://app.formbricks.com/api/v2",
|
||||
description: "Formbricks Client",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"/{environmentId}/displays": {
|
||||
"/client/{environmentId}/displays": {
|
||||
post: {
|
||||
security: [],
|
||||
description:
|
||||
"Create a new display for a valid survey ID. If a userId is passed, the display is linked to the user.",
|
||||
requestBody: {
|
||||
@@ -199,48 +211,25 @@ const v1ClientEndpoints = {
|
||||
tags: ["Client API > Display"],
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.formbricks.com/api/v2/client",
|
||||
url: "https://app.formbricks.com/api/v2",
|
||||
description: "Formbricks Client",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"/{environmentId}/displays/{displayId}": {
|
||||
put: {
|
||||
description:
|
||||
"Update a Display for a user. A use case can be when a user submits a response & you want to link it to an existing display.",
|
||||
parameters: [{ in: "path", name: "displayId", required: true, schema: { type: "string" } }],
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { example: { responseId: "response123" }, type: "object" },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
content: {
|
||||
"application/json": {
|
||||
example: { displayId: "display123" },
|
||||
schema: { type: "object" },
|
||||
},
|
||||
},
|
||||
description: "OK",
|
||||
},
|
||||
},
|
||||
summary: "Update Display",
|
||||
tags: ["Client API > Display"],
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.formbricks.com/api/v2/client",
|
||||
description: "Formbricks Client",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"/{environmentId}/environment": {
|
||||
"/client/{environmentId}/environment": {
|
||||
get: {
|
||||
security: [],
|
||||
description: "Retrieves the environment state to be used in Formbricks SDKs",
|
||||
parameters: [
|
||||
{
|
||||
in: "path",
|
||||
name: "environmentId",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
description: "The ID of the environment.",
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
"200": {
|
||||
content: {
|
||||
@@ -256,14 +245,15 @@ const v1ClientEndpoints = {
|
||||
tags: ["Client API > Environment"],
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.formbricks.com/api/v2/client",
|
||||
url: "https://app.formbricks.com/api/v2",
|
||||
description: "Formbricks Client",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"/{environmentId}/user": {
|
||||
"/client/{environmentId}/user": {
|
||||
post: {
|
||||
security: [],
|
||||
description:
|
||||
"Endpoint for creating or identifying a user within the specified environment. If the user already exists, this will identify them and potentially update user attributes. If they don't exist, it will create a new user.",
|
||||
requestBody: {
|
||||
@@ -288,14 +278,15 @@ const v1ClientEndpoints = {
|
||||
tags: ["Client API > User"],
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.formbricks.com/api/v2/client",
|
||||
url: "https://app.formbricks.com/api/v2",
|
||||
description: "Formbricks Client",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"/{environmentId}/storage": {
|
||||
"/client/{environmentId}/storage": {
|
||||
post: {
|
||||
security: [],
|
||||
summary: "Upload Private File",
|
||||
description:
|
||||
"API endpoint for uploading private files. Uploaded files are kept private so that only users with access to the specified environment can retrieve them. The endpoint validates the survey ID, file name, and file type from the request body, and returns a signed URL for S3 uploads along with a local upload URL.",
|
||||
@@ -442,14 +433,15 @@ const v1ClientEndpoints = {
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "https://app.formbricks.com/api/v2/client",
|
||||
url: "https://app.formbricks.com/api/v2",
|
||||
description: "Formbricks API Server",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"/{environmentId}/storage/local": {
|
||||
"/client/{environmentId}/storage/local": {
|
||||
post: {
|
||||
security: [],
|
||||
summary: "Upload Private File to Local Storage",
|
||||
description:
|
||||
'API endpoint for uploading private files to local storage. The request must include a valid signature, UUID, and timestamp to verify the upload. The file is provided as a Base64 encoded string in the request body. The "Content-Type" header must be set to a valid MIME type, and the file data must be a valid file object (buffer).',
|
||||
@@ -478,7 +470,8 @@ const v1ClientEndpoints = {
|
||||
},
|
||||
fileName: {
|
||||
type: "string",
|
||||
description: "The URI encoded file name.",
|
||||
description:
|
||||
"This must be the `fileName` returned from the [Upload Private File](/api-v2-reference/client-api->-file-upload/upload-private-file) endpoint (Step 1).",
|
||||
},
|
||||
fileType: {
|
||||
type: "string",
|
||||
|
||||
@@ -34,11 +34,18 @@ tags:
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
paths:
|
||||
/responses/{responseId}:
|
||||
/client/{environmentId}/responses/{responseId}:
|
||||
put:
|
||||
security: []
|
||||
description: Update an existing response for example when you want to mark a
|
||||
response as finished or you want to change an existing response's value.
|
||||
parameters:
|
||||
- in: path
|
||||
name: environmentId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The ID of the environment.
|
||||
- in: path
|
||||
name: responseId
|
||||
required: true
|
||||
@@ -77,10 +84,11 @@ paths:
|
||||
tags:
|
||||
- Client API > Response
|
||||
servers:
|
||||
- url: https://app.formbricks.com/api/v2/client
|
||||
- url: https://app.formbricks.com/api/v2
|
||||
description: Formbricks Client
|
||||
/{environmentId}/responses:
|
||||
/client/{environmentId}/responses:
|
||||
post:
|
||||
security: []
|
||||
description: Create a response for a survey and its fields with the user's
|
||||
responses. The userId & meta here is optional
|
||||
requestBody:
|
||||
@@ -104,10 +112,11 @@ paths:
|
||||
tags:
|
||||
- Client API > Response
|
||||
servers:
|
||||
- url: https://app.formbricks.com/api/v2/client
|
||||
- url: https://app.formbricks.com/api/v2
|
||||
description: Formbricks Client
|
||||
/{environmentId}/contacts/{userId}/attributes:
|
||||
/client/{environmentId}/contacts/{userId}/attributes:
|
||||
put:
|
||||
security: []
|
||||
description: Update a contact's attributes in Formbricks to keep them in sync
|
||||
with your app or when you want to set a custom attribute in Formbricks.
|
||||
parameters:
|
||||
@@ -152,10 +161,11 @@ paths:
|
||||
tags:
|
||||
- Client API > Contacts
|
||||
servers:
|
||||
- url: https://app.formbricks.com/api/v2/client
|
||||
- url: https://app.formbricks.com/api/v2
|
||||
description: Formbricks Client
|
||||
/{environmentId}/identify/contacts/{userId}:
|
||||
/client/{environmentId}/identify/contacts/{userId}:
|
||||
get:
|
||||
security: []
|
||||
description: Retrieves a contact's state including their segments, displays,
|
||||
responses and other tracking information. If the contact doesn't exist,
|
||||
it will be created.
|
||||
@@ -184,10 +194,11 @@ paths:
|
||||
tags:
|
||||
- Client API > Contacts
|
||||
servers:
|
||||
- url: https://app.formbricks.com/api/v2/client
|
||||
- url: https://app.formbricks.com/api/v2
|
||||
description: Formbricks Client
|
||||
/{environmentId}/displays:
|
||||
/client/{environmentId}/displays:
|
||||
post:
|
||||
security: []
|
||||
description: Create a new display for a valid survey ID. If a userId is passed,
|
||||
the display is linked to the user.
|
||||
requestBody:
|
||||
@@ -211,43 +222,19 @@ paths:
|
||||
tags:
|
||||
- Client API > Display
|
||||
servers:
|
||||
- url: https://app.formbricks.com/api/v2/client
|
||||
- url: https://app.formbricks.com/api/v2
|
||||
description: Formbricks Client
|
||||
/{environmentId}/displays/{displayId}:
|
||||
put:
|
||||
description: Update a Display for a user. A use case can be when a user submits
|
||||
a response & you want to link it to an existing display.
|
||||
/client/{environmentId}/environment:
|
||||
get:
|
||||
security: []
|
||||
description: Retrieves the environment state to be used in Formbricks SDKs
|
||||
parameters:
|
||||
- in: path
|
||||
name: displayId
|
||||
name: environmentId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
responseId: response123
|
||||
type: object
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
example:
|
||||
displayId: display123
|
||||
schema:
|
||||
type: object
|
||||
description: OK
|
||||
summary: Update Display
|
||||
tags:
|
||||
- Client API > Display
|
||||
servers:
|
||||
- url: https://app.formbricks.com/api/v2/client
|
||||
description: Formbricks Client
|
||||
/{environmentId}/environment:
|
||||
get:
|
||||
description: Retrieves the environment state to be used in Formbricks SDKs
|
||||
description: The ID of the environment.
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
@@ -262,10 +249,11 @@ paths:
|
||||
tags:
|
||||
- Client API > Environment
|
||||
servers:
|
||||
- url: https://app.formbricks.com/api/v2/client
|
||||
- url: https://app.formbricks.com/api/v2
|
||||
description: Formbricks Client
|
||||
/{environmentId}/user:
|
||||
/client/{environmentId}/user:
|
||||
post:
|
||||
security: []
|
||||
description: Endpoint for creating or identifying a user within the specified
|
||||
environment. If the user already exists, this will identify them and
|
||||
potentially update user attributes. If they don't exist, it will create
|
||||
@@ -292,10 +280,11 @@ paths:
|
||||
tags:
|
||||
- Client API > User
|
||||
servers:
|
||||
- url: https://app.formbricks.com/api/v2/client
|
||||
- url: https://app.formbricks.com/api/v2
|
||||
description: Formbricks Client
|
||||
/{environmentId}/storage:
|
||||
/client/{environmentId}/storage:
|
||||
post:
|
||||
security: []
|
||||
summary: Upload Private File
|
||||
description: API endpoint for uploading private files. Uploaded files are kept
|
||||
private so that only users with access to the specified environment can
|
||||
@@ -402,10 +391,11 @@ paths:
|
||||
example:
|
||||
error: Survey survey123 not found
|
||||
servers:
|
||||
- url: https://app.formbricks.com/api/v2/client
|
||||
- url: https://app.formbricks.com/api/v2
|
||||
description: Formbricks API Server
|
||||
/{environmentId}/storage/local:
|
||||
/client/{environmentId}/storage/local:
|
||||
post:
|
||||
security: []
|
||||
summary: Upload Private File to Local Storage
|
||||
description: API endpoint for uploading private files to local storage. The
|
||||
request must include a valid signature, UUID, and timestamp to verify
|
||||
@@ -433,7 +423,9 @@ paths:
|
||||
description: The ID of the survey associated with the file.
|
||||
fileName:
|
||||
type: string
|
||||
description: The URI encoded file name.
|
||||
description: This must be the `fileName` returned from the [Upload Private
|
||||
File](/api-v2-reference/client-api->-file-upload/upload-private-file)
|
||||
endpoint (Step 1).
|
||||
fileType:
|
||||
type: string
|
||||
description: The MIME type of the file.
|
||||
@@ -1531,12 +1523,15 @@ paths:
|
||||
put:
|
||||
operationId: uploadBulkContacts
|
||||
summary: Upload Bulk Contacts
|
||||
description: Uploads contacts in bulk
|
||||
description: Uploads contacts in bulk. Each contact in the payload must have an
|
||||
'email' attribute present in their attributes array. The email attribute
|
||||
is mandatory and must be a valid email format.
|
||||
tags:
|
||||
- Management API > Contacts
|
||||
requestBody:
|
||||
required: true
|
||||
description: The contacts to upload
|
||||
description: The contacts to upload. Each contact **must include an 'email'
|
||||
attribute** in their attributes array.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -1571,10 +1566,39 @@ paths:
|
||||
- value
|
||||
required:
|
||||
- attributes
|
||||
maxItems: 1000
|
||||
maxItems: 250
|
||||
required:
|
||||
- environmentId
|
||||
- contacts
|
||||
example:
|
||||
environmentId: env_01h2xce9q8p3w4x5y6z7a8b9c0
|
||||
contacts:
|
||||
- attributes:
|
||||
- attributeKey:
|
||||
key: email
|
||||
name: Email Address
|
||||
value: john.doe@example.com
|
||||
- attributeKey:
|
||||
key: firstName
|
||||
name: First Name
|
||||
value: John
|
||||
- attributeKey:
|
||||
key: lastName
|
||||
name: Last Name
|
||||
value: Doe
|
||||
- attributes:
|
||||
- attributeKey:
|
||||
key: email
|
||||
name: Email Address
|
||||
value: jane.smith@example.com
|
||||
- attributeKey:
|
||||
key: firstName
|
||||
name: First Name
|
||||
value: Jane
|
||||
- attributeKey:
|
||||
key: lastName
|
||||
name: Last Name
|
||||
value: Smith
|
||||
responses:
|
||||
"200":
|
||||
description: Contacts uploaded successfully.
|
||||
@@ -4383,6 +4407,22 @@ components:
|
||||
- enabled
|
||||
- message
|
||||
description: Email verification configuration (deprecated)
|
||||
recaptcha:
|
||||
type:
|
||||
- object
|
||||
- "null"
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
threshold:
|
||||
type: number
|
||||
multipleOf: 0.1
|
||||
minimum: 0.1
|
||||
maximum: 0.9
|
||||
required:
|
||||
- enabled
|
||||
- threshold
|
||||
description: Google reCAPTCHA configuration
|
||||
displayPercentage:
|
||||
type:
|
||||
- number
|
||||
@@ -4431,6 +4471,7 @@ components:
|
||||
- inlineTriggers
|
||||
- isBackButtonHidden
|
||||
- verifyEmail
|
||||
- recaptcha
|
||||
- displayPercentage
|
||||
- questions
|
||||
webhook:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { checkForLoomUrl, checkForVimeoUrl, checkForYoutubeUrl, convertToEmbedUrl } from "@/lib/video-upload";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
@@ -26,7 +27,7 @@ export function QuestionMedia({ imgUrl, videoUrl, altText = "Image" }: QuestionM
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="fb-group/image fb-relative fb-mb-4 fb-block fb-min-h-40 fb-rounded-md">
|
||||
<div className="fb-group/image fb-relative fb-mb-6 fb-block fb-min-h-40 fb-rounded-md">
|
||||
{isLoading ? (
|
||||
<div className="fb-absolute fb-inset-auto fb-flex fb-h-full fb-w-full fb-animate-pulse fb-items-center fb-justify-center fb-rounded-md fb-bg-slate-200" />
|
||||
) : null}
|
||||
@@ -35,10 +36,16 @@ export function QuestionMedia({ imgUrl, videoUrl, altText = "Image" }: QuestionM
|
||||
key={imgUrl}
|
||||
src={imgUrl}
|
||||
alt={altText}
|
||||
className="fb-rounded-custom"
|
||||
className={cn(
|
||||
"fb-rounded-custom fb-max-h-[40dvh] fb-mx-auto fb-object-contain",
|
||||
isLoading ? "fb-opacity-0" : ""
|
||||
)}
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
onError={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{videoUrlWithParams ? (
|
||||
@@ -48,10 +55,13 @@ export function QuestionMedia({ imgUrl, videoUrl, altText = "Image" }: QuestionM
|
||||
src={videoUrlWithParams}
|
||||
title="Question Video"
|
||||
frameBorder="0"
|
||||
className="fb-rounded-custom fb-aspect-video fb-w-full"
|
||||
className={cn("fb-rounded-custom fb-aspect-video fb-w-full", isLoading ? "fb-opacity-0" : "")}
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
onError={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
/>
|
||||
|
||||
@@ -171,7 +171,7 @@ describe("PictureSelectionQuestion", () => {
|
||||
render(<PictureSelectionQuestion {...mockProps} />);
|
||||
|
||||
const images = screen.getAllByRole("img");
|
||||
const label = images[0].closest("label");
|
||||
const label = images[0].closest("button");
|
||||
|
||||
fireEvent.keyDown(label!, { key: " " });
|
||||
|
||||
|
||||
@@ -43,6 +43,13 @@ export function PictureSelectionQuestion({
|
||||
isBackButtonHidden,
|
||||
}: Readonly<PictureSelectionProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>(() => {
|
||||
const initialLoadingState: Record<string, boolean> = {};
|
||||
question.choices.forEach((choice) => {
|
||||
initialLoadingState[choice.id] = true;
|
||||
});
|
||||
return initialLoadingState;
|
||||
});
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
@@ -115,35 +122,75 @@ export function PictureSelectionQuestion({
|
||||
<div className="fb-mt-4">
|
||||
<fieldset>
|
||||
<legend className="fb-sr-only">Options</legend>
|
||||
<div className="fb-bg-survey-bg fb-relative fb-grid fb-grid-cols-2 fb-gap-4">
|
||||
<div className="fb-bg-survey-bg fb-relative fb-grid fb-grid-cols-1 sm:fb-grid-cols-2 fb-gap-4">
|
||||
{questionChoices.map((choice) => (
|
||||
<label
|
||||
key={choice.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
htmlFor={choice.id}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
document.getElementById(choice.id)?.click();
|
||||
document.getElementById(choice.id)?.focus();
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
handleChange(choice.id);
|
||||
}}
|
||||
className={cn(
|
||||
"fb-relative fb-w-full fb-cursor-pointer fb-overflow-hidden fb-border fb-rounded-custom focus:fb-outline-none fb-aspect-[4/3] fb-min-h-[7rem] fb-max-h-[50vh] focus:fb-border-brand focus:fb-border-4 group/image",
|
||||
Array.isArray(value) && value.includes(choice.id)
|
||||
? "fb-border-brand fb-text-brand fb-z-10 fb-border-4 fb-shadow-sm"
|
||||
: ""
|
||||
)}>
|
||||
<img
|
||||
src={choice.imageUrl}
|
||||
id={choice.id}
|
||||
alt={getOriginalFileNameFromUrl(choice.imageUrl)}
|
||||
className="fb-h-full fb-w-full fb-object-cover"
|
||||
/>
|
||||
<div className="fb-relative" key={choice.id}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
e.currentTarget.click();
|
||||
e.currentTarget.focus();
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
handleChange(choice.id);
|
||||
}}
|
||||
className={cn(
|
||||
"fb-relative fb-w-full fb-cursor-pointer fb-overflow-hidden fb-border fb-rounded-custom focus-visible:fb-outline-none focus-visible:fb-ring-2 focus-visible:fb-ring-brand focus-visible:fb-ring-offset-2 fb-aspect-[4/3] fb-min-h-[7rem] fb-max-h-[50vh] group/image",
|
||||
Array.isArray(value) && value.includes(choice.id)
|
||||
? "fb-border-brand fb-text-brand fb-z-10 fb-border-4 fb-shadow-sm"
|
||||
: ""
|
||||
)}>
|
||||
{loadingImages[choice.id] && (
|
||||
<div className="fb-absolute fb-inset-0 fb-flex fb-h-full fb-w-full fb-animate-pulse fb-items-center fb-justify-center fb-rounded-md fb-bg-slate-200" />
|
||||
)}
|
||||
<img
|
||||
src={choice.imageUrl}
|
||||
id={choice.id}
|
||||
alt={getOriginalFileNameFromUrl(choice.imageUrl)}
|
||||
className={cn(
|
||||
"fb-h-full fb-w-full fb-object-cover",
|
||||
loadingImages[choice.id] ? "fb-opacity-0" : ""
|
||||
)}
|
||||
onLoad={() => {
|
||||
setLoadingImages((prev) => ({ ...prev, [choice.id]: false }));
|
||||
}}
|
||||
onError={() => {
|
||||
setLoadingImages((prev) => ({ ...prev, [choice.id]: false }));
|
||||
}}
|
||||
/>
|
||||
{question.allowMulti ? (
|
||||
<input
|
||||
id={`${choice.id}-checked`}
|
||||
name={`${choice.id}-checkbox`}
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"fb-border-border fb-rounded-custom fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-border",
|
||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id={`${choice.id}-radio`}
|
||||
name={`${question.id}`}
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"fb-border-border fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-rounded-full fb-border",
|
||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<a
|
||||
tabIndex={-1}
|
||||
href={choice.imageUrl}
|
||||
@@ -153,52 +200,25 @@ export function PictureSelectionQuestion({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="fb-absolute fb-bottom-2 fb-right-2 fb-flex fb-items-center fb-gap-2 fb-whitespace-nowrap fb-rounded-md fb-bg-slate-800 fb-bg-opacity-40 fb-p-1.5 fb-text-white fb-opacity-0 fb-backdrop-blur-lg fb-transition fb-duration-300 fb-ease-in-out hover:fb-bg-opacity-65 group-hover/image:fb-opacity-100">
|
||||
className="fb-absolute fb-bottom-4 fb-right-2 fb-flex fb-items-center fb-gap-2 fb-whitespace-nowrap fb-rounded-md fb-bg-slate-800 fb-bg-opacity-40 fb-p-1.5 fb-text-white fb-backdrop-blur-lg fb-transition fb-duration-300 fb-ease-in-out hover:fb-bg-opacity-65 group-hover/image:fb-opacity-100 fb-z-20">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-expand">
|
||||
<path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8" />
|
||||
<path d="M3 16.2V21m0 0h4.8M3 21l6-6" />
|
||||
<path d="M21 7.8V3m0 0h-4.8M21 3l-6 6" />
|
||||
<path d="M3 7.8V3m0 0h4.8M3 3l6 6" />
|
||||
className="lucide lucide-image-down-icon lucide-image-down">
|
||||
<path d="M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21" />
|
||||
<path d="m14 19 3 3v-5.5" />
|
||||
<path d="m17 22 3-3" />
|
||||
<circle cx="9" cy="9" r="2" />
|
||||
</svg>
|
||||
</a>
|
||||
{question.allowMulti ? (
|
||||
<input
|
||||
id={`${choice.id}-checked`}
|
||||
name={`${choice.id}-checkbox`}
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"fb-border-border fb-rounded-custom fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-border",
|
||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id={`${choice.id}-radio`}
|
||||
name={`${choice.id}-radio`}
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
checked={value.includes(choice.id)}
|
||||
className={cn(
|
||||
"fb-border-border fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-rounded-full fb-border",
|
||||
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
|
||||
)}
|
||||
required={question.required && value.length ? false : question.required}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
Reference in New Issue
Block a user