Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Sidebar from "./components/sidebar";
import EditorView from "./components/EditorView";
import SideWrapper from "./components/layout/SideWrapper";
import Canvas from "./components/canvas";
import CanvasTools from "./components/canvasTools";

import { useSyncedUsers } from "./hooks/useSyncedUsers";

Expand All @@ -19,8 +20,12 @@ function App() {
<EditorView />
</SideWrapper>
<Canvas className="z-0 h-full w-full" />
<SideWrapper side="left" className="left-4 top-4">
<SideWrapper
side="left"
className="left-4 top-4 flex flex-row items-start gap-2"
>
<Sidebar />
<CanvasTools />
</SideWrapper>
</div>
</QueryClientProvider>
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/components/LogoBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface LogoBtnProps {
export default function LogoBtn({ onClick }: LogoBtnProps) {
return (
<button
className="h-6 w-6 overflow-clip rounded-md bg-[#231f20] p-1"
className="h-8 w-8 overflow-clip rounded-md bg-[#231f20] p-1"
onClick={onClick}
>
<img src={logo} />
Expand Down
13 changes: 13 additions & 0 deletions apps/frontend/src/components/canvasTools/CursorButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import MousePointer from "@/icons/mousePointer";

interface CursorButtonProps {
color: string;
}

export default function CursorButton({ color }: CursorButtonProps) {
return (
<button className="flex h-9 w-9 items-center justify-center rounded-md hover:bg-[#F5F5F5]">
<MousePointer fill={color} className="h-6 w-6" />
</button>
);
}
42 changes: 42 additions & 0 deletions apps/frontend/src/components/canvasTools/CursorPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useState, useRef } from "react";
import BiggerCursor from "@/components/cursor/BiggerCursor";

interface CursorPreviewProps {
color: string;
clientId: string;
defaultCoors: { x: number; y: number };
}

export default function CursorPreview({
color,
clientId,
defaultCoors,
}: CursorPreviewProps) {
const [coors, setCoors] = useState(defaultCoors);
const previewRef = useRef<HTMLDivElement>(null);

const handleMouseMove = (e: React.MouseEvent) => {
if (previewRef.current) {
const rect = previewRef.current.getBoundingClientRect();
setCoors({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
}
};

const handleMouseLeave = () => {
setCoors(defaultCoors);
};

return (
<div
ref={previewRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="relative h-48 w-60 overflow-hidden rounded-md border-[1px] border-[#d0d9e0] bg-white bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:7px_7px]"
>
<BiggerCursor coors={coors} clientId={clientId} color={color} />
</div>
);
}
13 changes: 13 additions & 0 deletions apps/frontend/src/components/canvasTools/FormField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interface FormFieldProps {
label: string;
input: React.ReactNode;
}

export default function FormField({ label, input }: FormFieldProps) {
return (
<div className="flex flex-col gap-1.5">
<label className="font-medium text-[#433d3f]">{label}</label>
{input}
</div>
);
}
91 changes: 91 additions & 0 deletions apps/frontend/src/components/canvasTools/ProfileForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { RefreshCcw } from "lucide-react";
import FormField from "./FormField";
import useUserStore from "@/store/useUserStore";
import { getRandomColor } from "@/lib/utils";

interface ProfileFormProps {
color: string;
clientId: string;
onColorChange: (color: string) => void;
onClientIdChange: (clientId: string) => void;
onSave: () => void;
}

export default function ProfileForm({
color,
clientId,
onColorChange,
onClientIdChange,
onSave,
}: ProfileFormProps) {
const { currentUser, setCurrentUser, provider } = useUserStore();

const handleRefreshClick = () => {
const newColor = getRandomColor();
onColorChange(newColor);
};

const handleColorInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const isValidHex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
if (isValidHex.test(e.target.value)) {
onColorChange(e.target.value);
}
onColorChange(e.target.value);
};

const handleSave = () => {
provider.awareness.setLocalStateField("color", color);
provider.awareness.setLocalStateField("clientId", clientId);

setCurrentUser({
...currentUser,
color,
clientId,
});

onSave();
};

return (
<div className="flex w-36 flex-col justify-between">
<div className="flex flex-col gap-2">
<FormField
label="프로필"
input={
<input
value={clientId}
onChange={(e) => onClientIdChange(e.target.value)}
className="h-8 rounded-md border-[1px] border-[#d0d9e0] bg-[#f5f6fa] px-2"
placeholder="닉네임을 입력하세요"
/>
}
/>
<FormField
label="색상"
input={
<div className="flex flex-row gap-1.5 overflow-hidden">
<button
onClick={handleRefreshClick}
style={{ backgroundColor: color }}
className="flex h-[32px] w-[32px] shrink-0 items-center justify-center rounded-sm transition-colors"
>
<RefreshCcw size={18} color="#464646" strokeWidth={2.2} />
</button>
<input
value={color}
onChange={handleColorInput}
className="h-8 min-w-0 flex-1 rounded-md border-[1px] border-[#d0d9e0] bg-[#f5f6fa] px-2"
/>
</div>
}
/>
</div>
<button
onClick={handleSave}
className="self-end rounded-md bg-[#231f20] px-3.5 py-1.5 font-normal text-white"
>
저장
</button>
</div>
);
}
48 changes: 48 additions & 0 deletions apps/frontend/src/components/canvasTools/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Plus } from "lucide-react";
import { Popover } from "@/components/commons/popover";
import CursorButton from "./CursorButton";
import CursorPreview from "./CursorPreview";
import ProfileForm from "./ProfileForm";
import useUserStore from "@/store/useUserStore";
import { useEffect, useState } from "react";

export default function CanvasTools() {
const { currentUser } = useUserStore();
const [color, setColor] = useState(currentUser.color);
const [clientId, setClientId] = useState(currentUser.clientId);

useEffect(() => {
setColor(currentUser.color);
setClientId(currentUser.clientId);
}, [currentUser]);

return (
<div className="z-10 flex flex-row rounded-xl border-[1px] border-neutral-200 bg-white p-1.5 text-black shadow-md">
<Popover placement="bottom" align="start" offset={{ x: -6, y: 16 }}>
<Popover.Trigger>
<CursorButton color={color} />
</Popover.Trigger>
<Popover.Content className="rounded-lg border border-neutral-200 bg-white p-2 shadow-md">
<div className="flex flex-row gap-4 p-4">
<CursorPreview
defaultCoors={{ x: 90, y: 80 }}
clientId={clientId}
color={color}
/>
<ProfileForm
color={color}
clientId={clientId}
onColorChange={setColor}
onClientIdChange={setClientId}
onSave={() => {}}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거에요!

/>
</div>
</Popover.Content>
</Popover>

<button className="flex h-9 w-9 items-center justify-center rounded-md hover:bg-[#F5F5F5]">
<Plus size={26} color="#3f3f3f" strokeWidth={1.5} />
</button>
</div>
);
}
57 changes: 57 additions & 0 deletions apps/frontend/src/components/cursor/BiggerCursor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { motion } from "framer-motion";

export interface Coors {
x: number;
y: number;
}

interface CursorProps {
coors: Coors;
clientId: string;
color?: string;
}

export default function BiggerCursor({
coors,
color = "#ffb8b9",
clientId,
}: CursorProps) {
const { x, y } = coors;

return (
<motion.div
className="pointer-events-none absolute left-0 top-0 z-50"
initial={{ x, y }}
animate={{ x, y }}
transition={{
type: "spring",
bounce: 0.6,
damping: 30,
mass: 0.8,
stiffness: 350,
restSpeed: 0.01,
}}
>
<div className="relative">
<svg
width="27"
height="27"
viewBox="0 0 27 27"
fill={color}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.701 0.771174L24.8693 8.80848C26.1506 9.27302 26.1985 11.064 24.9439 11.5962L16.9292 14.9958C16.3473 15.2426 15.8806 15.7 15.6229 16.2762L11.704 25.0374C11.1542 26.2666 9.38566 26.2051 8.92279 24.9408L0.778147 2.69403C0.340278 1.498 1.50186 0.336418 2.701 0.771174Z"
stroke="black"
/>
</svg>
<div
className="absolute left-6 top-6 max-w-28 truncate rounded-md border-[1px] border-black px-2 py-1 text-center text-sm font-semibold shadow-sm"
style={{ backgroundColor: color }}
>
{clientId}
</div>
</div>
</motion.div>
);
}
16 changes: 9 additions & 7 deletions apps/frontend/src/components/sidebar/TopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ export default function TopNav({ onExpand, isExpanded }: TopNavProps) {
<VerticalDivider className="h-3" />
<WorkspaceNav workspaceTitle="프로젝트 Web15" />
</div>
<button onClick={onExpand}>
{isExpanded ? (
<X color="#3F3F3F" />
) : (
<Menu size={24} color="#3F3F3F" />
)}
</button>
<div className="flex h-7 w-7 items-center justify-center">
<button onClick={onExpand}>
{isExpanded ? (
<X color="#3F3F3F" />
) : (
<Menu size={24} color="#3F3F3F" />
)}
</button>
</div>
</div>
);
}
6 changes: 3 additions & 3 deletions apps/frontend/src/components/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ export default function Sidebar() {
};

return (
<div className="z-10 flex w-64 flex-col rounded-xl border-[1px] border-neutral-200 bg-white text-black shadow-md">
<div className="p-4">
<div className="z-10 flex w-60 flex-col rounded-xl border-[1px] border-neutral-200 bg-white text-black shadow-md">
<div className="p-2">
<TopNav onExpand={handleExpand} isExpanded={isExpanded} />
</div>
<div className={`${isExpanded ? "flex flex-col" : "hidden"} gap-3 pb-4`}>
<div className="w-full px-4">
<Tools />
</div>
<ScrollWrapper className="scrollbar scrollbar-thumb-[#d9d9d9] scrollbar-track-transparent max-h-[604px] overflow-x-clip">
<ScrollWrapper className="max-h-[604px] overflow-x-clip scrollbar scrollbar-track-transparent scrollbar-thumb-[#d9d9d9]">
<NoteList className="p-4 pb-0 pt-0" />
</ScrollWrapper>
</div>
Expand Down
34 changes: 34 additions & 0 deletions apps/frontend/src/icons/mousePointer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
interface MousePointerProps {
fill?: string;
className?: string;
}
export default function MousePointer({ fill, className }: MousePointerProps) {
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g clip-path="url(#clip0_570_13)">
<path
d="M5.65655 3.8599L18.6378 8.63731C19.9139 9.10697 19.9565 10.9034 18.704 11.4334L14.652 13.1481C14.0711 13.3939 13.6044 13.8514 13.3458 14.4285L11.2629 19.0757C10.7112 20.3068 8.94797 20.24 8.49015 18.9707L3.73369 5.78276C3.3006 4.58195 4.4622 3.42035 5.65655 3.8599Z"
stroke="black"
fill={fill}
/>
</g>
<defs>
<clipPath id="clip0_570_13">
<rect
width="18"
height="18"
fill="white"
transform="translate(3 3)"
/>
</clipPath>
</defs>
</svg>
);
}
Loading