work in progress

This commit is contained in:
Artur AGH
2023-10-02 15:02:45 +02:00
parent 658dfde935
commit 55cd3f963d
14 changed files with 398 additions and 38 deletions

View File

@@ -4,17 +4,31 @@ import {
} from "@/components/transformable-image";
import type { KonvaEventObject } from "konva/lib/Node";
import { ChangeEvent, useState } from "react";
import { ChangeEvent, FormEventHandler, useState } from "react";
import { Layer, Stage } from "react-konva";
import { v1 } from "uuid";
import TransformableText, {
TransformableTextProps,
} from "./transformable-text";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { useAppDispatch, useAppSelector } from "@/hooks";
import { appSlice } from "@/store/app.slice";
// Provider *
const Canvas = () => {
const dispatch = useAppDispatch()
const selectedItemId = useAppSelector((state) => state.app.selectedItemId)
const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id))
const [selectedImageId, selectImage] = useState<string | null>(null);
const [selectedTextId, selectText] = useState<string | null>(null);
const [inputText, setInputText] = useState("");
const [images, setImages] = useState<TransformableImageProps["imageProps"][]>(
[],
@@ -22,47 +36,25 @@ const Canvas = () => {
const [texts, setTexts] = useState<TransformableTextProps["textProps"][]>([]);
const checkDeselect = (e: KonvaEventObject<MouseEvent>) => {
// deselect when clicked on empty area
const clickedOnEmpty = e.target === e.target.getStage();
if (clickedOnEmpty) {
selectImage(null);
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputText(e.target.value);
};
const handleImageUploaded = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const imageId = v1();
const imageUrl = URL.createObjectURL(file);
setImages((prev) => [...prev, { imageUrl, imageId }]);
};
const handleTextAdd = (e: ChangeEvent<HTMLInputElement>) => {
const text = e.target.value;
const textId = v1();
setTexts((prev) => [...prev, { text, textId }]);
};
/*console.log(texts, " ++++ ", images);*/
return (
<main>
<input type="file" onChange={handleImageUploaded} />
<input
type="text"
onChange={handleTextAdd}
placeholder="tape your text"
/>
<main className="flex">
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer>
{images.map((image) => {
return (
<TransformableImage
onSelect={() => selectImage(image.imageId)}
isSelected={image.imageId === selectedImageId}
onSelect={() => selectItem(image.imageId)}
isSelected={image.imageId === selectedItemId}
onChange={(newAttrs) => {
setImages(
images.map((i) =>
@@ -78,8 +70,8 @@ const Canvas = () => {
{texts.map((text) => {
return (
<TransformableText
onSelect={() => selectText(text.textId)}
isSelected={text.textId === selectedTextId}
onSelect={() => selectItem(text.textId)}
isSelected={text.textId === selectedItemId}
onChange={(newAttrs) => {
setTexts(
texts.map((t) => (t.textId === text.textId ? newAttrs : t)),

View File

@@ -0,0 +1,15 @@
import { ReactNode } from "react";
import { Sidebar } from "./sidebar";
type Props = {
children: ReactNode;
};
export const Layout = ({ children }: Props) => {
return (
<div className="flex h-full ">
<Sidebar {} />
<main>{children}</main>
</div>
);
};

View File

@@ -0,0 +1,37 @@
import { Button } from "../ui/button";
import { Input } from "../ui/input";
type Props = {
handleImageUploaded: any;
handleInputChange: any;
inputText: any;
handleTextAdd: any;
};
export function Sidebar({
handleImageUploaded,
handleInputChange,
handleTextAdd,
inputText,
}: Props) {
return (
<div className="flex flex-col ">
<Input
type="file"
className="m-[2rem] w-auto "
onChange={handleImageUploaded}
/>
<div className="m-[2rem] flex max-w-md justify-between">
<Input
type="text"
placeholder="enter the text"
value={inputText}
onChange={handleInputChange}
/>
<Button className="mx-[2rem] text-xs" onClick={handleTextAdd}>
Add new Text
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from '@radix-ui/react-icons'
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

2
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./use-app-dispatch"
export * from "./use-app-selector"

View File

@@ -0,0 +1,4 @@
import type { AppDispatch } from "@/store/store";
import { useDispatch } from "react-redux";
export const useAppDispatch: () => AppDispatch = useDispatch

View File

@@ -0,0 +1,4 @@
import type { RootState } from "@/store/store";
import { TypedUseSelectorHook, useSelector } from "react-redux";
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

View File

@@ -5,6 +5,7 @@ import { type AppType } from "next/app";
import { api } from "@/utils/api";
import "@/styles/globals.css";
import { Layout } from "@/components/layout/layout";
const MyApp: AppType<{ session: Session | null }> = ({
Component,
@@ -12,7 +13,9 @@ const MyApp: AppType<{ session: Session | null }> = ({
}) => {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
<Layout>
<Component {...pageProps} />
</Layout>
</SessionProvider>
);
};

View File

@@ -1,7 +1,17 @@
import dynamic from "next/dynamic";
import Head from "next/head";
const Canvas = dynamic(() => import("../components/canvas"), { ssr: false });
export default function Home() {
return <Canvas />;
return (
<>
<Head>
<title>Labbel Application</title>
</Head>
<div className="flex">
<Canvas />
</div>
</>
);
}

47
src/store/app.slice.ts Normal file
View File

@@ -0,0 +1,47 @@
import { TransformableImageProps } from "@/components/transformable-image";
import { TransformableTextProps } from "@/components/transformable-text";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { KonvaEventObject } from "konva/lib/Node";
import { ChangeEvent } from "react";
import { v1 } from "uuid";
const initialState = {
selectedItemId: null as string | null,
images: [] as TransformableImageProps["imageProps"][],
texts: [] as TransformableTextProps["textProps"][],
};
export const appSlice = createSlice({
name: "app",
initialState,
reducers: {
addImage: (state, action: PayloadAction<ChangeEvent<HTMLInputElement>>) => {
const file = action.payload.target.files?.[0];
if (!file) return;
const imageId = v1();
const imageUrl = URL.createObjectURL(file);
state.images.push({ imageUrl, id: imageId });
},
addText: (state, action: PayloadAction<{ initialValue: string }>) => {
const textId = v1();
state.texts.push({ text: action.payload.initialValue, id: textId });
},
selectItem: (state, action: PayloadAction<string>) => {
state.selectedItemId = action.payload;
},
checkDeselect: (
state,
action: PayloadAction<KonvaEventObject<MouseEvent>>,
) => {
// deselect when clicked on empty area
const clickedOnEmpty =
action.payload.target === action.payload.target.getStage();
if (clickedOnEmpty) {
state.selectedItemId = null;
}
},
},
});

11
src/store/store.ts Normal file
View File

@@ -0,0 +1,11 @@
import { configureStore } from "@reduxjs/toolkit";
import { appSlice } from "./app.slice";
export const store = configureStore({
reducer: {
[appSlice.name]: appSlice.reducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;