Skip to content

Commit 1f8d0c2

Browse files
committed
feat: builded doc page and text editor simple for test only
1 parent de4954a commit 1f8d0c2

5 files changed

Lines changed: 428 additions & 22 deletions

File tree

Client/src/api/document.api.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// src/api/document.api.ts
21
import axios from "axios";
32

43
const api = axios.create({
@@ -7,18 +6,18 @@ const api = axios.create({
76
});
87

98
export interface CreateDocumentDto {
10-
userId: string;
11-
role: string;
9+
title: string;
10+
content: string;
1211
}
1312

1413
export interface DocumentType {
15-
id: string; // include id so store can map/compare
14+
id: string;
1615
title: string;
1716
content: string;
1817
}
1918

2019
export interface UpdateDocDto {
21-
title?: string; // fixed typo from tile -> title
20+
title?: string;
2221
content?: string;
2322
}
2423

@@ -36,7 +35,7 @@ export const documentApi = {
3635
documentId: string
3736
): Promise<DocumentType> => {
3837
const res = await api.get(
39-
`/workspace/${workspaceId}/document/${documentId}` // added leading /
38+
`/workspace/${workspaceId}/document/${documentId}`
4039
);
4140
return res.data;
4241
},

Client/src/app/router.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import LoginPage from "../pages/LoginPage";
66
import ProtectedRoute from "../components/ProtectedRoute";
77
import DashBoard from "../pages/DashBoard";
88
import SignupPage from "../pages/SignupPage";
9+
import DocumentsPage from "../pages/DocumentPage";
10+
import DocumentEditorPage from "../pages/DocumentEditorPage";
911
import RedirectIfLoggedIn from "../components/RedirectIfLoggedIn";
1012

1113
const router = createBrowserRouter([
@@ -44,6 +46,24 @@ const router = createBrowserRouter([
4446
</ProtectedRoute>
4547
),
4648
},
49+
50+
{
51+
path: "workspace/:workspaceId/documents",
52+
element: (
53+
<ProtectedRoute>
54+
<DocumentsPage />
55+
</ProtectedRoute>
56+
),
57+
},
58+
59+
{
60+
path: "workspace/:workspaceId/document/:documentId",
61+
element: (
62+
<ProtectedRoute>
63+
<DocumentEditorPage />
64+
</ProtectedRoute>
65+
),
66+
},
4767
],
4868
},
4969
]);

Client/src/pages/DashBoard.tsx

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useEffect, useState } from "react";
22
import { ChevronDown } from "lucide-react";
3+
import { useNavigate } from "react-router-dom";
34
import { useAuthUser } from "../store/auth.store";
45

