feat: Added Unsplash API for image backgrounds (#2341)

Co-authored-by: ShubhamPalriwala <spalriwalau@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
Vidhi Kapadia
2024-04-24 20:08:35 +05:30
committed by GitHub
parent b322f014ab
commit 2da2758255
31 changed files with 413 additions and 35 deletions

View File

@@ -173,6 +173,9 @@ ENTERPRISE_LICENSE_KEY=
# OpenTelemetry URL for tracing
# OPENTELEMETRY_LISTENER_URL=http://localhost:4318/v1/traces
# Unsplash API Key
UNSPLASH_ACCESS_KEY=
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
# REDIS_URL:

View File

@@ -24,9 +24,10 @@ type ThemeStylingProps = {
product: TProduct;
environmentId: string;
colors: string[];
isUnsplashConfigured: boolean;
};
export const ThemeStyling = ({ product, environmentId, colors }: ThemeStylingProps) => {
export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigured }: ThemeStylingProps) => {
const router = useRouter();
const [localProduct, setLocalProduct] = useState(product);
const [previewSurveyType, setPreviewSurveyType] = useState<TSurveyType>("link");
@@ -211,6 +212,7 @@ export const ThemeStyling = ({ product, environmentId, colors }: ThemeStylingPro
colors={colors}
key={styling.background?.bg}
hideCheckmark
isUnsplashConfigured={isUnsplashConfigured}
/>
</div>
</div>

View File

@@ -6,7 +6,7 @@ import {
getRemoveLinkBrandingPermission,
} from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { SURVEY_BG_COLORS } from "@formbricks/lib/constants";
import { SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
@@ -53,7 +53,12 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
title="Theme"
className="max-w-7xl"
description="Create a style theme for all surveys. You can enable custom styling for each survey.">
<ThemeStyling environmentId={params.environmentId} product={product} colors={SURVEY_BG_COLORS} />
<ThemeStyling
environmentId={params.environmentId}
product={product}
colors={SURVEY_BG_COLORS}
isUnsplashConfigured={UNSPLASH_ACCESS_KEY ? true : false}
/>
</SettingsCard>{" "}
<SettingsCard title="Logo" description="Upload your company logo to brand surveys and link previews.">
<EditLogo product={product} environmentId={params.environmentId} isViewer={isViewer} />

View File

@@ -3,6 +3,7 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { canUserAccessProduct } from "@formbricks/lib/product/auth";
import { getProduct } from "@formbricks/lib/product/service";
@@ -199,3 +200,61 @@ export const resetBasicSegmentFiltersAction = async (surveyId: string) => {
return await resetSegmentInSurvey(surveyId);
};
export async function getImagesFromUnsplashAction(searchQuery: string, page: number = 1) {
const baseUrl = "https://api.unsplash.com/search/photos";
const params = new URLSearchParams({
query: searchQuery,
client_id: UNSPLASH_ACCESS_KEY,
orientation: "landscape",
per_page: "9",
page: page.toString(),
});
try {
const response = await fetch(`${baseUrl}?${params}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to fetch images from Unsplash");
}
const { results } = await response.json();
return results.map((result) => {
const authorName = encodeURIComponent(result.user.first_name + " " + result.user.last_name);
const authorLink = encodeURIComponent(result.user.links.html);
return {
id: result.id,
alt_description: result.alt_description,
urls: {
regularWithAttribution: `${result.urls.regular}&dpr=2&authorLink=${authorLink}&authorName=${authorName}&utm_source=formbricks&utm_medium=referral`,
download: result.links.download_location,
},
};
});
} catch (error) {
throw new Error("Error getting images from Unsplash");
}
}
export async function triggerDownloadUnsplashImageAction(downloadUrl: string) {
try {
const response = await fetch(`${downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to download image from Unsplash");
}
return;
} catch (error) {
throw new Error("Error downloading image from Unsplash");
}
}

View File

@@ -20,6 +20,7 @@ interface BackgroundStylingCardProps {
hideCheckmark?: boolean;
disabled?: boolean;
environmentId: string;
isUnsplashConfigured: boolean;
}
export default function BackgroundStylingCard({
@@ -31,6 +32,7 @@ export default function BackgroundStylingCard({
hideCheckmark,
disabled,
environmentId,
isUnsplashConfigured,
}: BackgroundStylingCardProps) {
const { bgType, brightness } = styling?.background ?? {};
@@ -113,6 +115,7 @@ export default function BackgroundStylingCard({
colors={colors}
bgType={bgType}
environmentId={environmentId}
isUnsplashConfigured={isUnsplashConfigured}
/>
</div>

View File

@@ -1,12 +1,16 @@
import { FileInput } from "@formbricks/ui/FileInput";
interface ImageSurveyBgProps {
interface UploadImageSurveyBgProps {
environmentId: string;
handleBgChange: (url: string, bgType: string) => void;
background: string;
}
export const ImageSurveyBg = ({ environmentId, handleBgChange, background }: ImageSurveyBgProps) => {
export const UploadImageSurveyBg = ({
environmentId,
handleBgChange,
background,
}: UploadImageSurveyBgProps) => {
return (
<div className="mt-2 w-full">
<div className="flex w-full items-center justify-center">
@@ -16,9 +20,9 @@ export const ImageSurveyBg = ({ environmentId, handleBgChange, background }: Ima
environmentId={environmentId}
onFileUpload={(url: string[]) => {
if (url.length > 0) {
handleBgChange(url[0], "image");
handleBgChange(url[0], "upload");
} else {
handleBgChange("", "image");
handleBgChange("", "upload");
}
}}
fileUrl={background}

View File

@@ -23,6 +23,7 @@ type StylingViewProps = {
setStyling: React.Dispatch<React.SetStateAction<TSurveyStyling | null>>;
localStylingChanges: TSurveyStyling | null;
setLocalStylingChanges: React.Dispatch<React.SetStateAction<TSurveyStyling | null>>;
isUnsplashConfigured: boolean;
};
export const StylingView = ({
@@ -35,6 +36,7 @@ export const StylingView = ({
styling,
localStylingChanges,
setLocalStylingChanges,
isUnsplashConfigured,
}: StylingViewProps) => {
const [overwriteThemeStyling, setOverwriteThemeStyling] = useState(
localSurvey?.styling?.overwriteThemeStyling ?? false
@@ -162,6 +164,7 @@ export const StylingView = ({
environmentId={environment.id}
colors={colors}
disabled={!overwriteThemeStyling}
isUnsplashConfigured={isUnsplashConfigured}
/>
)}

View File

@@ -6,7 +6,8 @@ import { TabBar } from "@formbricks/ui/TabBar";
import { AnimatedSurveyBg } from "./AnimatedSurveyBg";
import { ColorSurveyBg } from "./ColorSurveyBg";
import { ImageSurveyBg } from "./ImageSurveyBg";
import { UploadImageSurveyBg } from "./ImageSurveyBg";
import { ImageFromUnsplashSurveyBg } from "./UnsplashImages";
interface SurveyBgSelectorTabProps {
handleBgChange: (bg: string, bgType: string) => void;
@@ -14,11 +15,13 @@ interface SurveyBgSelectorTabProps {
bgType: string | null | undefined;
environmentId: string;
styling: TSurveyStyling | TProductStyling | null;
isUnsplashConfigured: boolean;
}
const tabs = [
{ id: "color", label: "Color" },
{ id: "animation", label: "Animation" },
{ id: "upload", label: "Upload" },
{ id: "image", label: "Image" },
];
@@ -28,52 +31,59 @@ export default function SurveyBgSelectorTab({
colors,
bgType,
environmentId,
isUnsplashConfigured,
}: SurveyBgSelectorTabProps) {
const [activeTab, setActiveTab] = useState(bgType || "color");
const { background } = styling ?? {};
const bgUrl = styling?.background?.bg || "";
const [colorBackground, setColorBackground] = useState(background?.bg);
const [animationBackground, setAnimationBackground] = useState(background?.bg);
const [imageBackground, setImageBackground] = useState(background?.bg);
const [colorBackground, setColorBackground] = useState(bgUrl);
const [animationBackground, setAnimationBackground] = useState(bgUrl);
const [uploadBackground, setUploadBackground] = useState(bgUrl);
useEffect(() => {
const bgType = background?.bgType;
if (bgType === "color") {
setColorBackground(background?.bg);
setColorBackground(bgUrl);
setAnimationBackground("");
setImageBackground("");
setUploadBackground("");
}
if (bgType === "animation") {
setAnimationBackground(background?.bg);
setAnimationBackground(bgUrl);
setColorBackground("");
setImageBackground("");
setUploadBackground("");
}
if (bgType === "image") {
setImageBackground(background?.bg);
if (isUnsplashConfigured && bgType === "image") {
setColorBackground("");
setAnimationBackground("");
setUploadBackground("");
}
if (bgType === "upload") {
setUploadBackground(bgUrl);
setColorBackground("");
setAnimationBackground("");
}
}, [background?.bg, background?.bgType]);
}, [bgUrl, bgType, isUnsplashConfigured]);
const renderContent = () => {
switch (activeTab) {
case "color":
return (
<ColorSurveyBg handleBgChange={handleBgChange} colors={colors} background={colorBackground ?? ""} />
);
return <ColorSurveyBg handleBgChange={handleBgChange} colors={colors} background={colorBackground} />;
case "animation":
return <AnimatedSurveyBg handleBgChange={handleBgChange} background={animationBackground ?? ""} />;
case "image":
return <AnimatedSurveyBg handleBgChange={handleBgChange} background={animationBackground} />;
case "upload":
return (
<ImageSurveyBg
<UploadImageSurveyBg
environmentId={environmentId}
handleBgChange={handleBgChange}
background={imageBackground ?? ""}
background={uploadBackground}
/>
);
case "image":
if (isUnsplashConfigured) {
return <ImageFromUnsplashSurveyBg handleBgChange={handleBgChange} />;
}
default:
return null;
}
@@ -82,7 +92,7 @@ export default function SurveyBgSelectorTab({
return (
<div className="mt-4 flex flex-col items-center justify-center rounded-lg ">
<TabBar
tabs={tabs}
tabs={tabs.filter((tab) => tab.id !== "image" || isUnsplashConfigured)}
activeId={activeTab}
setActiveId={setActiveTab}
tabStyle="button"

View File

@@ -34,6 +34,7 @@ interface SurveyEditorProps {
isUserTargetingAllowed?: boolean;
isMultiLanguageAllowed?: boolean;
isFormbricksCloud: boolean;
isUnsplashConfigured: boolean;
}
export default function SurveyEditor({
@@ -49,6 +50,7 @@ export default function SurveyEditor({
isMultiLanguageAllowed,
isUserTargetingAllowed = false,
isFormbricksCloud,
isUnsplashConfigured,
}: SurveyEditorProps): JSX.Element {
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("questions");
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
@@ -174,6 +176,7 @@ export default function SurveyEditor({
setStyling={setStyling}
localStylingChanges={localStylingChanges}
setLocalStylingChanges={setLocalStylingChanges}
isUnsplashConfigured={isUnsplashConfigured}
/>
)}

View File

@@ -0,0 +1,234 @@
"use client";
import { debounce } from "lodash";
import { SearchIcon } from "lucide-react";
import UnsplashImage from "next/image";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { TSurveyBackgroundBgType } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import LoadingSpinner from "@formbricks/ui/LoadingSpinner";
import { getImagesFromUnsplashAction, triggerDownloadUnsplashImageAction } from "../actions";
interface ImageFromUnsplashSurveyBgProps {
handleBgChange: (url: string, bgType: TSurveyBackgroundBgType) => void;
}
interface UnsplashImage {
id: string;
alt_description: string;
urls: {
regularWithAttribution: string;
download?: string;
};
authorName?: string;
}
const defaultImages = [
{
id: "dog-1",
alt_description: "Dog",
urls: {
regularWithAttribution: "/image-backgrounds/dogs.webp",
},
},
{
id: "pencil",
alt_description: "Pencil",
urls: {
regularWithAttribution: "/image-backgrounds/pencil.webp",
},
},
{
id: "plant",
alt_description: "Plant",
urls: {
regularWithAttribution: "/image-backgrounds/plant.webp",
},
},
{
id: "dog-2",
alt_description: "Another Dog",
urls: {
regularWithAttribution: "/image-backgrounds/dog-2.webp",
},
},
{
id: "kitten-2",
alt_description: "Another Kitten",
urls: {
regularWithAttribution: "/image-backgrounds/kitten-2.webp",
},
},
{
id: "lollipop",
alt_description: "Lollipop",
urls: {
regularWithAttribution: "/image-backgrounds/lolipop.webp",
},
},
{
id: "oranges",
alt_description: "Oranges",
urls: {
regularWithAttribution: "/image-backgrounds/oranges.webp",
},
},
{
id: "flower",
alt_description: "Flower",
urls: {
regularWithAttribution: "/image-backgrounds/flowers.webp",
},
},
{
id: "supermario",
alt_description: "Super Mario",
urls: {
regularWithAttribution: "/image-backgrounds/supermario.webp",
},
},
{
id: "shapes",
alt_description: "Shapes",
urls: {
regularWithAttribution: "/image-backgrounds/shapes.webp",
},
},
{
id: "waves",
alt_description: "Waves",
urls: {
regularWithAttribution: "/image-backgrounds/waves.webp",
},
},
{
id: "kitten-1",
alt_description: "Kitten",
urls: {
regularWithAttribution: "/image-backgrounds/kittens.webp",
},
},
];
export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashSurveyBgProps) => {
const inputFocus = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const [query, setQuery] = useState("");
const [images, setImages] = useState<UnsplashImage[]>(defaultImages);
const [page, setPage] = useState(1);
useEffect(() => {
const fetchData = async (searchQuery: string, currentPage: number) => {
try {
setIsLoading(true);
const imagesFromUnsplash = await getImagesFromUnsplashAction(searchQuery, currentPage);
for (let i = 0; i < imagesFromUnsplash.length; i++) {
const authorName = new URL(imagesFromUnsplash[i].urls.regularWithAttribution).searchParams.get(
"authorName"
);
imagesFromUnsplash[i].authorName = authorName;
}
setImages((prevImages) => [...prevImages, ...imagesFromUnsplash]);
} catch (error) {
toast.error(error.message);
} finally {
setIsLoading(false);
}
};
const debouncedFetchData = debounce((q) => fetchData(q, page), 500);
if (query.trim() !== "") {
debouncedFetchData(query);
}
return () => {
debouncedFetchData.cancel();
};
}, [query, page, setImages]);
useEffect(() => {
inputFocus.current?.focus();
}, []);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
setPage(1);
setImages([]);
};
const handleImageSelected = async (imageUrl: string, downloadImageUrl?: string) => {
try {
handleBgChange(imageUrl, "image");
if (downloadImageUrl) {
await triggerDownloadUnsplashImageAction(downloadImageUrl);
}
} catch (error) {
toast.error(error.message);
}
};
const handleLoadMore = () => {
setPage((prevPage) => prevPage + 1);
};
return (
<div className="relative mt-2 w-full">
<div className="relative">
<SearchIcon className="absolute left-2 top-1/2 h-6 w-4 -translate-y-1/2 text-slate-500" />
<Input
value={query}
onChange={handleChange}
placeholder="Try 'lollipop' or 'mountain'..."
className="pl-8"
ref={inputFocus}
aria-label="Search for images"
/>
</div>
<div className="relative mt-4 grid grid-cols-3 gap-1">
{images.length > 0 &&
images.map((image) => (
<div key={image.id} className="group relative">
<UnsplashImage
width={300}
height={200}
src={image.urls.regularWithAttribution}
alt={image.alt_description}
onClick={() => handleImageSelected(image.urls.regularWithAttribution, image.urls.download)}
className="h-full cursor-pointer rounded-lg object-cover"
/>
{image.authorName && (
<span className="absolute bottom-1 right-1 hidden rounded bg-black bg-opacity-75 px-2 py-1 text-xs text-white group-hover:block">
{image.authorName}
</span>
)}
</div>
))}
{isLoading && (
<div className="col-span-3 flex items-center justify-center p-3">
<LoadingSpinner />
</div>
)}
{images.length > 0 && !isLoading && query.trim() !== "" && (
<Button
size="sm"
variant="secondary"
className="col-span-3 mt-3 flex items-center justify-center"
type="button"
onClick={handleLoadMore}>
Load More
</Button>
)}
{!isLoading && images.length === 0 && query.trim() !== "" && (
<div className="col-span-3 flex items-center justify-center text-sm text-slate-500">
No images found for &apos;{query}&apos;
</div>
)}
</div>
</div>
);
};

View File

@@ -4,7 +4,7 @@ import { getAdvancedTargetingPermission, getMultiLanguagePermission } from "@for
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS } from "@formbricks/lib/constants";
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -87,6 +87,7 @@ export default async function SurveysEditPage({ params }) {
isUserTargetingAllowed={isUserTargetingAllowed}
isMultiLanguageAllowed={isMultiLanguageAllowed}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isUnsplashConfigured={UNSPLASH_ACCESS_KEY ? true : false}
/>
);
}

View File

@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { TProduct } from "@formbricks/types/product";
@@ -24,6 +25,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
}) => {
const animatedBackgroundRef = useRef<HTMLVideoElement>(null);
const [backgroundLoaded, setBackgroundLoaded] = useState(false);
const [authorDetailsForUnsplash, setAuthorDetailsForUnsplash] = useState({ authorName: "", authorURL: "" });
// get the background from either the survey or the product styling
const background = useMemo(() => {
@@ -55,7 +57,18 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
// Cleanup
return () => video.removeEventListener("canplaythrough", onCanPlayThrough);
} else if (background?.bgType === "image" && background?.bg) {
} else if ((background?.bgType === "image" || background?.bgType === "upload") && background?.bg) {
if (background?.bgType === "image") {
// To not set for Default Images as they have relative URL & are not from Unsplash
if (!background?.bg.startsWith("/")) {
setAuthorDetailsForUnsplash({
authorName: new URL(background?.bg!).searchParams.get("authorName") || "",
authorURL: new URL(background?.bg!).searchParams.get("authorLink") || "",
});
} else {
setAuthorDetailsForUnsplash({ authorName: "", authorURL: "" });
}
}
// For images, we create a new Image object to listen for the 'load' event
const img = new Image();
img.onload = () => setBackgroundLoaded(true);
@@ -98,10 +111,40 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
</video>
);
case "image":
return (
<>
<div
className={`${baseClasses} ${loadedClass} bg-cover bg-center`}
style={{ backgroundImage: `url(${background?.bg})`, filter: `${filterStyle}` }}></div>
<div className={`absolute bottom-6 z-10 h-12 w-full lg:bottom-0`}>
<div className="mx-auto max-w-full p-3 text-center text-xs text-slate-400 lg:text-right">
{authorDetailsForUnsplash.authorName && (
<div className="ml-auto w-max">
<span>Photo by </span>
<Link
href={authorDetailsForUnsplash.authorURL + "?utm_source=formbricks&utm_medium=referral"}
target="_blank"
className="hover:underline">
{authorDetailsForUnsplash.authorName}
</Link>
<span> on </span>
<Link
href="https://unsplash.com/?utm_source=formbricks&utm_medium=referral"
target="_blank"
className="hover:underline">
Unsplash
</Link>
</div>
)}
</div>
</div>
</>
);
case "upload":
return (
<div
className={`${baseClasses} ${loadedClass} bg-cover bg-center`}
style={{ backgroundImage: `url(${background?.bg})`, filter: `${filterStyle}` }}
style={{ backgroundImage: `url(${survey.styling?.background?.bg})`, filter: `${filterStyle}` }}
/>
);
default:

View File

@@ -53,6 +53,10 @@ const nextConfig = {
protocol: "https",
hostname: "formbricks-cdn.s3.eu-central-1.amazonaws.com",
},
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
async rewrites() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -182,5 +182,6 @@ export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1";
export const CUSTOMER_IO_SITE_ID = env.CUSTOMER_IO_SITE_ID;
export const CUSTOMER_IO_API_KEY = env.CUSTOMER_IO_API_KEY;
export const UNSPLASH_ACCESS_KEY = env.UNSPLASH_ACCESS_KEY;
export const STRIPE_API_VERSION = "2024-04-10";

View File

@@ -86,6 +86,7 @@ export const env = createEnv({
UPLOADS_DIR: z.string().min(1).optional(),
VERCEL_URL: z.string().optional(),
WEBAPP_URL: z.string().url().optional(),
UNSPLASH_ACCESS_KEY: z.string().optional(),
},
/*
@@ -185,5 +186,6 @@ export const env = createEnv({
UPLOADS_DIR: process.env.UPLOADS_DIR,
VERCEL_URL: process.env.VERCEL_URL,
WEBAPP_URL: process.env.WEBAPP_URL,
UNSPLASH_ACCESS_KEY: process.env.UNSPLASH_ACCESS_KEY,
},
});

View File

@@ -18,7 +18,7 @@ export const ZCardArrangement = z.object({
export const ZSurveyStylingBackground = z.object({
bg: z.string().nullish(),
bgType: z.enum(["animation", "color", "image"]).nullish(),
bgType: z.enum(["animation", "color", "image", "upload"]).nullish(),
brightness: z.number().nullish(),
});

View File

@@ -69,7 +69,7 @@ export const ZSurveyProductOverwrites = z.object({
export type TSurveyProductOverwrites = z.infer<typeof ZSurveyProductOverwrites>;
export const ZSurveyBackgroundBgType = z.enum(["animation", "color", "image"]);
export const ZSurveyBackgroundBgType = z.enum(["animation", "color", "upload", "image"]);
export type TSurveyBackgroundBgType = z.infer<typeof ZSurveyBackgroundBgType>;

View File

@@ -146,7 +146,8 @@
"UPLOADS_DIR",
"VERCEL",
"VERCEL_URL",
"WEBAPP_URL"
"WEBAPP_URL",
"UNSPLASH_ACCESS_KEY"
]
},
"build:dev": {