Skip to content

Commit d12c617

Browse files
committed
feat: add placeholder extension to Imessage component and update styles for improved user experience
1 parent a515789 commit d12c617

File tree

5 files changed

+114
-57
lines changed

5 files changed

+114
-57
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@radix-ui/react-label": "^2.1.4",
1818
"@radix-ui/react-select": "^2.2.2",
1919
"@radix-ui/react-slot": "^1.2.0",
20+
"@tiptap/extension-placeholder": "^2.11.9",
2021
"@tiptap/pm": "^2.11.9",
2122
"@tiptap/react": "^2.11.9",
2223
"@tiptap/starter-kit": "^2.11.9",

pnpm-lock.yaml

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

src/components/animations/imessage.tsx

Lines changed: 91 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
"use client";
22

3-
import { Mic, PlusIcon, Sticker } from "lucide-react";
4-
import { AnimatePresence, motion, Variants } from "motion/react";
3+
import { cn } from "@/lib/utils";
54
import { EditorContent, useEditor } from "@tiptap/react";
5+
import Placeholder from "@tiptap/extension-placeholder";
66
import Starter from "@tiptap/starter-kit";
7+
import { Mic, PlusIcon, Sticker, X } from "lucide-react";
8+
import { AnimatePresence, motion, Variants } from "motion/react";
79
import { useState } from "react";
810

911
const transitionDebug = {
1012
type: "easeOut",
1113
duration: 0.2,
1214
};
1315

