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>
@@ -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:
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 '{query}'
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() {
|
||||
|
||||
BIN
apps/web/public/image-backgrounds/confetti.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
apps/web/public/image-backgrounds/dog-2.webp
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
apps/web/public/image-backgrounds/dogs.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/web/public/image-backgrounds/flowers.webp
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
apps/web/public/image-backgrounds/kitten-2.webp
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
apps/web/public/image-backgrounds/kittens.webp
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
apps/web/public/image-backgrounds/lolipop.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/web/public/image-backgrounds/oranges.webp
Normal file
|
After Width: | Height: | Size: 298 KiB |
BIN
apps/web/public/image-backgrounds/pencil.webp
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
apps/web/public/image-backgrounds/plant.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
apps/web/public/image-backgrounds/shapes.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/web/public/image-backgrounds/supermario.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
apps/web/public/image-backgrounds/waves.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -146,7 +146,8 @@
|
||||
"UPLOADS_DIR",
|
||||
"VERCEL",
|
||||
"VERCEL_URL",
|
||||
"WEBAPP_URL"
|
||||
"WEBAPP_URL",
|
||||
"UNSPLASH_ACCESS_KEY"
|
||||
]
|
||||
},
|
||||
"build:dev": {
|
||||
|
||||