diff --git a/next.config.mjs b/next.config.mjs index 35d3fc6..e0eb093 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -23,6 +23,18 @@ const config = { locales: ["en"], defaultLocale: "en", }, + + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'images.unsplash.com', + }, + ], + }, + + + }; export default config; diff --git a/package.json b/package.json index b147bf9..3e252dd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f45da7b..7459649 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/components/Test/content/edit-utils/sidebar.tsx b/src/components/Test/content/edit-utils/sidebar.tsx new file mode 100644 index 0000000..68c086f --- /dev/null +++ b/src/components/Test/content/edit-utils/sidebar.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import StageMode from "@/components/Test/content/edit-utils/stage-mode"; + +export default function Sidebar() { + return ( + <> + + + ); +} diff --git a/src/components/Test/content/edit-utils/stage-mode.tsx b/src/components/Test/content/edit-utils/stage-mode.tsx new file mode 100644 index 0000000..522f474 --- /dev/null +++ b/src/components/Test/content/edit-utils/stage-mode.tsx @@ -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 ( +
+ + + + Taille de l'etquieutte + + + + Sélectionnez la taille de votre raquette parmi les trois tailles + disponibles : S, M, L. + + + + + Complete + + + + Insert + + + + Edit + + + +
+ ); +} diff --git a/src/components/Test/content/playground-area/stage-area.tsx b/src/components/Test/content/playground-area/stage-area.tsx new file mode 100644 index 0000000..361fd94 --- /dev/null +++ b/src/components/Test/content/playground-area/stage-area.tsx @@ -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 ( + +
+
+ +
+ +
+ + +
+
+
+ ); +} diff --git a/src/components/Test/content/playground.tsx b/src/components/Test/content/playground.tsx new file mode 100644 index 0000000..4ee528c --- /dev/null +++ b/src/components/Test/content/playground.tsx @@ -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 ( + +
+
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/src/components/Test/navbar/Navbar.tsx b/src/components/Test/navbar/Navbar.tsx new file mode 100644 index 0000000..c9eb8de --- /dev/null +++ b/src/components/Test/navbar/Navbar.tsx @@ -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 ( +
+

Vitiquette

+
+ + + +
+ +
+
+
+ ); +} diff --git a/src/components/Test/navbar/Preview.tsx b/src/components/Test/navbar/Preview.tsx new file mode 100644 index 0000000..885d8bf --- /dev/null +++ b/src/components/Test/navbar/Preview.tsx @@ -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 ( + + + + + + + View code + + You can use the following code to start integrating your current + prompt and settings into your application. + + +
+
+
+              
+                
+                  import os
+                
+                
+                  import openai
+                
+                
+                
+                  openai.api_key = os.getenv(
+                  
+                    "OPENAI_API_KEY"
+                  
+                  )
+                
+                
+                response = openai.Completion.create(
+                
+                  {" "}
+                  model=
+                  "davinci",
+                
+                
+                  {" "}
+                  prompt="",
+                
+                
+                  {" "}
+                  temperature=0.9,
+                
+                
+                  {" "}
+                  max_tokens=5,
+                
+                
+                  {" "}
+                  top_p=1,
+                
+                
+                  {" "}
+                  frequency_penalty=0,
+                
+                
+                  {" "}
+                  presence_penalty=0,
+                
+                )
+              
+            
+
+
+

+ Your API Key can be found here. You should use environment + variables or a secret management tool to expose your key to your + applications. +

+
+
+
+
+ ); +} diff --git a/src/components/Test/navbar/Save.tsx b/src/components/Test/navbar/Save.tsx new file mode 100644 index 0000000..c571d38 --- /dev/null +++ b/src/components/Test/navbar/Save.tsx @@ -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 ( + + + + + + + Save preset + + This will save the current playground state as a preset which you + can access later or share with others. + + +
+
+ + +
+
+ + +
+
+ + + +
+
+ ); +} diff --git a/src/components/Test/navbar/list-de-vignerons.ts b/src/components/Test/navbar/list-de-vignerons.ts new file mode 100644 index 0000000..1cd0057 --- /dev/null +++ b/src/components/Test/navbar/list-de-vignerons.ts @@ -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", + }, +]; diff --git a/src/components/Test/navbar/select-vigneron.tsx b/src/components/Test/navbar/select-vigneron.tsx new file mode 100644 index 0000000..e07b158 --- /dev/null +++ b/src/components/Test/navbar/select-vigneron.tsx @@ -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(); + const router = useRouter(); + + return ( + + + + + + + + No presets found. + + {presets.map((preset) => ( + { + setSelectedPreset(preset); + setOpen(false); + }} + > + {preset.name} + + + ))} + + + router.push("/examples")}> + More examples + + + + + + ); +} diff --git a/src/components/canvas.tsx b/src/components/canvas.tsx index db2443c..1956631 100644 --- a/src/components/canvas.tsx +++ b/src/components/canvas.tsx @@ -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(null); + const layerRef = useRef(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, + itemBounds: ReturnType, + ) => { + 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, 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) => { + 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) => { + const layer = e.target.getLayer(); + // clear all previous lines on the screen + layer?.find(".guid-line").forEach((l) => l.destroy()); + }; + return (
@@ -72,6 +353,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} /> diff --git a/src/components/layout/layout.tsx b/src/components/layout/layout.tsx index 699610e..d869839 100644 --- a/src/components/layout/layout.tsx +++ b/src/components/layout/layout.tsx @@ -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) => {
{children}
+ + {/*Test Update Version */} + + {/*
+ + + +
+
+
{children}
+
+ +
+
+
+
+
*/} ); }; diff --git a/src/components/layout/sidebar/image-gallery.tsx b/src/components/layout/sidebar/image-gallery.tsx new file mode 100644 index 0000000..c340900 --- /dev/null +++ b/src/components/layout/sidebar/image-gallery.tsx @@ -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(false); + const [query, setQuery] = useState(""); + const [page, setPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); + + const [photos, setPhotos] = useState([]); + + const scrollAreaRef = useRef>(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 ( + + + + + + + + Template + + + setQuery(e.target.value)} + /> + +
+ {photos?.map((p, i) => ( +
+ {"image"} +
+ ))} +
+
+
+
+ ); +}; diff --git a/src/components/layout/sidebar/sidebar.tsx b/src/components/layout/sidebar/sidebar.tsx index c0ea5d0..d3e3c69 100644 --- a/src/components/layout/sidebar/sidebar.tsx +++ b/src/components/layout/sidebar/sidebar.tsx @@ -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() { + ); diff --git a/src/components/transformable-image.tsx b/src/components/transformable-image.tsx index 9082988..40d1e05 100644 --- a/src/components/transformable-image.tsx +++ b/src/components/transformable-image.tsx @@ -41,21 +41,11 @@ export const TransformableImage = ({ <> { - 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; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..80acc66 --- /dev/null +++ b/src/components/ui/form.tsx @@ -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 = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +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 "); + } + + 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( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +