vercel json add

This commit is contained in:
Artur Agh
2024-01-23 08:16:57 +01:00
parent 977b750476
commit 6cbc874042
19 changed files with 552 additions and 47 deletions

View File

@@ -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} />

View File

@@ -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);

View File

@@ -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>
);

View File

@@ -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}

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -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>
);
}

View File

@@ -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",
},
];

View File

@@ -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>
);
};

View File

@@ -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 ..."

View 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]);
};

View File

@@ -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;