56
import {
@@ -24,6 +25,7 @@ const ROLE_LABELS: WorkspaceRole[] = ["Owner", "Admin", "Editor", "Viewer"];
2425
const Dashboard: React.FC = () => {
2526
const user = useAuthUser();
2627
const userId = user?.id;
28+
const navigate = useNavigate();
2729

2830
const workspaces = useWorkspaces();
2931
const isLoading = useWorkspaceLoading();
@@ -250,29 +252,47 @@ const Dashboard: React.FC = () => {
250252
return (
251253
<div
252254
key={ws.id}
253-
className="group flex flex-col rounded-2xl border border-border bg-card p-6 shadow-sm hover:shadow-md hover:border-primary/40 transition-all duration-200"
255+
className="group flex flex-col rounded-2xl border border-border bg-card p-6 shadow-sm hover:shadow-md hover:border-primary/40 hover:cursor-pointer transition-all duration-200"
254256
>
255-
<div className="flex items-start justify-between gap-4">
256-
<div className="min-w-0">
257-
<h3 className="truncate text-lg font-semibold group-hover:text-primary transition-colors">
258-
{ws.name || "Untitled workspace"}
259-
</h3>
260-
<p className="mt-1 text-xs text-muted-foreground">
261-
{new Date(ws.createdAt).toLocaleDateString()}
262-
</p>
257+
<div
258+
className="flex flex-col h-full"
259+
role="button"
260+
tabIndex={0}
261+
onClick={() =>
262+
navigate(`/workspace/${ws.id}/documents`)
263+
}
264+
onKeyDown={(e) => {
265+
if (e.key === "Enter" || e.key === " ") {
266+
e.preventDefault();
267+
navigate(`/workspace/${ws.id}/documents`);
268+
}
269+
}}
270+
>
271+
<div className="flex items-start justify-between gap-4">
272+
<div className="min-w-0">
273+
<h3 className="truncate text-lg font-semibold group-hover:text-primary transition-colors">
274+
{ws.name || "Untitled workspace"}
275+
</h3>
276+
<p className="mt-1 text-xs text-muted-foreground">
277+
{new Date(ws.createdAt).toLocaleDateString()}
278+
</p>
279+
</div>
280+
281+
<span
282+
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold text-white shadow-sm ${color}`}
283+
>
284+
{role}
285+
</span>
263286
</div>
264-
265-
<span
266-
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold text-white shadow-sm ${color}`}
267-
>
268-
{role}
269-
</span>
270287
</div>
271288

272289
{(role === "Owner" || role === "Admin") && (
273290
<div className="mt-6 pt-4 border-t border-border flex items-center justify-between text-xs text-muted-foreground">
274291
<button
275-
onClick={() => handleInvite(ws.id)}
292+
onClick={(e) => {
293+
e.stopPropagation();
294+
handleInvite(ws.id);
295+
}}
276296
className="font-semibold text-primary hover:text-primary/80 transition-colors"
277297
aria-label={`Invite member to ${ws.name}`}
278298
>
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import React, { useEffect, useState } from "react";
2+
import { useParams, useNavigate } from "react-router-dom";
3+
import { ChevronLeft, Save, Trash2 } from "lucide-react";
4+
import { useDocumentStore } from "../store/document.store";
5+
import { useWorkspaces } from "../store/workspace.store";
6+
// import { useAuthUser } from "../store/auth.store";
7+
8+
const DocumentEditorPage: React.FC = () => {
9+
const { workspaceId, documentId } = useParams<{
10+
workspaceId: string;
11+
documentId: string;
12+
}>();
13+
const navigate = useNavigate();
14+
15+
// const user = useAuthUser();
16+
const workspaces = useWorkspaces();
17+
18+
const {
19+
current: document,
20+
isLoading,
21+
error,
22+
fetchOne,
23+
update,
24+
remove,
25+
} = useDocumentStore();
26+
27+
const [title, setTitle] = useState("");
28+
const [content, setContent] = useState("");
29+
const [isSaving, setIsSaving] = useState(false);
30+
31+
const workspace = workspaces.find((w) => w.id === workspaceId);
32+
const canEdit =
33+
workspace?.currentUserRole === "Owner" ||
34+
workspace?.currentUserRole === "Admin";
35+
36+
useEffect(() => {
37+
if (workspaceId && documentId) {
38+
fetchOne(workspaceId, documentId);
39+
}
40+
}, [workspaceId, documentId, fetchOne]);
41+
42+
useEffect(() => {
43+
if (document) {
44+
setTitle(document.title);
45+
setContent(document.content);
46+
}
47+
}, [document]);
48+
49+
const handleSave = async () => {
50+
if (!workspaceId || !documentId || !title.trim()) return;
51+
52+
setIsSaving(true);
53+
try {
54+
await update(workspaceId, documentId, { title, content });
55+
} finally {
56+
setIsSaving(false);
57+
}
58+
};
59+
60+
const handleDelete = async () => {
61+
if (!confirm("Delete this document? This cannot be undone.")) return;
62+
if (!workspaceId || !documentId) return;
63+
64+
await remove(workspaceId, documentId);
65+
navigate(`/workspace/${workspaceId}/documents`);
66+
};
67+
68+
if (isLoading) {
69+
return (
70+
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
71+
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-primary" />
72+
<p className="mt-3 text-sm">Loading document...</p>
73+
</div>
74+
);
75+
}
76+
77+
if (error) {
78+
return (
79+
<div className="rounded-xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive-foreground p-8">
80+
{error}
81+
<button
82+
onClick={() => navigate(`/workspace/${workspaceId}/documents`)}
83+
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 font-medium"
84+
>
85+
Back to Documents
86+
</button>
87+
</div>
88+
);
89+
}
90+
91+
if (!document) {
92+
return (
93+
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground text-center">
94+
<p className="text-lg font-semibold mb-2">Document not found</p>
95+
<button
96+
onClick={() => navigate(`/workspace/${workspaceId}/documents`)}
97+
className="px-6 py-2 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 font-medium"
98+
>
99+
Back to Documents
100+
</button>
101+
</div>
102+
);
103+
}
104+
105+
return (
106+
<div className="space-y-6 h-full flex flex-col">
107+
{/* Header */}
108+
<div className="flex items-center justify-between">
109+
<div className="flex items-center gap-3">
110+
<button
111+
onClick={() => navigate(`/workspace/${workspaceId}/documents`)}
112+
className="flex items-center gap-2 text-sm font-medium text-primary hover:text-primary/80 p-2 rounded-lg hover:bg-primary/5"
113+
>
114+
<ChevronLeft className="h-4 w-4" />
115+
Documents
116+
</button>
117+
<div>
118+
<h1 className="text-2xl font-semibold">{workspace?.name}</h1>
119+
</div>
120+
</div>
121+
<div className="flex items-center gap-2">
122+
{canEdit && (
123+
<>
124+
<button
125+
onClick={handleSave}
126+
disabled={isSaving}
127+
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm font-medium text-sm disabled:opacity-50 disabled:cursor-not-allowed transition-all"
128+
>
129+
<Save className="h-4 w-4" />
130+
{isSaving ? "Saving..." : "Save"}
131+
</button>
132+
<button
133+
onClick={handleDelete}
134+
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm font-medium text-sm transition-all"
135+
>
136+
<Trash2 className="h-4 w-4" />
137+
Delete
138+
</button>
139+
</>
140+
)}
141+
</div>
142+
</div>
143+
144+
{/* Editor */}
145+
<div className="flex-1 flex flex-col bg-card rounded-3xl border border-border shadow-sm overflow-hidden">
146+
<input
147+
value={title}
148+
onChange={(e) => setTitle(e.target.value)}
149+
placeholder="Untitled document"
150+
className="w-full px-8 py-6 text-3xl font-bold bg-transparent border-b border-border focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 resize-none"
151+
disabled={!canEdit}
152+
/>
153+
<textarea
154+
value={content}
155+
onChange={(e) => setContent(e.target.value)}
156+
placeholder="Start writing your document..."
157+
className="flex-1 w-full px-8 py-6 text-foreground bg-transparent resize-none focus:outline-none document-editor-textarea"
158+
disabled={!canEdit}
159+
/>
160+
</div>
161+
</div>
162+
);
163+
};
164+
165+
export default DocumentEditorPage;

0 commit comments

Comments
 (0)