Skip to content

Commit

Permalink
Merge branch 'release/0.1.6'
Browse files Browse the repository at this point in the history
  • Loading branch information
pipisebastian committed Dec 2, 2024
2 parents 167ebf6 + cb8c83b commit 04aa78a
Show file tree
Hide file tree
Showing 21 changed files with 284 additions and 26 deletions.
25 changes: 25 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,32 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./src/assets/icons/noctaDayIcon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="λ°€ν•˜λŠ˜μ˜ λ³„λΉ›μ²˜λŸΌ, 자유둜운 μΈν„°λž™μ…˜ μ‹€μ‹œκ°„ 에디터" />
<title>Nocta</title>
<meta name="keywords" content="μ‹€μ‹œκ°„,ν˜‘μ—…,에디터,λ…Έμ…˜,λ§ˆν¬λ‹€μš΄" />
<meta name="author" content="Team Glassmo" />

<meta property="og:site_name" content="Nocta" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.nocta.site/" />
<meta property="og:title" content="Nocta" />
<meta
property="og:description"
content="λ°€ν•˜λŠ˜μ˜ λ³„λΉ›μ²˜λŸΌ, 자유둜운 μΈν„°λž™μ…˜ μ‹€μ‹œκ°„ 에디터"
/>
<meta property="og:image" content="https://www.nocta.site/images/nocta.png" />

<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://www.nocta.site/" />
<meta property="twitter:title" content="Nocta" />
<meta
property="twitter:description"
content="λ°€ν•˜λŠ˜μ˜ λ³„λΉ›μ²˜λŸΌ, 자유둜운 μΈν„°λž™μ…˜ μ‹€μ‹œκ°„ 에디터"
/>
<meta property="twitter:image" content="https://www.nocta.site/images/nocta.png" />

<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://www.nocta.site/" />
</head>

<body>
Expand Down
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "tsc -b && vite build",
"build": "tsc -b && panda codegen && vite build",
"lint": "eslint \"src/**/*.{ts,tsx}\" --fix",
"preview": "vite preview",
"prepare": "panda codegen"
Expand Down
Binary file added client/public/images/nocta.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions client/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://www.nocta.site/sitemap.xml
9 changes: 9 additions & 0 deletions client/public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.nocta.site</loc>
<lastmod>2024-12-02</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>
Binary file added client/src/assets/images/background.avif
Binary file not shown.
Binary file added client/src/assets/images/background.webp
Binary file not shown.
10 changes: 9 additions & 1 deletion client/src/components/modal/InviteModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// InviteModal.tsx
import { useState } from "react";
import { useState, useEffect, useRef } from "react";
import { modalContentContainer, titleText, descriptionText, emailInput } from "./InviteModal.style";
import { Modal } from "./modal";

Expand All @@ -11,6 +11,13 @@ interface InviteModalProps {

export const InviteModal = ({ isOpen, onClose, onInvite }: InviteModalProps) => {
const [email, setEmail] = useState("");
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (isOpen) {
inputRef.current?.focus();
}
}, [isOpen]);

