feat: add confetti animation on thought submission and update dependencies
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { AuthProvider } from "@/hooks/use-auth";
|
import { AuthProvider } from "@/hooks/use-auth";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
@@ -50,6 +50,7 @@
|
|||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tone": "^15.1.22",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.1.5",
|
"zod": "^4.1.5",
|
||||||
},
|
},
|
||||||
@@ -474,6 +475,8 @@
|
|||||||
|
|
||||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||||
|
|
||||||
|
"automation-events": ["automation-events@7.1.12", "", { "dependencies": { "@babel/runtime": "^7.28.3", "tslib": "^2.8.1" } }, "sha512-JDdPQoV58WPm15/L3ABtIEiqyxLoW+yTYIEqYtrKZ7VizLSRXhMKRZbQ8CYc2mFq/lMRKUvqOj0OcT3zANFiXA=="],
|
||||||
|
|
||||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||||
|
|
||||||
"axe-core": ["axe-core@4.10.3", "", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="],
|
"axe-core": ["axe-core@4.10.3", "", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="],
|
||||||
@@ -992,6 +995,8 @@
|
|||||||
|
|
||||||
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
||||||
|
|
||||||
|
"standardized-audio-context": ["standardized-audio-context@25.3.77", "", { "dependencies": { "@babel/runtime": "^7.25.6", "automation-events": "^7.0.9", "tslib": "^2.7.0" } }, "sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A=="],
|
||||||
|
|
||||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||||
|
|
||||||
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
||||||
@@ -1030,6 +1035,8 @@
|
|||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"tone": ["tone@15.1.22", "", { "dependencies": { "standardized-audio-context": "^25.3.70", "tslib": "^2.3.1" } }, "sha512-TCScAGD4sLsama5DjvTUXlLDXSqPealhL64nsdV1hhr6frPWve0DeSo63AKnSJwgfg55fhvxj0iPPRwPN5o0ag=="],
|
||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
||||||
|
|
||||||
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
||||||
|
127
thoughts-frontend/components/confetti.tsx
Normal file
127
thoughts-frontend/components/confetti.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import * as Tone from "tone";
|
||||||
|
|
||||||
|
interface ConfettiProps {
|
||||||
|
fire: boolean;
|
||||||
|
onComplete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = ["#26ccff", "#a25afd", "#ff5e7e", "#88ff5a", "#fcff42"];
|
||||||
|
|
||||||
|
export function Confetti({ fire, onComplete }: ConfettiProps) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fire) {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const synth = new Tone.PolySynth(Tone.Synth, {
|
||||||
|
oscillator: { type: "sine" },
|
||||||
|
envelope: { attack: 0.005, decay: 0.1, sustain: 0.3, release: 1 },
|
||||||
|
}).toDestination();
|
||||||
|
|
||||||
|
const notes = ["C4", "E4", "G4", "A4"];
|
||||||
|
|
||||||
|
let animationFrameId: number;
|
||||||
|
const confetti: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
r: number;
|
||||||
|
d: number;
|
||||||
|
color: string;
|
||||||
|
tilt: number;
|
||||||
|
}[] = [];
|
||||||
|
const numConfetti = 100;
|
||||||
|
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
};
|
||||||
|
window.addEventListener("resize", resizeCanvas);
|
||||||
|
resizeCanvas();
|
||||||
|
|
||||||
|
for (let i = 0; i < numConfetti; i++) {
|
||||||
|
confetti.push({
|
||||||
|
x: Math.random() * canvas.width,
|
||||||
|
y: -20,
|
||||||
|
r: Math.random() * 6 + 1,
|
||||||
|
d: Math.random() * numConfetti,
|
||||||
|
color: colors[Math.floor(Math.random() * colors.length)],
|
||||||
|
tilt: Math.floor(Math.random() * 10) - 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let animationFinished = false;
|
||||||
|
|
||||||
|
const draw = () => {
|
||||||
|
if (animationFinished) return;
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
let allOffScreen = true;
|
||||||
|
|
||||||
|
for (let i = 0; i < numConfetti; i++) {
|
||||||
|
const c = confetti[i];
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineWidth = c.r / 2;
|
||||||
|
ctx.strokeStyle = c.color;
|
||||||
|
ctx.moveTo(c.x + c.tilt, c.y);
|
||||||
|
ctx.lineTo(c.x, c.y + c.tilt + c.r);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
c.y += Math.cos(c.d + i + 1.2) + 1.5 + c.r / 2;
|
||||||
|
c.x += Math.sin(i) * 1.5;
|
||||||
|
|
||||||
|
if (c.y <= canvas.height) {
|
||||||
|
allOffScreen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allOffScreen) {
|
||||||
|
animationFinished = true;
|
||||||
|
onComplete();
|
||||||
|
} else {
|
||||||
|
animationFrameId = requestAnimationFrame(draw);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
Tone.start();
|
||||||
|
const now = Tone.now();
|
||||||
|
notes.forEach((note, i) => {
|
||||||
|
synth.triggerAttackRelease(note, "8n", now + i * 0.1);
|
||||||
|
});
|
||||||
|
draw();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Audio could not be started", error);
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", resizeCanvas);
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [fire, onComplete]);
|
||||||
|
|
||||||
|
if (!fire) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
pointerEvents: "none",
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -25,10 +25,13 @@ import { CreateThoughtSchema, createThought } from "@/lib/api";
|
|||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Globe, Lock, Users } from "lucide-react";
|
import { Globe, Lock, Users } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Confetti } from "./confetti";
|
||||||
|
|
||||||
export function PostThoughtForm() {
|
export function PostThoughtForm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
const [showConfetti, setShowConfetti] = useState(false);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof CreateThoughtSchema>>({
|
const form = useForm<z.infer<typeof CreateThoughtSchema>>({
|
||||||
resolver: zodResolver(CreateThoughtSchema),
|
resolver: zodResolver(CreateThoughtSchema),
|
||||||
@@ -44,6 +47,7 @@ export function PostThoughtForm() {
|
|||||||
try {
|
try {
|
||||||
await createThought(values, token);
|
await createThought(values, token);
|
||||||
toast.success("Your thought has been posted!");
|
toast.success("Your thought has been posted!");
|
||||||
|
setShowConfetti(true);
|
||||||
form.reset();
|
form.reset();
|
||||||
router.refresh(); // This is the key to updating the feed
|
router.refresh(); // This is the key to updating the feed
|
||||||
} catch {
|
} catch {
|
||||||
@@ -52,67 +56,70 @@ export function PostThoughtForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<>
|
||||||
<CardContent className="p-4">
|
<Confetti fire={showConfetti} onComplete={() => setShowConfetti(false)} />
|
||||||
<Form {...form}>
|
<Card>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<CardContent className="p-4">
|
||||||
<FormField
|
<Form {...form}>
|
||||||
control={form.control}
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
name="content"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder="What's on your mind?"
|
|
||||||
className="resize-none"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="visibility"
|
name="content"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Select
|
<FormItem>
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className="w-[150px]">
|
<Textarea
|
||||||
<SelectValue placeholder="Visibility" />
|
placeholder="What's on your mind?"
|
||||||
</SelectTrigger>
|
className="resize-none"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<FormMessage />
|
||||||
<SelectItem value="Public">
|
</FormItem>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Globe className="h-4 w-4" /> Public
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="FriendsOnly">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Users className="h-4 w-4" /> Friends Only
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="Private">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Lock className="h-4 w-4" /> Private
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
<div className="flex justify-between items-center">
|
||||||
{form.formState.isSubmitting ? "Posting..." : "Post Thought"}
|
<FormField
|
||||||
</Button>
|
control={form.control}
|
||||||
</div>
|
name="visibility"
|
||||||
</form>
|
render={({ field }) => (
|
||||||
</Form>
|
<Select
|
||||||
</CardContent>
|
onValueChange={field.onChange}
|
||||||
</Card>
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="w-[150px]">
|
||||||
|
<SelectValue placeholder="Visibility" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Public">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Globe className="h-4 w-4" /> Public
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="FriendsOnly">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="h-4 w-4" /> Friends Only
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="Private">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Lock className="h-4 w-4" /> Private
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||||
|
{form.formState.isSubmitting ? "Posting..." : "Post Thought"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
// components/reply-form.tsx
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -17,6 +16,8 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { CreateThoughtSchema, createThought } from "@/lib/api";
|
import { CreateThoughtSchema, createThought } from "@/lib/api";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Confetti } from "./confetti";
|
||||||
|
|
||||||
interface ReplyFormProps {
|
interface ReplyFormProps {
|
||||||
parentThoughtId: string;
|
parentThoughtId: string;
|
||||||
@@ -26,6 +27,7 @@ interface ReplyFormProps {
|
|||||||
export function ReplyForm({ parentThoughtId, onReplySuccess }: ReplyFormProps) {
|
export function ReplyForm({ parentThoughtId, onReplySuccess }: ReplyFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
const [showConfetti, setShowConfetti] = useState(false);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof CreateThoughtSchema>>({
|
const form = useForm<z.infer<typeof CreateThoughtSchema>>({
|
||||||
resolver: zodResolver(CreateThoughtSchema),
|
resolver: zodResolver(CreateThoughtSchema),
|
||||||
@@ -46,45 +48,50 @@ export function ReplyForm({ parentThoughtId, onReplySuccess }: ReplyFormProps) {
|
|||||||
await createThought(values, token);
|
await createThought(values, token);
|
||||||
toast.success("Your reply has been posted!");
|
toast.success("Your reply has been posted!");
|
||||||
form.reset();
|
form.reset();
|
||||||
onReplySuccess(); // Call the callback
|
setShowConfetti(true);
|
||||||
router.refresh(); // Refresh the page to show the new reply
|
console.log("Showing confetti");
|
||||||
} catch (err) {
|
onReplySuccess();
|
||||||
|
router.refresh();
|
||||||
|
} catch {
|
||||||
toast.error("Failed to post reply. Please try again.");
|
toast.error("Failed to post reply. Please try again.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 p-4">
|
<Confetti fire={showConfetti} onComplete={() => setShowConfetti(false)} />
|
||||||
<FormField
|
<Form {...form}>
|
||||||
control={form.control}
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 p-4">
|
||||||
name="content"
|
<FormField
|
||||||
render={({ field }) => (
|
control={form.control}
|
||||||
<FormItem>
|
name="content"
|
||||||
<FormControl>
|
render={({ field }) => (
|
||||||
<Textarea
|
<FormItem>
|
||||||
placeholder="Post your reply..."
|
<FormControl>
|
||||||
className="resize-none bg-white glass-effect glossy-efect bottom shadow-fa-sm"
|
<Textarea
|
||||||
{...field}
|
placeholder="Post your reply..."
|
||||||
/>
|
className="resize-none bg-white glass-effect glossy-efect bottom shadow-fa-sm"
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage />
|
/>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
<div className="flex justify-end gap-2">
|
)}
|
||||||
<Button
|
/>
|
||||||
type="button"
|
<div className="flex justify-end gap-2">
|
||||||
variant="ghost"
|
<Button
|
||||||
onClick={onReplySuccess} // Close button
|
type="button"
|
||||||
>
|
variant="ghost"
|
||||||
Cancel
|
onClick={onReplySuccess} // Close button
|
||||||
</Button>
|
>
|
||||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
Cancel
|
||||||
{form.formState.isSubmitting ? "Replying..." : "Reply"}
|
</Button>
|
||||||
</Button>
|
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||||
</div>
|
{form.formState.isSubmitting ? "Replying..." : "Reply"}
|
||||||
</form>
|
</Button>
|
||||||
</Form>
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -55,6 +55,7 @@
|
|||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tone": "^15.1.22",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user