Skip to content

Commit 91cd801

Browse files
Merge pull request #36 from ChicoState/widget-todo
Implemented multi note tabs and pin/unpin functionality for NotePad w…
2 parents aad096f + 9f47728 commit 91cd801

File tree

4 files changed

+304
-25
lines changed

4 files changed

+304
-25
lines changed

package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/widgets/Notepad.css

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
height: 100%;
44
display: flex;
55
flex-direction: column;
6-
padding: 12px;
6+
padding: 8px;
77
}
88

99
.title {
10-
font-size: 1.2rem;
10+
display: flex;
11+
justify-content: center;
12+
align-items: center;
13+
gap: 8px;
14+
font-size: 1rem;
1115
font-weight: 700;
12-
margin-bottom: 10px;
16+
padding: 4px;
1317
color: #fffc;
1418
text-align: center;
1519
}
@@ -38,3 +42,117 @@
3842
border-color: #fff4;
3943
background-color: #0002;
4044
}
45+
46+
/* Tabs container */
47+
.tabs {
48+
display: flex;
49+
border-bottom: 1px solid #fff3;
50+
overflow: hidden;
51+
flex-wrap: wrap;
52+
}
53+
54+
/* Individual tabs */
55+
.tab {
56+
flex: 0 1 auto;
57+
min-width: 40px;
58+
max-width: 200px;
59+
padding: 4px 8px;
60+
margin-right: 4px;
61+
border-radius: 8px 8px 0 0;
62+
background-color: transparent;
63+
color: #fffc;
64+
cursor: pointer;
65+
display: flex;
66+
align-items: center;
67+
justify-content: space-between;
68+
gap: 6px;
69+
font-size: 0.8rem;
70+
transition:
71+
flex 0.2s ease,
72+
background-color 0.2s;
73+
}
74+
75+
.tab:hover {
76+
background-color: #fff1;
77+
}
78+
79+
.activeTab {
80+
background-color: #fff2;
81+
font-weight: 600;
82+
flex: 1 0 auto;
83+
}
84+
85+
.tab span {
86+
white-space: nowrap;
87+
overflow: hidden;
88+
text-overflow: ellipsis;
89+
flex-shrink: 1;
90+
}
91+
92+
/* Close button inside tabs */
93+
.closeBtn {
94+
border: none;
95+
background: none;
96+
color: #fff9;
97+
cursor: pointer;
98+
font-size: 0.9rem;
99+
flex-shrink: 0;
100+
}
101+
102+
.closeBtn:hover {
103+
color: #fff;
104+
}
105+
106+
/* Add button */
107+
.addBtn {
108+
flex-shrink: 0;
109+
/*height: 100%;*/
110+
padding: 4px 8px;
111+
margin-left: 6px;
112+
border: none;
113+
border-radius: 8px 8px 0 0;
114+
background-color: #fff1;
115+
color: #fff;
116+
cursor: pointer;
117+
font-size: 1.1rem;
118+
transition: background-color 0.2s;
119+
}
120+
121+
.addBtn:hover {
122+
background-color: #fff2;
123+
}
124+
125+
/* Inline rename input */
126+
.renameInput {
127+
background: transparent;
128+
border: none;
129+
outline: none;
130+
color: #fff;
131+
font-weight: 600;
132+
font-family: "Inter", sans-serif;
133+
font-size: 0.95rem;
134+
width: 100px;
135+
font-size: 0.8rem;
136+
background-color: #0004;
137+
border-radius: 4px;
138+
padding: 2px 4px;
139+
}
140+
141+
.renameInput:focus {
142+
background-color: #0006;
143+
}
144+
145+
.pinBtn {
146+
border: none;
147+
background: none;
148+
cursor: pointer;
149+
padding: 0;
150+
display: flex;
151+
align-items: center;
152+
flex-shrink: 0;
153+
transition: transform 0.2s ease;
154+
}
155+
156+
.pinBtn:hover {
157+
transform: scale(1.2);
158+
}

src/widgets/Notepad.tsx

Lines changed: 158 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,174 @@
11
import React, { useState, useEffect } from "react";
22
import globalStyles from "../App.css";
33
import styles from "./Notepad.css";
4+
import { PushPinIcon } from "@phosphor-icons/react";
5+
6+
interface NoteState {
7+
id: string;
8+
title: string;
9+
content: string;
10+
pinned: boolean;
11+
}
412