const handleInvite = () => {
onInvite(email);
Expand All @@ -30,6 +37,7 @@ export const InviteModal = ({ isOpen, onClose, onInvite }: InviteModalProps) =>
<h2 className={titleText}>μ›Œν¬μŠ€νŽ˜μ΄μŠ€ μ΄ˆλŒ€</h2>
<p className={descriptionText}>μ΄ˆλŒ€ν•  μ‚¬μš©μžμ˜ 이메일을 μž…λ ₯ν•΄μ£Όμ„Έμš”</p>
<input
ref={inputRef}
className={emailInput}
onChange={(e) => setEmail(e.target.value)}
placeholder="이메일 μ£Όμ†Œ μž…λ ₯"
Expand Down
11 changes: 10 additions & 1 deletion client/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ export const Modal = ({
return createPortal(
<AnimatePresence>
{isOpen && (
<div className={container}>
<div
role="dialog"
className={container}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === "Escape") {
secondaryButtonOnClick?.();
}
}}
>
<motion.div
initial={overlayAnimation.initial}
animate={overlayAnimation.animate}
Expand Down
14 changes: 12 additions & 2 deletions client/src/components/sidebar/components/menuButton/MenuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { InviteModal } from "@src/components/modal/InviteModal";
import { useModal } from "@src/components/modal/useModal";
import { useSocketStore } from "@src/stores/useSocketStore";
import { useToastStore } from "@src/stores/useToastStore";
import { useWorkspaceStore } from "@src/stores/useWorkspaceStore";
import { useUserInfo } from "@stores/useUserStore";
import { menuItemWrapper, textBox, menuButtonContainer } from "./MenuButton.style";
import { MenuIcon } from "./components/MenuIcon";
Expand All @@ -18,7 +19,7 @@ export const MenuButton = () => {
openModal: openInviteModal,
closeModal: closeInviteModal,
} = useModal();

const currentRole = useWorkspaceStore((state) => state.currentRole);
const handleMenuClick = () => {
setIsOpen((prev) => !prev);
};
Expand Down Expand Up @@ -81,6 +82,15 @@ export const MenuButton = () => {
workspaceId: workspace.id,
});
};

const handleInviteModal = () => {
if (isInviteModalOpen) return;
if (currentRole === "editor") {
addToast("Editor κΆŒν•œμœΌλ‘œλŠ” μ΄ˆλŒ€ν•  수 μ—†μŠ΅λ‹ˆλ‹€.");
return;
}
openInviteModal();
};
return (
<>
<button
Expand All @@ -92,7 +102,7 @@ export const MenuButton = () => {
<MenuIcon />
<p className={textBox}>{name ?? "Nocta"}</p>
</button>
<WorkspaceSelectModal isOpen={isOpen} userName={name} onInviteClick={openInviteModal} />
<WorkspaceSelectModal isOpen={isOpen} userName={name} onInviteClick={handleInviteModal} />
<InviteModal
isOpen={isInviteModalOpen}
onClose={closeInviteModal}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { WorkspaceListItem } from "@noctaCrdt/Interfaces"; // 이전에 λ§Œλ“ 
import { useSocketStore } from "@src/stores/useSocketStore";
import { useToastStore } from "@src/stores/useToastStore";
import { useUserInfo } from "@src/stores/useUserStore";
import { useWorkspaceStore } from "@src/stores/useWorkspaceStore";
import {
itemContainer,
itemContent,
Expand All @@ -28,10 +29,12 @@ export const WorkspaceSelectItem = ({
const { userId } = useUserInfo();
const { workspace, switchWorkspace } = useSocketStore();
const { addToast } = useToastStore();
const setCurrentRole = useWorkspaceStore((state) => state.setCurrentRole);
const isActive = workspace?.id === id; // ν˜„μž¬ μ›Œν¬μŠ€νŽ˜μ΄μŠ€ 확인
const handleClick = () => {
if (!isActive) {
switchWorkspace(userId, id);
setCurrentRole(role);
addToast(`μ›Œν¬μŠ€νŽ˜μ΄μŠ€(${name})에 μ ‘μ†ν•˜μ˜€μŠ΅λ‹ˆλ‹€.`);
}
};
Expand Down
35 changes: 34 additions & 1 deletion client/src/features/editor/components/block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,39 @@ export const Block: React.FC<BlockProps> = memo(
}
};

const handleKeyDown = (
e: React.KeyboardEvent<HTMLDivElement>,
blockRef: HTMLDivElement | null,
block: CRDTBlock,
) => {
switch (e.key) {
case e.metaKey && "b": {
e.preventDefault();
onTextStyleUpdate("bold", block.id, selectedNodes);
break;
}
case e.metaKey && "i": {
e.preventDefault();
onTextStyleUpdate("italic", block.id, selectedNodes);
break;
}
case e.metaKey && "u": {
e.preventDefault();
onTextStyleUpdate("underline", block.id, selectedNodes);
break;
}
case e.metaKey && "Shift" && "s": {
onTextStyleUpdate("strikethrough", block.id, selectedNodes);
e.preventDefault();
break;
}
default: {
onKeyDown(e, blockRef, block);
break;
}
}
};

const handleAnimationSelect = (animation: AnimationType) => {
onAnimationSelect(block.id, animation);
};
Expand Down Expand Up @@ -276,7 +309,7 @@ export const Block: React.FC<BlockProps> = memo(
<IconBlock type={block.type} index={block.listIndex} indent={block.indent} />
<div
ref={blockRef}
onKeyDown={(e) => onKeyDown(e, blockRef.current, block)}
onKeyDown={(e) => handleKeyDown(e, blockRef.current, block)}
onInput={handleInput}
onClick={(e) => onClick(block.id, e)}
onCopy={(e) => onCopy(e, blockRef.current, block)}
Expand Down
86 changes: 71 additions & 15 deletions client/src/features/editor/hooks/useCopyAndPaste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useCallback } from "react";
import { useSocketStore } from "@src/stores/useSocketStore";
import { EditorStateProps } from "../Editor";
import { getTextOffset } from "../utils/domSyncUtils";
import { checkMarkdownPattern } from "../utils/markdownPatterns";

interface ClipboardMetadata {
value: string;
Expand All @@ -26,7 +27,12 @@ export const useCopyAndPaste = ({
setEditorState,
isLocalChange,
}: UseCopyAndPasteProps) => {
const { sendCharInsertOperation, sendCharDeleteOperation } = useSocketStore();
const {
sendCharInsertOperation,
sendCharDeleteOperation,
sendBlockInsertOperation,
sendBlockUpdateOperation,
} = useSocketStore();

const handleCopy = useCallback(
(e: React.ClipboardEvent<HTMLDivElement>, blockRef: HTMLDivElement | null, block: Block) => {
Expand Down Expand Up @@ -123,25 +129,75 @@ export const useCopyAndPaste = ({
editorCRDT.currentBlock!.crdt.currentCaret = caretPosition + metadata.length;
} else {
const text = e.clipboardData.getData("text/plain");

if (!block || text.length === 0) return;

const caretPosition = block.crdt.currentCaret;

// ν…μŠ€νŠΈλ₯Ό ν•œ κΈ€μžμ”© 순차적으둜 μ‚½μž…
text.split("").forEach((char, index) => {
const insertPosition = caretPosition + index;
const charNode = block.crdt.localInsert(insertPosition, char, block.id, pageId);
sendCharInsertOperation({
type: "charInsert",
node: charNode.node,
blockId: block.id,
pageId,
if (text.includes("\n")) {
let currentBlockIndex = editorCRDT.LinkedList.spread().findIndex(
(b) => b.id === block.id,
);
const textList = text.split("\n");
textList.forEach((line, index) => {
console.log(line);
if (index === 0) {
line.split("").forEach((char, index) => {
const charNode = block.crdt.localInsert(index, char, block.id, pageId);
sendCharInsertOperation({
type: "charInsert",
node: charNode.node,
blockId: block.id,
pageId,
});
});
const isMarkdownGrammer = checkMarkdownPattern(line);
if (isMarkdownGrammer && block.type === "p") {
block.type = isMarkdownGrammer.type;
sendBlockUpdateOperation(editorCRDT.localUpdate(block, pageId));
for (let i = 0; i < isMarkdownGrammer.length; i++) {
sendCharDeleteOperation(block.crdt.localDelete(0, block.id, pageId));
}
}
} else {
const newBlock = editorCRDT.localInsert(currentBlockIndex, "");
sendBlockInsertOperation({
type: "blockInsert",
node: newBlock.node,
pageId,
});
line.split("").forEach((char, index) => {
sendCharInsertOperation(
newBlock.node.crdt.localInsert(index, char, newBlock.node.id, pageId),
);
});
const isMarkdownGrammer = checkMarkdownPattern(line);
if (isMarkdownGrammer && newBlock.node.type === "p") {
newBlock.node.type = isMarkdownGrammer.type;
sendBlockUpdateOperation(editorCRDT.localUpdate(newBlock.node, pageId));
for (let i = 0; i < isMarkdownGrammer.length; i++) {
sendCharDeleteOperation(
newBlock.node.crdt.localDelete(0, newBlock.node.id, pageId),
);
}
}
}
currentBlockIndex += 1;
});
});

// 캐럿 μœ„μΉ˜ μ—…λ°μ΄νŠΈ
editorCRDT.currentBlock!.crdt.currentCaret = caretPosition + text.length;
} else {
// ν…μŠ€νŠΈλ₯Ό ν•œ κΈ€μžμ”© 순차적으둜 μ‚½μž…
text.split("").forEach((char, index) => {
const insertPosition = caretPosition + index;
const charNode = block.crdt.localInsert(insertPosition, char, block.id, pageId);
sendCharInsertOperation({
type: "charInsert",
node: charNode.node,
blockId: block.id,
pageId,
});
});
// 캐럿 μœ„μΉ˜ μ—…λ°μ΄νŠΈ
editorCRDT.currentBlock!.crdt.currentCaret = caretPosition + text.length;
}
}

setEditorState({
Expand Down
14 changes: 13 additions & 1 deletion client/src/stores/useSocketStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "@noctaCrdt/Interfaces";
import { io, Socket } from "socket.io-client";
import { create } from "zustand";
import { useWorkspaceStore } from "./useWorkspaceStore";

class BatchProcessor {
private batch: any[] = [];
Expand Down Expand Up @@ -136,7 +137,7 @@ export const useSocketStore = create<SocketStore>((set, get) => ({

socket.on("workspace", (workspace: WorkSpaceSerializedProps) => {
const { setWorkspace } = get();
setWorkspace(workspace); // μˆ˜μ •λœ λΆ€λΆ„
setWorkspace(workspace);
});

socket.on("workspace/connections", (connections: Record<string, number>) => {
Expand All @@ -153,12 +154,23 @@ export const useSocketStore = create<SocketStore>((set, get) => ({

socket.on("workspace/list", (workspaces: WorkspaceListItem[]) => {
set({ availableWorkspaces: workspaces });
const { availableWorkspaces } = get();
console.log(availableWorkspaces);
const { workspace } = get();
const currentWorkspace = availableWorkspaces.find((ws) => ws.id === workspace!.id);
if (currentWorkspace) {
useWorkspaceStore.getState().setCurrentRole(currentWorkspace.role);
}
});

socket.on("error", (error: Error) => {
console.error("Socket error:", error);
});

socket.on("workspace/role", (data: { role: "owner" | "editor" }) => {
useWorkspaceStore.getState().setCurrentRole(data.role);
});

socket.connect();
},

Expand Down
15 changes: 15 additions & 0 deletions client/src/stores/useWorkspaceStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { create } from "zustand";

// μ›Œν¬μŠ€νŽ˜μ΄μŠ€ κΆŒν•œ νƒ€μž… μ •μ˜

interface WorkspaceStore {
// ν˜„μž¬ μ„ νƒλœ μ›Œν¬μŠ€νŽ˜μ΄μŠ€μ˜ κΆŒν•œ
currentRole: string | null;
// κΆŒν•œ μ„€μ • ν•¨μˆ˜
setCurrentRole: (role: string | null) => void;
}

export const useWorkspaceStore = create<WorkspaceStore>((set) => ({
currentRole: null,
setCurrentRole: (role) => set({ currentRole: role }),
}));
Loading

0 comments on commit 04aa78a

Please sign in to comment.