mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-30 12:36:26 +00:00
vercel json add
This commit is contained in:
@@ -34,7 +34,7 @@ export default function LayerItem({ item }: { item: StageItem }) {
|
||||
style={style}
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
className="flex items-center justify-between rounded-md border bg-yellow-50 p-2 text-black"
|
||||
className="flex items-center justify-between rounded-md border p-2 text-black"
|
||||
>
|
||||
<IconButton {...listeners}>
|
||||
<GripVertical size={18} />
|
||||
|
||||
@@ -5,12 +5,9 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import axios from "axios";
|
||||
import * as process from "process";
|
||||
import { LuLayoutTemplate } from "react-icons/lu";
|
||||
import Image from "next/image";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export const ImageGallery = () => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
@@ -11,12 +11,15 @@ import * as process from "process";
|
||||
import Image from "next/image";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { addImage } from "@/store/app.slice";
|
||||
import { models } from "@/components/layout/sidebar/stage-settings/stage-size-selector/models";
|
||||
import { useAppDispatch } from "@/hooks";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
export const ImageGallery = ({ open }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
@@ -41,7 +44,6 @@ export const ImageGallery = ({ open }: Props) => {
|
||||
|
||||
if (query) {
|
||||
url = `${searchUrl}${clientID}${urlPage}${urlQuery}`;
|
||||
console.log(url);
|
||||
} else {
|
||||
url = `${mainUrl}${clientID}${urlPage}`;
|
||||
}
|
||||
@@ -55,7 +57,6 @@ export const ImageGallery = ({ open }: Props) => {
|
||||
} else if (query) {
|
||||
return [...oldPhotos, ...data.results];
|
||||
} else {
|
||||
console.log(data);
|
||||
return [...oldPhotos, ...data];
|
||||
}
|
||||
});
|
||||
@@ -70,10 +71,10 @@ export const ImageGallery = ({ open }: Props) => {
|
||||
const ref = scrollAreaRef.current;
|
||||
const handler = () => {
|
||||
if (scrollAreaRef.current) {
|
||||
console.log(
|
||||
/* console.log(
|
||||
scrollAreaRef.current.clientHeight + scrollAreaRef.current.scrollTop,
|
||||
scrollAreaRef.current.scrollHeight - 2,
|
||||
);
|
||||
);*/
|
||||
}
|
||||
if (
|
||||
isLoading ||
|
||||
@@ -104,16 +105,17 @@ export const ImageGallery = ({ open }: Props) => {
|
||||
return () => ref?.removeEventListener("scroll", handler);
|
||||
}, [isLoading, open]);
|
||||
|
||||
const handleImageAdd = (e) => {
|
||||
console.log(e.target);
|
||||
const handleImageAdd = (url: string, width: number, height: number) => {
|
||||
const aspectRatio = height / width;
|
||||
const finalWidth = 200;
|
||||
const finalHeight = finalWidth * aspectRatio;
|
||||
dispatch(
|
||||
addImage({ imageUrl: url, width: finalWidth, height: finalHeight }),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="min-h-full p-3">
|
||||
<CardHeader className="mb-2 p-2 text-center">
|
||||
<CardTitle>Template</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<Card className="h-full min-h-[600px] p-3">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search Images"
|
||||
@@ -122,24 +124,27 @@ export const ImageGallery = ({ open }: Props) => {
|
||||
/>
|
||||
|
||||
<div
|
||||
className="mt-3 max-h-64 w-full grid-cols-2 justify-center overflow-auto"
|
||||
className="mt-3 grid max-h-[550px] w-full grid-cols-2 justify-center gap-2 overflow-auto "
|
||||
ref={scrollAreaRef}
|
||||
>
|
||||
{photos?.map((p, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick={(e) => handleImageAdd(e)}
|
||||
className="rounded-md border-2 border-muted bg-popover p-2 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<Image
|
||||
key={p.i}
|
||||
src={p.urls.small}
|
||||
alt={"image"}
|
||||
height={100}
|
||||
width={100}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{photos?.map((p, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => handleImageAdd(p.urls.raw, p.width, p.height)}
|
||||
className=" rounded-md border-muted bg-popover p-2 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<Image
|
||||
key={p.i}
|
||||
src={p.urls.small}
|
||||
alt={"image"}
|
||||
height={100}
|
||||
width={100}
|
||||
className="h-32 w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -28,9 +28,6 @@ export const Layers = () => {
|
||||
|
||||
return (
|
||||
<Card className="p-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center">Layers</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid items-center gap-2 p-2 ">
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import React, { useState } from "react";
|
||||
import { LuLayoutTemplate } from "react-icons/lu";
|
||||
import {
|
||||
ColumnsIcon,
|
||||
GearIcon,
|
||||
ImageIcon,
|
||||
LayersIcon,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
} from "@radix-ui/react-icons";
|
||||
import { StageSize } from "@/components/layout/sidebar/stage-settings/stage-size";
|
||||
import { StageBackground } from "@/components/layout/sidebar/stage-settings/stage-background";
|
||||
import { StageBackgroundChange } from "@/components/layout/sidebar/stage-settings/stage-background-change/stage-background-change";
|
||||
|
||||
export function Sidebar() {
|
||||
const [openGallery, setOpenGallery] = useState<boolean>(false);
|
||||
@@ -20,7 +22,7 @@ export function Sidebar() {
|
||||
return (
|
||||
<div className="items-left flex w-full flex-col p-2 pt-4">
|
||||
<Tabs>
|
||||
<TabsList className="grid grid-cols-5">
|
||||
<TabsList className="grid grid-cols-6">
|
||||
<TabsTrigger value="stage">
|
||||
<GearIcon />
|
||||
</TabsTrigger>
|
||||
@@ -41,6 +43,9 @@ export function Sidebar() {
|
||||
<TabsTrigger value="layers">
|
||||
<LayersIcon />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="background">
|
||||
<ColumnsIcon />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="stage">
|
||||
<StageSize />
|
||||
@@ -58,6 +63,9 @@ export function Sidebar() {
|
||||
<TabsContent value="layers">
|
||||
<Layers />
|
||||
</TabsContent>
|
||||
<TabsContent value="background">
|
||||
<StageBackgroundChange />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import React, { type ElementRef, useEffect, useRef, useState } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import axios from "axios";
|
||||
import * as process from "process";
|
||||
import Image from "next/image";
|
||||
import { selectBackground, setBackgroundImage } from "@/store/app.slice";
|
||||
import { useAppDispatch, useAppSelector } from "@/hooks";
|
||||
import { SliderPicker } from "react-color";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
export const StageBackgroundChange = ({ open }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { stage } = useAppSelector((state) => state.app.history[0]);
|
||||
const [query, setQuery] = useState<string>("gradient");
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [photos, setPhotos] = useState<any[]>([]);
|
||||
const scrollAreaRef = useRef<ElementRef<"div">>(null);
|
||||
const mainUrl = "https://api.unsplash.com/photos";
|
||||
const searchUrl = "https://api.unsplash.com/search/photos";
|
||||
const clientID = `?client_id=${process.env.NEXT_PUBLIC_ACCESS_KEY}`;
|
||||
const bgColor = useAppSelector((state) => state.app.backgroundColor);
|
||||
|
||||
useEffect(() => {
|
||||
fetchImages();
|
||||
}, [query, page]);
|
||||
|
||||
const fetchImages = async () => {
|
||||
setIsLoading(true);
|
||||
let url;
|
||||
const urlPage = `&page=${page}`;
|
||||
const urlQuery = `&query=${query}`;
|
||||
|
||||
if (query) {
|
||||
url = `${searchUrl}${clientID}${urlPage}${urlQuery}`;
|
||||
} else {
|
||||
url = `${mainUrl}${clientID}${urlPage}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.get(url);
|
||||
|
||||
setPhotos((oldPhotos) => {
|
||||
if (query && page === 1) {
|
||||
return data.results;
|
||||
} else if (query) {
|
||||
return [...oldPhotos, ...data.results];
|
||||
} else {
|
||||
return [...oldPhotos, ...data];
|
||||
}
|
||||
});
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const ref = scrollAreaRef.current;
|
||||
const handler = () => {
|
||||
if (scrollAreaRef.current) {
|
||||
/*console.log(
|
||||
scrollAreaRef.current.clientHeight + scrollAreaRef.current.scrollTop,
|
||||
scrollAreaRef.current.scrollHeight - 2,
|
||||
);*/
|
||||
}
|
||||
if (
|
||||
isLoading ||
|
||||
!scrollAreaRef.current ||
|
||||
scrollAreaRef.current.clientHeight + scrollAreaRef.current.scrollTop <
|
||||
scrollAreaRef.current.scrollHeight - 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPage((oldPage) => {
|
||||
return oldPage + 1;
|
||||
});
|
||||
};
|
||||
|
||||
scrollAreaRef.current?.addEventListener("scroll", handler);
|
||||
return () => ref?.removeEventListener("scroll", handler);
|
||||
}, [isLoading, open]);
|
||||
|
||||
const handleImageAdd = (url: string) => {
|
||||
dispatch(
|
||||
setBackgroundImage({
|
||||
imageUrl: url,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleBackgroundSelect = (bgColor) => {
|
||||
// dispatch(selectBackground(bgColor.hex));
|
||||
|
||||
console.log(bgColor);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="h-full min-h-[600px] p-3">
|
||||
{/*<Input
|
||||
type="text"
|
||||
placeholder="Search Images"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
|
||||
|
||||
*/}
|
||||
|
||||
<div className=" mt-4">
|
||||
{/* <input
|
||||
className="h-[2rem] w-[2rem]"
|
||||
type="color"
|
||||
value={bgColor ?? undefined}
|
||||
onChange={(e) => handleBackgroundSelect(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="h-[2rem] w-[2rem]"
|
||||
type="color"
|
||||
value={bgColor ?? undefined}
|
||||
onChange={(e) => handleBackgroundSelect(e.target.value)}
|
||||
/>*/}
|
||||
<SliderPicker
|
||||
color={bgColor}
|
||||
onChangeComplete={(e) => handleBackgroundSelect(e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="grid max-h-[550px] w-full grid-cols-2 justify-center gap-2 overflow-auto "
|
||||
ref={scrollAreaRef}
|
||||
>
|
||||
{photos?.map((p, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => handleImageAdd(p.urls.raw)}
|
||||
className="rounded-md border-muted bg-popover p-2 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<Image
|
||||
key={p.i}
|
||||
src={p.urls.small}
|
||||
alt={"image"}
|
||||
height={100}
|
||||
width={100}
|
||||
className="h-32 w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import { type PopoverProps } from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMutationObserver } from "@/hooks/use-mutation-observer";
|
||||
import {
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
import { type Model, type ModelType } from "./models";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
|
||||
interface ModelSelectorProps extends PopoverProps {
|
||||
types: readonly ModelType[];
|
||||
models: Model[];
|
||||
}
|
||||
|
||||
export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [selectedModel, setSelectedModel] = React.useState<Model>(models[0]);
|
||||
const [peekedModel, setPeekedModel] = React.useState<Model>(models[0]);
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Label htmlFor="model">Selectionnez la taille d'etiquette</Label>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
align="start"
|
||||
className="w-[260px] bg-yellow-400 text-sm"
|
||||
side="left"
|
||||
>
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusantium
|
||||
alias aut eaque eligendi fugiat harum, id ipsa ipsum maxime molestiae
|
||||
nesciunt nobis odio quisquam quos reprehenderit sapiente suscipit, vel
|
||||
voluptatibus.
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
<Popover open={open} onOpenChange={setOpen} {...props}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
aria-label="Select a model"
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{selectedModel ? selectedModel.name : "Select a model..."}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-[250px] p-0">
|
||||
<HoverCard>
|
||||
<HoverCardContent
|
||||
side="left"
|
||||
align="start"
|
||||
forceMount
|
||||
className="min-h-[500px] min-w-[200px] "
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<h4 className="font-medium leading-none">{peekedModel.name}</h4>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{peekedModel.description}
|
||||
</div>
|
||||
{peekedModel.image ? (
|
||||
<div>
|
||||
<Image
|
||||
src={peekedModel.image}
|
||||
alt={peekedModel.name}
|
||||
width={200}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
<Command loop>
|
||||
<CommandList className="h-[var(--cmdk-list-height)] max-h-[400px]">
|
||||
<HoverCardTrigger />
|
||||
{types.map((type) => (
|
||||
<CommandGroup key={type} heading={type}>
|
||||
{models
|
||||
.filter((model) => model.type === type)
|
||||
.map((model) => (
|
||||
<ModelItem
|
||||
key={model.id}
|
||||
model={model}
|
||||
isSelected={selectedModel?.id === model.id}
|
||||
onPeek={(model) => setPeekedModel(model)}
|
||||
onSelect={() => {
|
||||
setSelectedModel(model);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</HoverCard>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ModelItemProps {
|
||||
model: Model;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
onPeek: (model: Model) => void;
|
||||
}
|
||||
|
||||
function ModelItem({ model, isSelected, onSelect, onPeek }: ModelItemProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useMutationObserver(ref, (mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes") {
|
||||
if (mutation.attributeName === "aria-selected") {
|
||||
onPeek(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={model.id}
|
||||
onSelect={onSelect}
|
||||
ref={ref}
|
||||
className="aria-selected:bg-primary aria-selected:text-primary-foreground"
|
||||
>
|
||||
{model.name}
|
||||
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
isSelected ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export const types = ["GPT-3", "Codex"] as const;
|
||||
|
||||
export type ModelType = (typeof types)[number];
|
||||
|
||||
export interface Model<Type = string> {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
image: string;
|
||||
type: Type;
|
||||
}
|
||||
|
||||
export const models: Model<ModelType>[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Demie (S)",
|
||||
description:
|
||||
"Most capable GPT-3 model. Can do any task the other models can do, often with higher quality, longer output and better instruction-following. Also supports inserting completions within text.",
|
||||
type: "GPT-3",
|
||||
image: "/bottle-images/bouteille-demie.png",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Bouteille (M)",
|
||||
image: "/bottle-images/bouteille-standard.png",
|
||||
description: "Very capable, but faster and lower cost than Davinci.",
|
||||
type: "GPT-3",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Magnum (L)",
|
||||
image: "/bottle-images/bouteille-magnum.png",
|
||||
description: "Capable of straightforward tasks, very fast, and lower cost.",
|
||||
type: "GPT-3",
|
||||
},
|
||||
];
|
||||
@@ -3,6 +3,8 @@ import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAppDispatch } from "@/hooks";
|
||||
import { updateStage } from "@/store/app.slice";
|
||||
import { ModelSelector } from "@/components/layout/sidebar/stage-settings/stage-size-selector/model-selector";
|
||||
import { models, types } from "./stage-size-selector/models";
|
||||
|
||||
export const StageSize = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -15,7 +17,7 @@ export const StageSize = () => {
|
||||
};
|
||||
return (
|
||||
<Card className="p-3">
|
||||
<CardHeader>
|
||||
{/*<CardHeader>
|
||||
<CardTitle>{`Sélectionnez la taille d'étiquette`}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
@@ -29,14 +31,9 @@ export const StageSize = () => {
|
||||
<Button onClick={() => handleStageSizeSelect(410, 289)}>
|
||||
Grand Format
|
||||
</Button>
|
||||
</div>
|
||||
</div>*/}
|
||||
|
||||
{/*<CardFooter className="mt-8 justify-between">*/}
|
||||
{/* <Button variant="ghost" onClick={() => setOpen(false)}>*/}
|
||||
{/* Cancel*/}
|
||||
{/* </Button>*/}
|
||||
{/* <Button onClick={handleSizeSelect}>Submit</Button>*/}
|
||||
{/*</CardFooter>*/}
|
||||
<ModelSelector types={types} models={models} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,9 +22,6 @@ export default function TextInput() {
|
||||
};
|
||||
return (
|
||||
<Card className="p-3">
|
||||
<CardHeader className="pt-2">
|
||||
<CardTitle className="text-center">Text</CardTitle>
|
||||
</CardHeader>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Entrez votre text ..."
|
||||
|
||||
20
src/hooks/use-mutation-observer.ts
Normal file
20
src/hooks/use-mutation-observer.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from "react";
|
||||
|
||||
export const useMutationObserver = (
|
||||
ref: React.MutableRefObject<HTMLElement | null>,
|
||||
callback: MutationCallback,
|
||||
options = {
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
},
|
||||
) => {
|
||||
React.useEffect(() => {
|
||||
if (ref.current) {
|
||||
const observer = new MutationObserver(callback);
|
||||
observer.observe(ref.current, options);
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
}, [ref, callback, options]);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import type { AnyAction, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { createSlice, current } from "@reduxjs/toolkit";
|
||||
import { v1 } from "uuid";
|
||||
import { type WritableDraft } from "immer/src/types/types-external";
|
||||
import { CANVAS_PADDING_X, CANVAS_PADDING_Y } from "@/consts/canvas-params";
|
||||
|
||||
export enum StageItemType {
|
||||
Text = "text",
|
||||
@@ -61,6 +62,16 @@ const defaultImageConfig = {
|
||||
scaleY: 1,
|
||||
};
|
||||
|
||||
const defaultBackgroundImageConfig = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
};
|
||||
|
||||
const addNewItemToHistory =
|
||||
(
|
||||
callback: (
|
||||
@@ -195,6 +206,36 @@ export const appSlice = createSlice({
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
setBackgroundImage: addNewItemToHistory(
|
||||
(
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
imageUrl: string;
|
||||
}>,
|
||||
currentHistoryEntry,
|
||||
) => {
|
||||
const image = action.payload;
|
||||
if (!image) return;
|
||||
const imageId = v1();
|
||||
const newImage = {
|
||||
type: StageItemType.Image,
|
||||
id: imageId,
|
||||
isBlocked: false,
|
||||
params: {
|
||||
imageUrl: action.payload.imageUrl,
|
||||
width: state.history[0]?.stage.width,
|
||||
height: state.history[0]?.stage.height,
|
||||
...defaultBackgroundImageConfig,
|
||||
},
|
||||
} as const;
|
||||
const newHistoryEntry = {
|
||||
...currentHistoryEntry,
|
||||
items: [...currentHistoryEntry.items, newImage],
|
||||
};
|
||||
state.history.push(newHistoryEntry);
|
||||
},
|
||||
),
|
||||
// -
|
||||
selectItem: addNewItemToHistory(
|
||||
(state, action: PayloadAction<string>, currentHistoryEntry) => {
|
||||
@@ -332,4 +373,5 @@ export const {
|
||||
setBlockedItem,
|
||||
goBack,
|
||||
goForward,
|
||||
setBackgroundImage,
|
||||
} = appSlice.actions;
|
||||
|
||||
Reference in New Issue
Block a user