513
export function Notepad() {
6-
const [note, setNote] = useState("");
14+
const [notes, setNotes] = useState<NoteState[]>([
15+
{ id: "1", title: "Note 1", content: "", pinned: false },
16+
]);
17+
const [activeNoteId, setActiveNoteId] = useState("1");
18+
const [editingId, setEditingId] = useState<string>(null);
19+
const [tempTitle, setTempTitle] = useState("");
720

8-
// Load saved note on startup
21+
// Load from localStorage
922
useEffect(() => {
10-
const savedNote = localStorage.getItem("notepad-note");
11-
if (savedNote) setNote(savedNote);
23+
const saved = localStorage.getItem("multi-notes");
24+
if (saved) {
25+
const parsed = JSON.parse(saved);
26+
setNotes(parsed);
27+
if (parsed.length > 0) setActiveNoteId(parsed[0].id);
28+
} else {
29+
createNewNote(true);
30+
}
1231
}, []);
1332

14-
// Save note to localStorage on every change
33+
// Save to localStorage
1534
useEffect(() => {
16-
localStorage.setItem("notepad-note", note);
17-
}, [note]);
35+
localStorage.setItem("multi-notes", JSON.stringify(notes));
36+
}, [notes]);
37+
38+
const createNewNote = (isInitial = false) => {
39+
const id = Date.now().toString();
40+
const nextNumber = isInitial || notes.length === 0 ? 1 : notes.length + 1;
41+
const newNote = {
42+
id,
43+
title: `Note ${nextNumber}`,
44+
content: "",
45+
pinned: false,
46+
};
47+
setNotes((prev) => [...prev, newNote]);
48+
setActiveNoteId(id);
49+
};
50+
51+
const deleteNote = (id: string) => {
52+
const remaining = notes.filter((n) => n.id !== id);
53+
setNotes(remaining);
54+
if (remaining.length > 0) {
55+
setActiveNoteId(remaining[0].id);
56+
} else {
57+
createNewNote(true);
58+
}
59+
};
60+
61+
const renameNote = (id: string, newTitle: string) => {
62+
setNotes((prev) =>
63+
prev.map((n) => (n.id === id ? { ...n, title: newTitle } : n)),
64+
);
65+
};
66+
67+
const updateContent = (id: string, newContent: string) => {
68+
setNotes((prev) =>
69+
prev.map((n) => (n.id === id ? { ...n, content: newContent } : n)),
70+
);
71+
};
72+
73+
const togglePin = (id: string) => {
74+
setNotes(
75+
(prev) =>
76+
prev
77+
.map((n) => (n.id === id ? { ...n, pinned: !n.pinned } : n))
78+
.sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0)), // pinned notes first
79+
);
80+
};
81+
82+
const startEditing = (id: string, currentTitle: string) => {
83+
setEditingId(id);
84+
setTempTitle(currentTitle);
85+
};
86+
87+
const finishEditing = () => {
88+
if (editingId) renameNote(editingId, tempTitle.trim() || "Untitled");
89+
setEditingId(null);
90+
setTempTitle("");
91+
};
92+
93+
const activeNote = notes.find((n) => n.id === activeNoteId);
1894

1995
return (
2096
<div className={[globalStyles.container, styles.body].join(" ")}>
21-
<h3 className={styles.title}>My Notes</h3>
22-
<textarea
23-
className={[globalStyles.container, styles.textarea].join(" ")}
24-
value={note}
25-
onChange={(e) => setNote(e.target.value)}
26-
placeholder="Type your notes here..."
27-
/>
97+
{/* Tabs */}
98+
<div className={styles.tabs}>
99+
{notes.map((note) => (
100+
<div
101+
key={note.id}
102+
className={`${styles.tab} ${
103+
note.id === activeNoteId ? styles.activeTab : ""
104+
}`}
105+
onClick={() => setActiveNoteId(note.id)}
106+
>
107+
{editingId === note.id ? (
108+
<input
109+
className={styles.renameInput}
110+
value={tempTitle}
111+
autoFocus
112+
onChange={(e) => setTempTitle(e.target.value)}
113+
onBlur={finishEditing}
114+
onKeyDown={(e) => e.key === "Enter" && finishEditing()}
115+
/>
116+
) : (
117+
<span
118+
onDoubleClick={() => startEditing(note.id, note.title)}
119+
title="Double-click to rename"
120+
>
121+
{note.title}
122+
</span>
123+
)}
124+
125+
{/* Close button */}
126+
<button
127+
className={styles.closeBtn}
128+
onClick={(e) => {
129+
e.stopPropagation();
130+
deleteNote(note.id);
131+
}}
132+
>
133+
×
134+
</button>
135+
</div>
136+
))}
137+
<button className={styles.addBtn} onClick={() => createNewNote()}>
138+
+
139+
</button>
140+
</div>
141+
142+
{/* Note editor */}
143+
{activeNote && (
144+
<>
145+
<h3 className={styles.title}>
146+
{activeNote.title}
147+
148+
{/* Pin button using Phosphor PinIcon */}
149+
<button
150+
className={styles.pinBtn}
151+
onClick={(e) => {
152+
e.stopPropagation();
153+
togglePin(activeNote.id);
154+
}}
155+
title={activeNote.pinned ? "Unpin note" : "Pin note"}
156+
>
157+
<PushPinIcon
158+
size={16}
159+
weight={activeNote.pinned ? "fill" : "regular"}
160+
color={activeNote.pinned ? "#ffd700" : "#fff9"}
161+
/>
162+
</button>
163+
</h3>
164+
<textarea
165+
className={[globalStyles.container, styles.textarea].join(" ")}
166+
value={activeNote.content}
167+
onChange={(e) => updateContent(activeNote.id, e.target.value)}
168+
placeholder="Type your notes here..."
169+
/>
170+
</>
171+
)}
28172
</div>
29173
);
30174
}

0 commit comments

Comments
 (0)