Started test version

This commit is contained in:
Artur AGH
2023-12-16 09:28:22 +01:00
parent a6135ff964
commit 493c787831
22 changed files with 1073 additions and 44 deletions

View File

@@ -23,6 +23,18 @@ const config = {
locales: ["en"],
defaultLocale: "en",
},
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
],
},
};
export default config;

View File

@@ -15,6 +15,7 @@
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^1.7.17",
"@hookform/resolvers": "^3.3.2",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.1.1",
"@radix-ui/react-avatar": "^1.0.4",
@@ -27,6 +28,7 @@
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
@@ -54,6 +56,7 @@
"next-auth": "^4.23.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.49.0",
"react-icons": "^4.11.0",
"react-konva": "^18.2.10",
"react-konva-utils": "^1.0.5",
@@ -63,7 +66,7 @@
"tailwindcss-animate": "^1.0.7",
"use-image": "^1.1.1",
"uuid": "^9.0.1",
"zod": "^3.21.4"
"zod": "^3.22.4"
},
"devDependencies": {
"@types/eslint": "^8.44.2",

28
pnpm-lock.yaml generated
View File

@@ -17,6 +17,9 @@ dependencies:
'@headlessui/react':
specifier: ^1.7.17
version: 1.7.17(react-dom@18.2.0)(react@18.2.0)
'@hookform/resolvers':
specifier: ^3.3.2
version: 3.3.2(react-hook-form@7.49.0)
'@next-auth/prisma-adapter':
specifier: ^1.0.7
version: 1.0.7(@prisma/client@5.5.0)(next-auth@4.24.3)
@@ -53,6 +56,9 @@ dependencies:
'@radix-ui/react-slider':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot':
specifier: ^1.0.2
version: 1.0.2(@types/react@18.2.31)(react@18.2.0)
'@radix-ui/react-tabs':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
@@ -134,6 +140,9 @@ dependencies:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
react-hook-form:
specifier: ^7.49.0
version: 7.49.0(react@18.2.0)
react-icons:
specifier: ^4.11.0
version: 4.11.0(react@18.2.0)
@@ -162,7 +171,7 @@ dependencies:
specifier: ^9.0.1
version: 9.0.1
zod:
specifier: ^3.21.4
specifier: ^3.22.4
version: 3.22.4
devDependencies:
@@ -349,6 +358,14 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@hookform/resolvers@3.3.2(react-hook-form@7.49.0):
resolution: {integrity: sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==}
peerDependencies:
react-hook-form: ^7.0.0
dependencies:
react-hook-form: 7.49.0(react@18.2.0)
dev: false
/@humanwhocodes/config-array@0.11.13:
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
engines: {node: '>=10.10.0'}
@@ -4526,6 +4543,15 @@ packages:
scheduler: 0.23.0
dev: false
/react-hook-form@7.49.0(react@18.2.0):
resolution: {integrity: sha512-gf4qyY4WiqK2hP/E45UUT6wt3Khl49pleEVcIzxhLBrD6m+GMWtLRk0vMrRv45D1ZH8PnpXFwRPv0Pewske2jw==}
engines: {node: '>=18', pnpm: '8'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18
dependencies:
react: 18.2.0
dev: false
/react-icons@4.11.0(react@18.2.0):
resolution: {integrity: sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==}
peerDependencies:

View File

@@ -0,0 +1,10 @@
import React from "react";
import StageMode from "@/components/Test/content/edit-utils/stage-mode";
export default function Sidebar() {
return (
<>
<StageMode />
</>
);
}

View File

@@ -0,0 +1,40 @@
import React from "react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { TabsList, TabsTrigger } from "@/components/ui/tabs";
import { PiBeerBottle } from "react-icons/pi";
export default function StageMode() {
return (
<div className="grid gap-2">
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<span className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Taille de l&apos;etquieutte
</span>
</HoverCardTrigger>
<HoverCardContent className="w-[320px] text-sm" side="left">
Sélectionnez la taille de votre raquette parmi les trois tailles
disponibles : S, M, L.
</HoverCardContent>
</HoverCard>
<TabsList className="grid h-[2.5rem] grid-cols-3">
<TabsTrigger value="complete">
<span className="sr-only">Complete</span>
<PiBeerBottle size={10} />
</TabsTrigger>
<TabsTrigger value="insert">
<span className="sr-only">Insert</span>
<PiBeerBottle size={15} />
</TabsTrigger>
<TabsTrigger value="edit">
<span className="sr-only">Edit</span>
<PiBeerBottle size={22} />
</TabsTrigger>
</TabsList>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import React from "react";
import { TabsContent } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { CounterClockwiseClockIcon } from "@radix-ui/react-icons";
import Canvas from "@/components/canvas";
export default function StageArea() {
return (
<TabsContent value="complete" className="mt-0 border-0 p-0">
<div className="flex h-full flex-col space-y-4">
<div className="min-h-[400px] flex-1 rounded-md border border-input bg-background p-4 2xl:min-h-[750px]">
<Canvas />
</div>
<div className="flex items-center space-x-2">
<Button>Submit</Button>
<Button variant="secondary">
<span className="sr-only">Show history</span>
<CounterClockwiseClockIcon className="h-4 w-4" />
</Button>
</div>
</div>
</TabsContent>
);
}

View File

@@ -0,0 +1,21 @@
import React from "react";
import { Tabs } from "@/components/ui/tabs";
import Sidebar from "@/components/Test/content/edit-utils/sidebar";
import StageArea from "@/components/Test/content/playground-area/stage-area";
export default function Playground() {
return (
<Tabs defaultValue="complete" className="flex-1">
<div className="container h-full py-6">
<div className="grid h-full items-stretch gap-6 md:grid-cols-[1fr_200px]">
<div className="md:order-1">
<StageArea />
</div>
<div className="hidden flex-col space-y-4 sm:flex md:order-2">
<Sidebar />
</div>
</div>
</div>
</Tabs>
);
}

View File

@@ -0,0 +1,22 @@
import React from "react";
import { Save } from "@/components/Test/navbar/Save";
import { Preview } from "@/components/Test/navbar/Preview";
import { SelectVigneron } from "@/components/Test/navbar/select-vigneron";
import { presets } from "@/components/Test/navbar/list-de-vignerons";
import { UserNav } from "@/components/layout/user-nav";
export default function Navbar() {
return (
<div className="container flex flex-col items-start justify-between space-y-2 py-4 sm:flex-row sm:items-center sm:space-y-0 md:h-16">
<h2 className="text-lg font-semibold">Vitiquette</h2>
<div className="ml-auto flex w-full space-x-2 sm:justify-end">
<SelectVigneron presets={presets} />
<Save />
<Preview />
<div className="ml-auto flex items-center space-x-4">
<UserNav />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,89 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function Preview() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary">Preview</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[625px]">
<DialogHeader>
<DialogTitle>View code</DialogTitle>
<DialogDescription>
You can use the following code to start integrating your current
prompt and settings into your application.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<div className="rounded-md bg-black p-6">
<pre>
<code className="grid gap-1 text-sm text-muted-foreground [&_span]:h-4">
<span>
<span className="text-sky-300">import</span> os
</span>
<span>
<span className="text-sky-300">import</span> openai
</span>
<span />
<span>
openai.api_key = os.getenv(
<span className="text-green-300">
&quot;OPENAI_API_KEY&quot;
</span>
)
</span>
<span />
<span>response = openai.Completion.create(</span>
<span>
{" "}
model=
<span className="text-green-300">&quot;davinci&quot;</span>,
</span>
<span>
{" "}
prompt=<span className="text-amber-300">&quot;&quot;</span>,
</span>
<span>
{" "}
temperature=<span className="text-amber-300">0.9</span>,
</span>
<span>
{" "}
max_tokens=<span className="text-amber-300">5</span>,
</span>
<span>
{" "}
top_p=<span className="text-amber-300">1</span>,
</span>
<span>
{" "}
frequency_penalty=<span className="text-amber-300">0</span>,
</span>
<span>
{" "}
presence_penalty=<span className="text-green-300">0</span>,
</span>
<span>)</span>
</code>
</pre>
</div>
<div>
<p className="text-sm text-muted-foreground">
Your API Key can be found here. You should use environment
variables or a secret management tool to expose your key to your
applications.
</p>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,44 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
export function Save() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary">Save</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[475px]">
<DialogHeader>
<DialogTitle>Save preset</DialogTitle>
<DialogDescription>
This will save the current playground state as a preset which you
can access later or share with others.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" autoFocus />
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input id="description" />
</div>
</div>
<DialogFooter>
<Button type="submit">Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,47 @@
export interface Preset {
id: string;
name: string;
}
export const presets: Preset[] = [
{
id: "9cb0e66a-9937-465d-a188-2c4c4ae2401f",
name: "Grammatical Standard English",
},
{
id: "61eb0e32-2391-4cd3-adc3-66efe09bc0b7",
name: "Summarize for a 2nd grader",
},
{
id: "a4e1fa51-f4ce-4e45-892c-224030a00bdd",
name: "Text to command",
},
{
id: "cc198b13-4933-43aa-977e-dcd95fa30770",
name: "Q&A",
},
{
id: "adfa95be-a575-45fd-a9ef-ea45386c64de",
name: "English to other languages",
},
{
id: "c569a06a-0bd6-43a7-adf9-bf68c09e7a79",
name: "Parse unstructured data",
},
{
id: "15ccc0d7-f37a-4f0a-8163-a37e162877dc",
name: "Classification",
},
{
id: "4641ef41-1c0f-421d-b4b2-70fe431081f3",
name: "Natural language to Python",
},
{
id: "48d34082-72f3-4a1b-a14d-f15aca4f57a0",
name: "Explain code",
},
{
id: "dfd42fd5-0394-4810-92c6-cc907d3bfd1a",
name: "Chat",
},
];

View File

@@ -0,0 +1,82 @@
"use client";
import * as React from "react";
import { useRouter } from "next/navigation";
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import { type PopoverProps } from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { type Preset } from "./list-de-vignerons";
interface PresetSelectorProps extends PopoverProps {
presets: Preset[];
}
export function SelectVigneron({ presets, ...props }: PresetSelectorProps) {
const [open, setOpen] = React.useState(false);
const [selectedPreset, setSelectedPreset] = React.useState<Preset>();
const router = useRouter();
return (
<Popover open={open} onOpenChange={setOpen} {...props}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-label="Load a preset..."
aria-expanded={open}
className="flex-1 justify-between md:max-w-[200px] lg:max-w-[300px]"
>
{selectedPreset ? selectedPreset.name : "Load a preset..."}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput placeholder="Search presets..." />
<CommandEmpty>No presets found.</CommandEmpty>
<CommandGroup heading="Examples">
{presets.map((preset) => (
<CommandItem
key={preset.id}
onSelect={() => {
setSelectedPreset(preset);
setOpen(false);
}}
>
{preset.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
selectedPreset?.id === preset.id
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
<CommandGroup className="pt-0">
<CommandItem onSelect={() => router.push("/examples")}>
More examples
</CommandItem>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@@ -9,16 +9,36 @@ import {
updateImage,
updateText,
} from "@/store/app.slice";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Toolbar } from "@/components/toolbar";
import TransformableText from "@/components/transformable-text";
import LayerBorder from "@/components/layer-border";
import { CANVAS_PADDING_X, CANVAS_PADDING_Y } from "@/consts/canvas-params";
import Legacy from "@/components/layout/sidebar/legacy";
import { type Shape } from "konva/lib/Shape";
import Konva from "konva";
type Snap = "start" | "center" | "end";
type SnappingEdges = {
vertical: Array<{
guide: number;
offset: number;
snap: Snap;
}>;
horizontal: Array<{
guide: number;
offset: number;
snap: Snap;
}>;
};
const GUIDELINE_OFFSET = 5;
const Canvas = () => {
const dispatch = useAppDispatch();
const stage = useAppSelector((state) => state.app.stage);
const stageRef = useRef<Konva.Stage>(null);
const layerRef = useRef<Konva.Layer>(null);
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id));
const items = useAppSelector((state) => state.app.items);
@@ -56,6 +76,267 @@ const Canvas = () => {
setSelected(!isEditing);
}
/* Drag Drop logic */
const getObjectSnappingEdges = useCallback((node: Shape): SnappingEdges => {
const box = node.getClientRect();
const absPos = node.absolutePosition();
return {
vertical: [
{
guide: Math.round(box.x),
offset: Math.round(absPos.x - box.x),
snap: "start",
},
{
guide: Math.round(box.x + box.width / 2),
offset: Math.round(absPos.x - box.x - box.width / 2),
snap: "center",
},
{
guide: Math.round(box.x + box.width),
offset: Math.round(absPos.x - box.x - box.width),
snap: "end",
},
],
horizontal: [
{
guide: Math.round(box.y),
offset: Math.round(absPos.y - box.y),
snap: "start",
},
{
guide: Math.round(box.y + box.height / 2),
offset: Math.round(absPos.y - box.y - box.height / 2),
snap: "center",
},
{
guide: Math.round(box.y + box.height),
offset: Math.round(absPos.y - box.y - box.height),
snap: "end",
},
],
};
}, []);
const getLineGuideStops = (skipShape: Konva.Shape) => {
const stage = skipShape.getStage();
if (!stage) return { vertical: [], horizontal: [] };
// we can snap to playground-area borders and the center of the playground-area
const vertical = [0, stage.width() / 2, stage.width()];
const horizontal = [0, stage.height() / 2, stage.height()];
// and we snap over edges and center of each object on the canvas
stage.find(".object").forEach((guideItem) => {
if (guideItem === skipShape) {
return;
}
const box = guideItem.getClientRect();
// and we can snap to all edges of shapes
vertical.push(box.x, box.x + box.width, box.x + box.width / 2);
horizontal.push(box.y, box.y + box.height, box.y + box.height / 2);
});
return {
vertical,
horizontal,
};
};
const getGuides = useCallback(
(
lineGuideStops: ReturnType<typeof getLineGuideStops>,
itemBounds: ReturnType<typeof getObjectSnappingEdges>,
) => {
const resultV: Array<{
lineGuide: number;
diff: number;
snap: Snap;
offset: number;
}> = [];
const resultH: Array<{
lineGuide: number;
diff: number;
snap: Snap;
offset: number;
}> = [];
lineGuideStops.vertical.forEach((lineGuide) => {
itemBounds.vertical.forEach((itemBound) => {
const diff = Math.abs(lineGuide - itemBound.guide);
if (diff < GUIDELINE_OFFSET) {
resultV.push({
lineGuide: lineGuide,
diff: diff,
snap: itemBound.snap,
offset: itemBound.offset,
});
}
});
});
lineGuideStops.horizontal.forEach((lineGuide) => {
itemBounds.horizontal.forEach((itemBound) => {
const diff = Math.abs(lineGuide - itemBound.guide);
if (diff < GUIDELINE_OFFSET) {
resultH.push({
lineGuide: lineGuide,
diff: diff,
snap: itemBound.snap,
offset: itemBound.offset,
});
}
});
});
const guides: Array<{
lineGuide: number;
offset: number;
orientation: "V" | "H";
snap: "start" | "center" | "end";
}> = [];
const minV = resultV.sort((a, b) => a.diff - b.diff)[0];
const minH = resultH.sort((a, b) => a.diff - b.diff)[0];
if (minV) {
guides.push({
lineGuide: minV.lineGuide,
offset: minV.offset,
orientation: "V",
snap: minV.snap,
});
}
if (minH) {
guides.push({
lineGuide: minH.lineGuide,
offset: minH.offset,
orientation: "H",
snap: minH.snap,
});
}
return guides;
},
[],
);
const drawGuides = useCallback(
(guides: ReturnType<typeof getGuides>, layer: Konva.Layer) => {
guides.forEach((lg) => {
if (lg.orientation === "H") {
const line = new Konva.Line({
points: [-6000, 0, 6000, 0],
stroke: "rgb(0, 161, 255)",
strokeWidth: 1,
name: "guid-line",
dash: [4, 6],
});
layer.add(line);
line.absolutePosition({
x: 0,
y: lg.lineGuide,
});
} else if (lg.orientation === "V") {
const line = new Konva.Line({
points: [0, -6000, 0, 6000],
stroke: "rgb(0, 161, 255)",
strokeWidth: 1,
name: "guid-line",
dash: [4, 6],
});
layer.add(line);
line.absolutePosition({
x: lg.lineGuide,
y: 0,
});
}
});
},
[],
);
const onDragMove = useCallback(
(e: Konva.KonvaEventObject<DragEvent>) => {
const layer = e.target.getLayer();
if (!layer) {
return;
}
// clear all previous lines on the screen
layer.find(".guid-line").forEach((l) => l.destroy());
// find possible snapping lines
const lineGuideStops = getLineGuideStops(e.target as Konva.Shape);
// find snapping points of current object
const itemBounds = getObjectSnappingEdges(e.target as Konva.Shape);
// now find where can we snap current object
const guides = getGuides(lineGuideStops, itemBounds);
// do nothing if no snapping
if (!guides.length) {
return;
}
drawGuides(guides, layer);
const absPos = e.target.absolutePosition();
// now force object position
guides.forEach((lg) => {
switch (lg.snap) {
case "start": {
switch (lg.orientation) {
case "V": {
absPos.x = lg.lineGuide + lg.offset;
break;
}
case "H": {
absPos.y = lg.lineGuide + lg.offset;
break;
}
}
break;
}
case "center": {
switch (lg.orientation) {
case "V": {
absPos.x = lg.lineGuide + lg.offset;
break;
}
case "H": {
absPos.y = lg.lineGuide + lg.offset;
break;
}
}
break;
}
case "end": {
switch (lg.orientation) {
case "V": {
absPos.x = lg.lineGuide + lg.offset;
break;
}
case "H": {
absPos.y = lg.lineGuide + lg.offset;
break;
}
}
break;
}
}
});
e.target.absolutePosition(absPos);
},
[drawGuides, getGuides, getObjectSnappingEdges],
);
const onDragEnd = (e: Konva.KonvaEventObject<DragEvent>) => {
const layer = e.target.getLayer();
// clear all previous lines on the screen
layer?.find(".guid-line").forEach((l) => l.destroy());
};
return (
<div className="relative flex h-full w-full flex-col items-center">
<Toolbar />
@@ -72,6 +353,7 @@ const Canvas = () => {
>
<Stage
className="bg-white"
ref={stageRef}
width={stage.width}
height={stage.height}
onTouchStart={deselectHandler}
@@ -98,6 +380,7 @@ const Canvas = () => {
clipY={CANVAS_PADDING_Y}
clipWidth={stage.width - 2 * CANVAS_PADDING_X}
clipHeight={stage.height - 2 * CANVAS_PADDING_Y}
ref={layerRef}
>
{items.toReversed().map((item) => {
if (item.type === StageItemType.Image) {
@@ -109,7 +392,12 @@ const Canvas = () => {
onChange={(newAttrs) => {
dispatch(updateImage({ id: item.id, ...newAttrs }));
}}
imageProps={item.params}
imageProps={{
...item.params,
onDragMove,
onDragEnd,
name: "object",
}}
key={item.id}
isBlocked={item.isBlocked}
/>
@@ -126,7 +414,12 @@ const Canvas = () => {
onChange={(newAttrs) => {
dispatch(updateText({ id: item.id, ...newAttrs }));
}}
textProps={item.params}
textProps={{
...item.params,
onDragMove,
onDragEnd,
name: "object",
}}
key={item.id}
id={item.id}
/>

View File

@@ -1,6 +1,9 @@
import type { ReactNode } from "react";
import { Sidebar } from "./sidebar/sidebar";
import { Navbar } from "./navbar";
import React from "react";
import { Separator } from "@/components/ui/separator";
import { Tabs } from "@/components/ui/tabs";
import { Sidebar } from "@/components/layout/sidebar/sidebar";
import { Navbar } from "@/components/layout/navbar";
type Props = {
children: ReactNode;
@@ -14,6 +17,23 @@ export const Layout = ({ children }: Props) => {
<Sidebar />
<main className="h-full w-full bg-slate-300">{children}</main>
</div>
{/*Test Update Version */}
{/* <div className="hidden h-full flex-col md:flex">
<Navbar />
<Separator />
<Tabs defaultValue="complete" className="flex-1">
<div className="container h-full py-6">
<div className="grid h-full items-stretch gap-6 md:grid-cols-[1fr_200px]">
<div className="md:order-1">{children}</div>
<div className="hidden flex-col space-y-4 sm:flex md:order-2">
<Sidebar />
</div>
</div>
</div>
</Tabs>
</div>*/}
</div>
);
};

View File

@@ -0,0 +1,140 @@
import React, { type ElementRef, useEffect, useRef, useState } from "react";
import {
Popover,
PopoverContent,
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);
const [query, setQuery] = useState<string>("");
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}`;
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}`;
console.log(url);
} 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 {
console.log(data);
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]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
onClick={() => setOpen(true)}
variant="outline"
className="text-xl"
>
<LuLayoutTemplate />
</Button>
</PopoverTrigger>
<PopoverContent side="right" className="mt-4">
<Card className="p-3">
<CardHeader className="mb-2 p-2 text-center">
<CardTitle>Template</CardTitle>
</CardHeader>
<Input
type="text"
placeholder="Search Images"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div
className=" grid h-64 w-full grid-cols-2 gap-4 overflow-auto"
ref={scrollAreaRef}
>
{photos?.map((p, i) => (
<div
key={i}
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>
))}
</div>
</Card>
</PopoverContent>
</Popover>
);
};

View File

@@ -4,7 +4,7 @@ import SizeSelect from "@/components/layout/sidebar/size-select";
import { BackgroundSelect } from "@/components/layout/sidebar/background-select";
import { Layers } from "@/components/layout/sidebar/layers";
import { SelectTemplate } from "@/components/layout/sidebar/select-template";
import { Switch } from "@/components/ui/switch";
import { ImageGallery } from "@/components/layout/sidebar/image-gallery";
export function Sidebar() {
return (
@@ -14,6 +14,7 @@ export function Sidebar() {
<ImageInput />
<SelectTemplate />
<BackgroundSelect />
<ImageGallery />
<Layers />
</div>
);

View File

@@ -41,21 +41,11 @@ export const TransformableImage = ({
<>
<Image
onDragMove={(e) => {
console.log(e);
const positionX = e.target.x() + e.target.width() / 2;
const positionY = e.target.y() + e.target.height() / 2;
const canvasXCenter = (e.target.getStage()?.width() ?? 0) / 2;
const canvasYCenter = (e.target.getStage()?.height() ?? 0) / 2;
const isInTheXCenter =
Math.abs(Math.round(canvasXCenter) - Math.round(positionX)) <= 5;
const isInTheYCenter =
Math.abs(Math.round(canvasYCenter) - Math.round(positionY)) <= 5;
if (isBlocked) {
return;
}
document.body.classList.toggle("show-vertical-line", isInTheXCenter);
document.body.classList.toggle(
"show-horizontal-line",
isInTheYCenter,
);
imageProps.onDragMove(e);
}}
id={id}
alt={"canvas image"}
@@ -69,18 +59,7 @@ export const TransformableImage = ({
if (isBlocked) {
return;
}
const positionX = e.target.x() + e.target.width() / 2;
const canvasXCenter = (e.target.getStage()?.width() ?? 0) / 2;
const isInTheCenter =
Math.abs(Math.round(canvasXCenter) - Math.round(positionX)) <= 5;
onChange({
...imageProps,
image,
x: isInTheCenter
? canvasXCenter - e.target.width() / 2
: e.target.x(),
y: e.target.y(),
});
imageProps.onDragEnd?.(e);
}}
onTransformEnd={() => {
const node = imageRef.current;

171
src/components/ui/form.tsx Normal file
View File

@@ -0,0 +1,171 @@
import * as React from "react";
import type * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import { Controller, FormProvider, useFormContext } from "react-hook-form";
import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View File

@@ -1,11 +1,11 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
@@ -18,12 +18,12 @@ const PopoverContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent }
export { Popover, PopoverTrigger, PopoverContent };

View File

@@ -8,9 +8,9 @@ const ScrollArea = React.forwardRef<
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
ref={ref}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}

View File

View File

@@ -1,6 +1,10 @@
import dynamic from "next/dynamic";
import Head from "next/head";
const Canvas = dynamic(() => import("../components/canvas"), { ssr: false });
/*const Canvas = dynamic(
() => import("../components/Test/content/playground-area/stage-area"),
{ ssr: false },
);*/
export default function Home() {
return (