16+
const selectedMessageVariants: Variants = {
17+
initial: {
18+
y: 50,
19+
},
20+
animate: {
21+
y: 0,
22+
},
23+
};
1424
const Imessage = () => {
1525
const [messages, setMessages] = useState<
1626
{
1727
id: number;
1828
text: string;
1929
}[]
2030
>([]);
31+
const [selectedMessage, setSelectedMessage] = useState<string | null>(null);
32+
2133
const [newMessage, setNewMessage] = useState<string>("");
2234

2335
const handleSubmit = () => {
@@ -33,71 +45,102 @@ const Imessage = () => {
3345
<div className="flex flex-col justify-end items-end gap-0.5 flex-1 h-full w-full overflow-hidden">
3446
<AnimatePresence mode="wait">
3547
{messages.map((message) => (
36-
<motion.div
48+
<div
3749
key={message.id}
38-
layout="position"
39-
className="z-10 max-w-[80%] break-words rounded bg-background"
40-
layoutId={`container-[${messages.length - 1}]`}
41-
transition={transitionDebug}
50+
className="flex flex-col items-end w-full"
51+
onDoubleClick={() => setSelectedMessage(message.text)}
4252
>
43-
<div className="px-3 py-2 text-base ">{message.text}</div>
44-
</motion.div>
53+
<motion.div
54+
layoutId={`container-[${messages.length - 1}]`}
55+
transition={transitionDebug}
56+
layout="position"
57+
className="px-3 py-2 text-base max-w-[80%] bg-background z-10 break-words rounded"
58+
>
59+
{message.text}
60+
</motion.div>
61+
</div>
4562
))}
4663
</AnimatePresence>
4764
</div>
48-
<div className="flex w-full items-end bg-background rounded-[22px] p-1">
49-
<div className="flex gap-0.5">
50-
<Button>
51-
<PlusIcon className="size-5" />
52-
</Button>
53-
<Button>
54-
<Sticker className="size-5" />
65+
<motion.div
66+
className="w-full flex flex-col bg-background p-2 gap-2 relative overflow-hidden"
67+
animate={{
68+
borderRadius: selectedMessage ? "10px 10px 22px 22px" : "28px",
69+
}}
70+
>
71+
<AnimatePresence>
72+
{selectedMessage && (
73+
<motion.div
74+
variants={selectedMessageVariants}
75+
initial="initial"
76+
animate="animate"
77+
exit="initial"
78+
className="w-full bg-muted h-20 rounded-md relative p-2 z-0"
79+
>
80+
<p className="text-sm line-clamp-2 w-full text-muted-foreground">
81+
{selectedMessage}
82+
</p>
83+
<Button
84+
className="absolute top-0 right-0 size-6 center"
85+
onClick={() => setSelectedMessage(null)}
86+
>
87+
<X className="size-4" />
88+
</Button>
89+
</motion.div>
90+
)}
91+
</AnimatePresence>
92+
<div className="flex w-full items-end z-[9999999] bg-background">
93+
<div className="flex gap-0.5">
94+
<Button>
95+
<PlusIcon className="size-5" />
96+
</Button>
97+
<Button>
98+
<Sticker className="size-5" />
99+
</Button>
100+
</div>
101+
<Input setMessage={setNewMessage} handleSubmit={handleSubmit} />
102+
<motion.div
103+
key={messages.length}
104+
layout="position"
105+
className="pointer-events-none absolute z-10 flex h-9 w-[80%] items-center overflow-hidden break-words rounded-full [word-break:break-word] "
106+
layoutId={`container-[${messages.length}]`}
107+
transition={transitionDebug}
108+
initial={{ opacity: 0.6, zIndex: -1 }}
109+
animate={{ opacity: 0.6, zIndex: -1 }}
110+
exit={{ opacity: 1, zIndex: 1 }}
111+
>
112+
<div className="">{newMessage}</div>
113+
</motion.div>
114+
<Button className="size-10 group hover:bg-[#1daa61] rounded-full center transition-all duration-300">
115+
<Mic className="" />
55116
</Button>
56117
</div>
57-
<Input setMessage={setNewMessage} handleSubmit={handleSubmit} />
58-
<motion.div
59-
key={messages.length}
60-
layout="position"
61-
className="pointer-events-none absolute z-10 flex h-9 w-[80%] items-center overflow-hidden break-words rounded-full [word-break:break-word] "
62-
layoutId={`container-[${messages.length}]`}
63-
transition={transitionDebug}
64-
initial={{ opacity: 0.6, zIndex: -1 }}
65-
animate={{ opacity: 0.6, zIndex: -1 }}
66-
exit={{ opacity: 1, zIndex: 1 }}
67-
>
68-
<div className="">{newMessage}</div>
69-
</motion.div>
70-
<Button>
71-
<Mic className="size-5" />
72-
</Button>
73-
</div>
118+
</motion.div>
74119
</div>
75120
);
76121
};
77122

78123
const Button = ({
79124
children,
80-
onClick,
81-
}: {
125+
className,
126+
...props
127+
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
82128
children: React.ReactNode;
83-
onClick?: () => void;
84129
}) => {
85130
return (
86131
<button
87132
type="submit"
88-
className="flex size-10 items-center justify-center bg-background rounded-full hover:bg-muted transition-all duration-300"
89-
onClick={onClick}
133+
className={cn(
134+
"flex size-10 items-center justify-center bg-background rounded-full hover:bg-muted transition-all duration-300",
135+
className
136+
)}
137+
{...props}
90138
>
91139
{children}
92140
</button>
93141
);
94142
};
95143

96-
const paragraphVariants: Variants = {
97-
hidden: { opacity: 0, x: 10, filter: "blur(10px)" },
98-
visible: { opacity: 1, x: 0, filter: "blur(0px)" },
99-
};
100-
101144
const Input = ({
102145
setMessage,
103146
handleSubmit,
@@ -107,10 +150,12 @@ const Input = ({
107150
}) => {
108151
const editor = useEditor({
109152
extensions: [
110-
Starter.configure({
111-
italic: false,
153+
Starter,
154+
Placeholder.configure({
155+
placeholder: "Type a message",
112156
}),
113157
],
158+
114159
onUpdate: ({ editor }) => {
115160
setMessage(editor.getText());
116161
},
@@ -127,19 +172,9 @@ const Input = ({
127172
<div className="flex flex-1 relative">
128173
<EditorContent
129174
editor={editor}
130-
className="max-h-96 overflow-y-auto min-h-10 outline-none py-2 w-full px-1"
175+
className="max-h-96 overflow-y-auto min-h-10 outline-none py-2 w-full px-1 bg-background"
131176
onKeyDown={handleKeyDown}
132-
starter-kit
133177
/>
134-
<motion.p
135-
variants={paragraphVariants}
136-
animate={editor?.getText().length === 0 ? "visible" : "hidden"}
137-
exit="hidden"
138-
initial="hidden"
139-
className="absolute left-1 top-0 bottom-0 my-auto h-fit text-neutral-500"
140-
>
141-
Type a message
142-
</motion.p>
143178
</div>
144179
);
145180
};

src/constants/components.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const COMPONENTS: Component[] = [
2727
href: "https://x.com/alisamadi__/status/1914333366394212708",
2828
},
2929
{
30-
name: "Management bottom bar",
30+
name: "Custom Textarea",
3131
component: Animated.CustomTextarea,
3232
href: "",
3333
notReady: true,

src/index.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,10 @@
150150
);
151151
background-size: 10px 10px;
152152
}
153+
.tiptap p.is-editor-empty:first-child::before {
154+
color: hsl(var(--muted-foreground));
155+
content: attr(data-placeholder);
156+
float: left;
157+
height: 0;
158+
pointer-events: none;
159+
}

0 commit comments

Comments
 (0)