feat: background settings (color, blur)
This commit is contained in:
184
apps/web/src/components/background-settings.tsx
Normal file
184
apps/web/src/components/background-settings.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { BackgroundIcon } from "./icons";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { colors } from "@/data/colors";
|
||||||
|
import { useProjectStore } from "@/stores/project-store";
|
||||||
|
import { PipetteIcon } from "lucide-react";
|
||||||
|
|
||||||
|
type BackgroundTab = "color" | "blur";
|
||||||
|
|
||||||
|
export function BackgroundSettings() {
|
||||||
|
const { activeProject, updateBackgroundType } = useProjectStore();
|
||||||
|
|
||||||
|
// ✅ Good: derive activeTab from activeProject during rendering
|
||||||
|
const activeTab = activeProject?.backgroundType || "color";
|
||||||
|
|
||||||
|
const handleColorSelect = (color: string) => {
|
||||||
|
updateBackgroundType("color", { backgroundColor: color });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlurSelect = (blurIntensity: number) => {
|
||||||
|
updateBackgroundType("blur", { blurIntensity });
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
label: "Color",
|
||||||
|
value: "color",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Blur",
|
||||||
|
value: "blur",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="icon"
|
||||||
|
className="!size-5 border border-muted-foreground"
|
||||||
|
>
|
||||||
|
<BackgroundIcon className="!size-4" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="flex flex-col items-start w-[20rem] h-[16rem] overflow-hidden p-0">
|
||||||
|
<div className="flex items-center justify-between w-full gap-2 z-10 bg-popover p-3">
|
||||||
|
<h2 className="text-sm">Background</h2>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<span
|
||||||
|
key={tab.value}
|
||||||
|
onClick={() => {
|
||||||
|
// Switch to the background type when clicking tabs
|
||||||
|
if (tab.value === "color") {
|
||||||
|
updateBackgroundType("color", {
|
||||||
|
backgroundColor:
|
||||||
|
activeProject?.backgroundColor || "#000000",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateBackgroundType("blur", {
|
||||||
|
blurIntensity: activeProject?.blurIntensity || 8,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground cursor-pointer",
|
||||||
|
activeTab === tab.value && "text-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{activeTab === "color" ? (
|
||||||
|
<ColorView
|
||||||
|
selectedColor={activeProject?.backgroundColor || "#000000"}
|
||||||
|
onColorSelect={handleColorSelect}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<BlurView
|
||||||
|
selectedBlur={activeProject?.blurIntensity || 8}
|
||||||
|
onBlurSelect={handleBlurSelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ColorView({
|
||||||
|
selectedColor,
|
||||||
|
onColorSelect,
|
||||||
|
}: {
|
||||||
|
selectedColor: string;
|
||||||
|
onColorSelect: (color: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full">
|
||||||
|
<div className="absolute top-8 left-0 w-[calc(100%-1rem)] h-12 bg-gradient-to-b from-popover to-transparent pointer-events-none"></div>
|
||||||
|
<div className="grid grid-cols-4 gap-2 w-full h-full p-3 pt-0 overflow-auto">
|
||||||
|
<div className="w-full aspect-square rounded-sm cursor-pointer border border-foreground/15 hover:border-primary flex items-center justify-center">
|
||||||
|
<PipetteIcon className="size-4" />
|
||||||
|
</div>
|
||||||
|
{colors.map((color) => (
|
||||||
|
<ColorItem
|
||||||
|
key={color}
|
||||||
|
color={color}
|
||||||
|
isSelected={color === selectedColor}
|
||||||
|
onClick={() => onColorSelect(color)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ColorItem({
|
||||||
|
color,
|
||||||
|
isSelected,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
color: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-full aspect-square rounded-sm cursor-pointer hover:border-2 hover:border-primary",
|
||||||
|
isSelected && "border-2 border-primary"
|
||||||
|
)}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlurView({
|
||||||
|
selectedBlur,
|
||||||
|
onBlurSelect,
|
||||||
|
}: {
|
||||||
|
selectedBlur: number;
|
||||||
|
onBlurSelect: (blurIntensity: number) => void;
|
||||||
|
}) {
|
||||||
|
const blurLevels = [
|
||||||
|
{ label: "Light", value: 4 },
|
||||||
|
{ label: "Medium", value: 8 },
|
||||||
|
{ label: "Heavy", value: 18 },
|
||||||
|
];
|
||||||
|
const blurImage =
|
||||||
|
"https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-3 gap-2 w-full p-3 pt-0">
|
||||||
|
{blurLevels.map((blur) => (
|
||||||
|
<div
|
||||||
|
key={blur.value}
|
||||||
|
className={cn(
|
||||||
|
"w-full aspect-square rounded-sm cursor-pointer hover:border-2 hover:border-primary relative overflow-hidden",
|
||||||
|
selectedBlur === blur.value && "border-2 border-primary"
|
||||||
|
)}
|
||||||
|
onClick={() => onBlurSelect(blur.value)}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={blurImage}
|
||||||
|
alt={`Blur preview ${blur.label}`}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
style={{ filter: `blur(${blur.value}px)` }}
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-1 left-1 right-1 text-center">
|
||||||
|
<span className="text-xs text-white bg-black/50 px-1 rounded">
|
||||||
|
{blur.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -21,6 +21,8 @@ import { useState, useRef, useEffect } from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { formatTimeCode } from "@/lib/time";
|
import { formatTimeCode } from "@/lib/time";
|
||||||
import { FONT_CLASS_MAP } from "@/lib/font-config";
|
import { FONT_CLASS_MAP } from "@/lib/font-config";
|
||||||
|
import { BackgroundSettings } from "../background-settings";
|
||||||
|
import { useProjectStore } from "@/stores/project-store";
|
||||||
|
|
||||||
interface ActiveElement {
|
interface ActiveElement {
|
||||||
element: TimelineElement;
|
element: TimelineElement;
|
||||||
@ -39,6 +41,7 @@ export function PreviewPanel() {
|
|||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
});
|
});
|
||||||
|
const { activeProject } = useProjectStore();
|
||||||
|
|
||||||
// Calculate optimal preview size that fits in container while maintaining aspect ratio
|
// Calculate optimal preview size that fits in container while maintaining aspect ratio
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -136,6 +139,85 @@ export function PreviewPanel() {
|
|||||||
// Check if there are any elements in the timeline at all
|
// Check if there are any elements in the timeline at all
|
||||||
const hasAnyElements = tracks.some((track) => track.elements.length > 0);
|
const hasAnyElements = tracks.some((track) => track.elements.length > 0);
|
||||||
|
|
||||||
|
// Get media elements for blur background (video/image only)
|
||||||
|
const getBlurBackgroundElements = (): ActiveElement[] => {
|
||||||
|
return activeElements.filter(
|
||||||
|
({ element, mediaItem }) =>
|
||||||
|
element.type === "media" &&
|
||||||
|
mediaItem &&
|
||||||
|
(mediaItem.type === "video" || mediaItem.type === "image") &&
|
||||||
|
element.mediaId !== "test" // Exclude test elements
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const blurBackgroundElements = getBlurBackgroundElements();
|
||||||
|
|
||||||
|
// Render blur background layer
|
||||||
|
const renderBlurBackground = () => {
|
||||||
|
if (
|
||||||
|
!activeProject?.backgroundType ||
|
||||||
|
activeProject.backgroundType !== "blur" ||
|
||||||
|
blurBackgroundElements.length === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first media element for background (could be enhanced to use primary/focused element)
|
||||||
|
const backgroundElement = blurBackgroundElements[0];
|
||||||
|
const { element, mediaItem } = backgroundElement;
|
||||||
|
|
||||||
|
if (!mediaItem) return null;
|
||||||
|
|
||||||
|
const blurIntensity = activeProject.blurIntensity || 8;
|
||||||
|
|
||||||
|
if (mediaItem.type === "video") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`blur-${element.id}`}
|
||||||
|
className="absolute inset-0 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
filter: `blur(${blurIntensity}px)`,
|
||||||
|
transform: "scale(1.1)", // Slightly zoom to avoid blur edge artifacts
|
||||||
|
transformOrigin: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VideoPlayer
|
||||||
|
src={mediaItem.url!}
|
||||||
|
poster={mediaItem.thumbnailUrl}
|
||||||
|
clipStartTime={element.startTime}
|
||||||
|
trimStart={element.trimStart}
|
||||||
|
trimEnd={element.trimEnd}
|
||||||
|
clipDuration={element.duration}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaItem.type === "image") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`blur-${element.id}`}
|
||||||
|
className="absolute inset-0 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
filter: `blur(${blurIntensity}px)`,
|
||||||
|
transform: "scale(1.1)", // Slightly zoom to avoid blur edge artifacts
|
||||||
|
transformOrigin: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={mediaItem.url!}
|
||||||
|
alt={mediaItem.name}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// Render an element
|
// Render an element
|
||||||
const renderElement = (elementData: ActiveElement, index: number) => {
|
const renderElement = (elementData: ActiveElement, index: number) => {
|
||||||
const { element, mediaItem } = elementData;
|
const { element, mediaItem } = elementData;
|
||||||
@ -265,12 +347,17 @@ export function PreviewPanel() {
|
|||||||
{hasAnyElements ? (
|
{hasAnyElements ? (
|
||||||
<div
|
<div
|
||||||
ref={previewRef}
|
ref={previewRef}
|
||||||
className="relative overflow-hidden rounded-sm bg-black border"
|
className="relative overflow-hidden rounded-sm border"
|
||||||
style={{
|
style={{
|
||||||
width: previewDimensions.width,
|
width: previewDimensions.width,
|
||||||
height: previewDimensions.height,
|
height: previewDimensions.height,
|
||||||
|
backgroundColor:
|
||||||
|
activeProject?.backgroundType === "blur"
|
||||||
|
? "transparent"
|
||||||
|
: activeProject?.backgroundColor || "#000000",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{renderBlurBackground()}
|
||||||
{activeElements.length === 0 ? (
|
{activeElements.length === 0 ? (
|
||||||
<div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
|
<div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
|
||||||
No elements at current time
|
No elements at current time
|
||||||
@ -280,6 +367,14 @@ export function PreviewPanel() {
|
|||||||
renderElement(elementData, index)
|
renderElement(elementData, index)
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
{/* Show message when blur is selected but no media available */}
|
||||||
|
{activeProject?.backgroundType === "blur" &&
|
||||||
|
blurBackgroundElements.length === 0 &&
|
||||||
|
activeElements.length > 0 && (
|
||||||
|
<div className="absolute bottom-2 left-2 right-2 bg-black/70 text-white text-xs p-2 rounded">
|
||||||
|
Add a video or image to use blur background
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -346,7 +441,8 @@ function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) {
|
|||||||
<Play className="h-3 w-3" />
|
<Play className="h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div className="flex items-center gap-3">
|
||||||
|
<BackgroundSettings />
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
@ -37,3 +37,64 @@ export function GithubIcon({ className }: { className?: string }) {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function BackgroundIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="353"
|
||||||
|
height="353"
|
||||||
|
viewBox="0 0 353 353"
|
||||||
|
fill="none"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<g clipPath="url(#clip0_1_3)">
|
||||||
|
<rect
|
||||||
|
x="-241.816"
|
||||||
|
y="233.387"
|
||||||
|
width="592.187"
|
||||||
|
height="17.765"
|
||||||
|
transform="rotate(-37 -241.816 233.387)"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="-189.907"
|
||||||
|
y="306.804"
|
||||||
|
width="592.187"
|
||||||
|
height="17.765"
|
||||||
|
transform="rotate(-37 -189.907 306.804)"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="-146.928"
|
||||||
|
y="389.501"
|
||||||
|
width="592.187"
|
||||||
|
height="17.765"
|
||||||
|
transform="rotate(-37 -146.928 389.501)"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="-103.144"
|
||||||
|
y="477.904"
|
||||||
|
width="592.187"
|
||||||
|
height="17.765"
|
||||||
|
transform="rotate(-37 -103.144 477.904)"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="-57.169"
|
||||||
|
y="570.714"
|
||||||
|
width="592.187"
|
||||||
|
height="17.765"
|
||||||
|
transform="rotate(-37 -57.169 570.714)"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1_3">
|
||||||
|
<rect width="353" height="353" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
243
apps/web/src/data/colors.ts
Normal file
243
apps/web/src/data/colors.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
export const colors = [
|
||||||
|
"#fef2f2",
|
||||||
|
"#ffe2e2",
|
||||||
|
"#ffc9c9",
|
||||||
|
"#ffa2a2",
|
||||||
|
"#ff6467",
|
||||||
|
"#fb2c36",
|
||||||
|
"#e7000b",
|
||||||
|
"#c10007",
|
||||||
|
"#9f0712",
|
||||||
|
"#82181a",
|
||||||
|
"#460809",
|
||||||
|
"#fff7ed",
|
||||||
|
"#ffedd4",
|
||||||
|
"#ffd6a7",
|
||||||
|
"#ffb86a",
|
||||||
|
"#ff8904",
|
||||||
|
"#ff6900",
|
||||||
|
"#f54900",
|
||||||
|
"#ca3500",
|
||||||
|
"#9f2d00",
|
||||||
|
"#7e2a0c",
|
||||||
|
"#441306",
|
||||||
|
"#fffbeb",
|
||||||
|
"#fef3c6",
|
||||||
|
"#fee685",
|
||||||
|
"#ffd230",
|
||||||
|
"#ffb900",
|
||||||
|
"#fe9a00",
|
||||||
|
"#e17100",
|
||||||
|
"#bb4d00",
|
||||||
|
"#973c00",
|
||||||
|
"#7b3306",
|
||||||
|
"#461901",
|
||||||
|
"#fefce8",
|
||||||
|
"#fef9c2",
|
||||||
|
"#fff085",
|
||||||
|
"#ffdf20",
|
||||||
|
"#fdc700",
|
||||||
|
"#f0b100",
|
||||||
|
"#d08700",
|
||||||
|
"#a65f00",
|
||||||
|
"#894b00",
|
||||||
|
"#733e0a",
|
||||||
|
"#432004",
|
||||||
|
"#f7fee7",
|
||||||
|
"#ecfcca",
|
||||||
|
"#d8f999",
|
||||||
|
"#bbf451",
|
||||||
|
"#9ae600",
|
||||||
|
"#7ccf00",
|
||||||
|
"#5ea500",
|
||||||
|
"#497d00",
|
||||||
|
"#3c6300",
|
||||||
|
"#35530e",
|
||||||
|
"#192e03",
|
||||||
|
"#f0fdf4",
|
||||||
|
"#dcfce7",
|
||||||
|
"#b9f8cf",
|
||||||
|
"#7bf1a8",
|
||||||
|
"#05df72",
|
||||||
|
"#00c950",
|
||||||
|
"#00a63e",
|
||||||
|
"#008236",
|
||||||
|
"#016630",
|
||||||
|
"#0d542b",
|
||||||
|
"#032e15",
|
||||||
|
"#ecfdf5",
|
||||||
|
"#d0fae5",
|
||||||
|
"#a4f4cf",
|
||||||
|
"#5ee9b5",
|
||||||
|
"#00d492",
|
||||||
|
"#00bc7d",
|
||||||
|
"#009966",
|
||||||
|
"#007a55",
|
||||||
|
"#006045",
|
||||||
|
"#004f3b",
|
||||||
|
"#002c22",
|
||||||
|
"#f0fdfa",
|
||||||
|
"#cbfbf1",
|
||||||
|
"#96f7e4",
|
||||||
|
"#46ecd5",
|
||||||
|
"#00d5be",
|
||||||
|
"#00bba7",
|
||||||
|
"#009689",
|
||||||
|
"#00786f",
|
||||||
|
"#005f5a",
|
||||||
|
"#0b4f4a",
|
||||||
|
"#022f2e",
|
||||||
|
"#ecfeff",
|
||||||
|
"#cefafe",
|
||||||
|
"#a2f4fd",
|
||||||
|
"#53eafd",
|
||||||
|
"#00d3f2",
|
||||||
|
"#00b8db",
|
||||||
|
"#0092b8",
|
||||||
|
"#007595",
|
||||||
|
"#005f78",
|
||||||
|
"#104e64",
|
||||||
|
"#053345",
|
||||||
|
"#f0f9ff",
|
||||||
|
"#dff2fe",
|
||||||
|
"#b8e6fe",
|
||||||
|
"#74d4ff",
|
||||||
|
"#00bcff",
|
||||||
|
"#00a6f4",
|
||||||
|
"#0084d1",
|
||||||
|
"#0069a8",
|
||||||
|
"#00598a",
|
||||||
|
"#024a70",
|
||||||
|
"#052f4a",
|
||||||
|
"#eff6ff",
|
||||||
|
"#dbeafe",
|
||||||
|
"#bedbff",
|
||||||
|
"#8ec5ff",
|
||||||
|
"#51a2ff",
|
||||||
|
"#2b7fff",
|
||||||
|
"#155dfc",
|
||||||
|
"#1447e6",
|
||||||
|
"#193cb8",
|
||||||
|
"#1c398e",
|
||||||
|
"#162456",
|
||||||
|
"#eef2ff",
|
||||||
|
"#e0e7ff",
|
||||||
|
"#c6d2ff",
|
||||||
|
"#a3b3ff",
|
||||||
|
"#7c86ff",
|
||||||
|
"#615fff",
|
||||||
|
"#4f39f6",
|
||||||
|
"#432dd7",
|
||||||
|
"#372aac",
|
||||||
|
"#312c85",
|
||||||
|
"#1e1a4d",
|
||||||
|
"#f5f3ff",
|
||||||
|
"#ede9fe",
|
||||||
|
"#ddd6ff",
|
||||||
|
"#c4b4ff",
|
||||||
|
"#a684ff",
|
||||||
|
"#8e51ff",
|
||||||
|
"#7f22fe",
|
||||||
|
"#7008e7",
|
||||||
|
"#5d0ec0",
|
||||||
|
"#4d179a",
|
||||||
|
"#2f0d68",
|
||||||
|
"#faf5ff",
|
||||||
|
"#f3e8ff",
|
||||||
|
"#e9d4ff",
|
||||||
|
"#dab2ff",
|
||||||
|
"#c27aff",
|
||||||
|
"#ad46ff",
|
||||||
|
"#9810fa",
|
||||||
|
"#8200db",
|
||||||
|
"#6e11b0",
|
||||||
|
"#59168b",
|
||||||
|
"#3c0366",
|
||||||
|
"#fdf4ff",
|
||||||
|
"#fae8ff",
|
||||||
|
"#f6cfff",
|
||||||
|
"#f4a8ff",
|
||||||
|
"#ed6aff",
|
||||||
|
"#e12afb",
|
||||||
|
"#c800de",
|
||||||
|
"#a800b7",
|
||||||
|
"#8a0194",
|
||||||
|
"#721378",
|
||||||
|
"#4b004f",
|
||||||
|
"#fdf2f8",
|
||||||
|
"#fce7f3",
|
||||||
|
"#fccee8",
|
||||||
|
"#fda5d5",
|
||||||
|
"#fb64b6",
|
||||||
|
"#f6339a",
|
||||||
|
"#e60076",
|
||||||
|
"#c6005c",
|
||||||
|
"#a3004c",
|
||||||
|
"#861043",
|
||||||
|
"#510424",
|
||||||
|
"#fff1f2",
|
||||||
|
"#ffe4e6",
|
||||||
|
"#ffccd3",
|
||||||
|
"#ffa1ad",
|
||||||
|
"#ff637e",
|
||||||
|
"#ff2056",
|
||||||
|
"#ec003f",
|
||||||
|
"#c70036",
|
||||||
|
"#a50036",
|
||||||
|
"#8b0836",
|
||||||
|
"#4d0218",
|
||||||
|
"#f8fafc",
|
||||||
|
"#f1f5f9",
|
||||||
|
"#e2e8f0",
|
||||||
|
"#cad5e2",
|
||||||
|
"#90a1b9",
|
||||||
|
"#62748e",
|
||||||
|
"#45556c",
|
||||||
|
"#314158",
|
||||||
|
"#1d293d",
|
||||||
|
"#0f172b",
|
||||||
|
"#020618",
|
||||||
|
"#f9fafb",
|
||||||
|
"#f3f4f6",
|
||||||
|
"#e5e7eb",
|
||||||
|
"#d1d5dc",
|
||||||
|
"#99a1af",
|
||||||
|
"#6a7282",
|
||||||
|
"#4a5565",
|
||||||
|
"#364153",
|
||||||
|
"#1e2939",
|
||||||
|
"#101828",
|
||||||
|
"#030712",
|
||||||
|
"#fafafa",
|
||||||
|
"#f4f4f5",
|
||||||
|
"#e4e4e7",
|
||||||
|
"#d4d4d8",
|
||||||
|
"#9f9fa9",
|
||||||
|
"#71717b",
|
||||||
|
"#52525c",
|
||||||
|
"#3f3f46",
|
||||||
|
"#27272a",
|
||||||
|
"#18181b",
|
||||||
|
"#09090b",
|
||||||
|
"#f5f5f5",
|
||||||
|
"#e5e5e5",
|
||||||
|
"#d4d4d4",
|
||||||
|
"#a1a1a1",
|
||||||
|
"#737373",
|
||||||
|
"#525252",
|
||||||
|
"#404040",
|
||||||
|
"#262626",
|
||||||
|
"#171717",
|
||||||
|
"#0a0a0a",
|
||||||
|
"#fafaf9",
|
||||||
|
"#f5f5f4",
|
||||||
|
"#e7e5e4",
|
||||||
|
"#d6d3d1",
|
||||||
|
"#a6a09b",
|
||||||
|
"#79716b",
|
||||||
|
"#57534d",
|
||||||
|
"#44403b",
|
||||||
|
"#292524",
|
||||||
|
"#1c1917",
|
||||||
|
"#0c0a09",
|
||||||
|
];
|
@ -60,6 +60,9 @@ class StorageService {
|
|||||||
thumbnail: project.thumbnail,
|
thumbnail: project.thumbnail,
|
||||||
createdAt: project.createdAt.toISOString(),
|
createdAt: project.createdAt.toISOString(),
|
||||||
updatedAt: project.updatedAt.toISOString(),
|
updatedAt: project.updatedAt.toISOString(),
|
||||||
|
backgroundColor: project.backgroundColor,
|
||||||
|
backgroundType: project.backgroundType,
|
||||||
|
blurIntensity: project.blurIntensity,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.projectsAdapter.set(project.id, serializedProject);
|
await this.projectsAdapter.set(project.id, serializedProject);
|
||||||
@ -77,6 +80,9 @@ class StorageService {
|
|||||||
thumbnail: serializedProject.thumbnail,
|
thumbnail: serializedProject.thumbnail,
|
||||||
createdAt: new Date(serializedProject.createdAt),
|
createdAt: new Date(serializedProject.createdAt),
|
||||||
updatedAt: new Date(serializedProject.updatedAt),
|
updatedAt: new Date(serializedProject.updatedAt),
|
||||||
|
backgroundColor: serializedProject.backgroundColor,
|
||||||
|
backgroundType: serializedProject.backgroundType,
|
||||||
|
blurIntensity: serializedProject.blurIntensity,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,11 @@ interface ProjectStore {
|
|||||||
closeProject: () => void;
|
closeProject: () => void;
|
||||||
renameProject: (projectId: string, name: string) => Promise<void>;
|
renameProject: (projectId: string, name: string) => Promise<void>;
|
||||||
duplicateProject: (projectId: string) => Promise<string>;
|
duplicateProject: (projectId: string) => Promise<string>;
|
||||||
|
updateProjectBackground: (backgroundColor: string) => Promise<void>;
|
||||||
|
updateBackgroundType: (
|
||||||
|
type: "color" | "blur",
|
||||||
|
options?: { backgroundColor?: string; blurIntensity?: number }
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useProjectStore = create<ProjectStore>((set, get) => ({
|
export const useProjectStore = create<ProjectStore>((set, get) => ({
|
||||||
@ -35,6 +40,9 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
thumbnail: "",
|
thumbnail: "",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
backgroundType: "color",
|
||||||
|
blurIntensity: 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
set({ activeProject: newProject });
|
set({ activeProject: newProject });
|
||||||
@ -234,4 +242,55 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateProjectBackground: async (backgroundColor: string) => {
|
||||||
|
const { activeProject } = get();
|
||||||
|
if (!activeProject) return;
|
||||||
|
|
||||||
|
const updatedProject = {
|
||||||
|
...activeProject,
|
||||||
|
backgroundColor,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await storageService.saveProject(updatedProject);
|
||||||
|
set({ activeProject: updatedProject });
|
||||||
|
await get().loadAllProjects(); // Refresh the list
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update project background:", error);
|
||||||
|
toast.error("Failed to update background", {
|
||||||
|
description: "Please try again",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBackgroundType: async (
|
||||||
|
type: "color" | "blur",
|
||||||
|
options?: { backgroundColor?: string; blurIntensity?: number }
|
||||||
|
) => {
|
||||||
|
const { activeProject } = get();
|
||||||
|
if (!activeProject) return;
|
||||||
|
|
||||||
|
const updatedProject = {
|
||||||
|
...activeProject,
|
||||||
|
backgroundType: type,
|
||||||
|
...(options?.backgroundColor && {
|
||||||
|
backgroundColor: options.backgroundColor,
|
||||||
|
}),
|
||||||
|
...(options?.blurIntensity && { blurIntensity: options.blurIntensity }),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await storageService.saveProject(updatedProject);
|
||||||
|
set({ activeProject: updatedProject });
|
||||||
|
await get().loadAllProjects(); // Refresh the list
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update background type:", error);
|
||||||
|
toast.error("Failed to update background", {
|
||||||
|
description: "Please try again",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -5,4 +5,7 @@ export interface TProject {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
mediaItems?: string[];
|
mediaItems?: string[];
|
||||||
|
backgroundColor?: string;
|
||||||
|
backgroundType?: "color" | "blur";
|
||||||
|
blurIntensity?: number; // in pixels (4, 8, 18)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user