diff --git a/package-lock.json b/package-lock.json
index be7d4ff..753e2e1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,9 +10,11 @@
"dependencies": {
"@lexical/react": "^0.34.0",
"@lexical/rich-text": "^0.34.0",
+ "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
@@ -26,6 +28,7 @@
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"emoji-picker-react": "^4.13.2",
+ "framer-motion": "^12.23.22",
"http-status-codes": "^2.3.0",
"js-cookie": "^3.0.5",
"lexical": "^0.34.0",
@@ -1004,6 +1007,12 @@
"node": ">= 10"
}
},
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
@@ -1033,6 +1042,33 @@
}
}
},
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
+ "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -1451,6 +1487,37 @@
}
}
},
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
+ "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-separator": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
@@ -1596,6 +1663,24 @@
}
}
},
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
@@ -2583,6 +2668,33 @@
"node": ">= 6"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.23.22",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz",
+ "integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.23.21",
+ "motion-utils": "^12.23.6",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4059,6 +4171,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.23.21",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.21.tgz",
+ "integrity": "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.23.6"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
+ "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -4968,6 +5095,15 @@
}
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
diff --git a/package.json b/package.json
index dfa5ce7..b136150 100644
--- a/package.json
+++ b/package.json
@@ -13,9 +13,11 @@
"dependencies": {
"@lexical/react": "^0.34.0",
"@lexical/rich-text": "^0.34.0",
+ "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
@@ -29,6 +31,7 @@
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"emoji-picker-react": "^4.13.2",
+ "framer-motion": "^12.23.22",
"http-status-codes": "^2.3.0",
"js-cookie": "^3.0.5",
"lexical": "^0.34.0",
diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts
index accbb9e..3bab295 100644
--- a/src/app/api/user/route.ts
+++ b/src/app/api/user/route.ts
@@ -84,7 +84,7 @@ export async function GET(req: NextRequest) {
{ status: StatusCodes.BAD_REQUEST }
);
else {
- const id = query_id ? query_id : token ? CLIENT_AUTH_TOKENS[token] : null;
+ const id = query_id ? parseInt(query_id) : token ? CLIENT_AUTH_TOKENS[token] : null;
if (!id)
return NextResponse.json(
diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx
new file mode 100644
index 0000000..9b47b86
--- /dev/null
+++ b/src/app/home/page.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import { Card } from "@/components/ui/card";
+import { useState } from "react";
+import AppSidebar from "@/components/Sidebar";
+import MessageBox from "@/components/MessageBox";
+
+export default function HomePage() {
+ const [messages, setMessages] = useState([
+ { id: 1, user: "Alice", text: "Hey, how are you?" },
+ { id: 2, user: "You", text: "Doing good, just working on the chat app!" },
+ ]);
+
+ const [input, setInput] = useState("");
+
+ function sendMessage() {
+ if (!input.trim()) return;
+ setMessages([...messages, { id: Date.now(), user: "You", text: input }]);
+ setInput("");
+ }
+
+ return (
+
+ {}}
+ sendMessage={() => {}}
+ />
+
+ );
+}
+
+function ServerButton({
+ label,
+ icon,
+}: {
+ label: string;
+ icon: React.ReactNode;
+}) {
+ return (
+
+ );
+}
+
+function DMItem({ name, status }: { name: string; status: string }) {
+ return (
+
+
+ {name[0]}
+
+
+ {name}
+ {status}
+
+
+ );
+}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index 546cc95..cb098e0 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -5,16 +5,39 @@ import { Input } from "@/components/ui/input";
import axios from "axios";
import { useState } from "react";
import Cookies from "js-cookie";
-import { redirect } from "next/navigation";
+import { useRouter } from "next/navigation";
+import { Label } from "@/components/ui/label";
+import Link from "next/link";
export default function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
+ const [feedback, setFeedback] = useState<
+ undefined | { kind: "error" | "info"; message: string }
+ >();
+
+ const router = useRouter();
const login = async () => {
- const res = await axios.put("/api/user", { username, password });
- Cookies.set("token", (res.data as any).token);
- redirect("/");
+ setFeedback(undefined);
+ try {
+ const res = await axios.put("/api/user", { username, password });
+
+ Cookies.set("token", (res.data as any).token);
+ setFeedback({
+ kind: "info",
+ message: "Login successful! Redirecting...",
+ });
+
+ // small delay to show feedback before redirect
+ setTimeout(() => router.push("/home"), 800);
+ } catch (err: any) {
+ setFeedback({
+ kind: "error",
+ message:
+ err.response?.data?.message || "Login failed. Please try again.",
+ });
+ }
};
return (
@@ -22,8 +45,8 @@ export default function Login() {
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 2aa95be..5e50005 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,22 +1,120 @@
"use client";
-import AppSidebar from "@/components/Sidebar";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { MessageCircle, Users, Shield } from "lucide-react";
+import { motion } from "framer-motion";
+import { useRouter } from "next/navigation";
+
+export default function LandingPage() {
+ const router = useRouter();
-export default function Home() {
return (
-
- ok
-
+
+ {/* Header */}
+
+
+ {/* Hero Section */}
+
+
+ Chat. Connect. Collaborate.
+
+
+ A modern chat app for teams and friends. Stay connected with real-time
+ messaging, secure channels, and seamless collaboration.
+
+
+
+
+
+
+
+ {/* Features */}
+
+ }
+ title="Real-Time Chat"
+ description="Experience instant messaging with typing indicators, read receipts, and more with our self-hostable node system."
+ />
+ }
+ title="Group Channels"
+ description="Organize conversations into channels for projects, teams, or friends by hosting your own server."
+ />
+ }
+ title="Secure & Private"
+ description="Your data is encrypted and protected, ensuring safe communication."
+ />
+
+
+ {/* Call to Action */}
+ {/*
*/}
+
+ {/* Footer */}
+
+
+ );
+}
+
+function FeatureCard({
+ icon,
+ title,
+ description,
+}: {
+ icon: React.ReactNode;
+ title: string;
+ description: string;
+}) {
+ return (
+
+
+ {icon}
+ {title}
+ {description}
+
+
);
}
diff --git a/src/app/server/[id]/page.tsx b/src/app/server/[id]/page.tsx
index 0837937..e370432 100644
--- a/src/app/server/[id]/page.tsx
+++ b/src/app/server/[id]/page.tsx
@@ -8,7 +8,7 @@ import Message from "@/types/message";
import useUser, { User } from "@/hooks/get-user";
import Cookies from "js-cookie";
import axios from "axios";
-import { StringMap } from "@/types/typeUtils";
+import { NumberMap, StringMap } from "@/types/typeUtils";
import ServerType from "@/types/server";
export default function Server() {
@@ -17,7 +17,7 @@ export default function Server() {
const wsRef = useRef(null);
const [messages, setMessages] = useState([]);
const { user, loading } = useUser();
- const [userList, setUserList] = useState>({});
+ const [userList, setUserList] = useState>({});
useEffect(() => {
if (!ip || !user || loading) return;
@@ -83,9 +83,18 @@ export default function Server() {
}, [ip, user, loading]);
return (
-
+
m.channel_id === "general")
.toReversed()}
diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx
new file mode 100644
index 0000000..71ff4c8
--- /dev/null
+++ b/src/app/signup/page.tsx
@@ -0,0 +1,150 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import axios from "axios";
+import { useState } from "react";
+import Cookies from "js-cookie";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+
+export default function SignUp() {
+ const [username, setUsername] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [acceptTerms, setAcceptTerms] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [feedback, setFeedback] = useState<
+ undefined | { kind: "error" | "info"; message: string }
+ >();
+
+ const router = useRouter();
+
+ const validateInput = ():
+ | { kind: "error" | "info"; message: string }
+ | undefined => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
+
+ if (!emailRegex.test(email)) {
+ return { kind: "error", message: "Invalid email format." };
+ }
+ if (!usernameRegex.test(username)) {
+ return {
+ kind: "error",
+ message:
+ "Username must be 3–20 characters and only letters, numbers, or underscores.",
+ };
+ }
+ if (password.length < 6) {
+ return {
+ kind: "error",
+ message: "Password must be at least 6 characters long.",
+ };
+ }
+ if (password !== confirmPassword) {
+ return { kind: "error", message: "Passwords do not match." };
+ }
+ if (!acceptTerms) {
+ return {
+ kind: "error",
+ message: "You must accept the terms to sign up.",
+ };
+ }
+ return;
+ };
+
+ const signup = async () => {
+ setFeedback(undefined);
+ const validationError = validateInput();
+ if (validationError) {
+ setFeedback(validationError);
+ return;
+ }
+
+ try {
+ setLoading(true);
+ const res = await axios.post("/api/user", { username, password, email });
+ Cookies.set("token", (res.data as any).token);
+
+ setFeedback({ kind: "info", message: "Account created! Redirecting..." });
+ setTimeout(() => router.push("/home"), 1000);
+ } catch (err: any) {
+ setFeedback({
+ kind: "error",
+ message:
+ err.response?.data?.message ||
+ "Signup failed. Please try again later.",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/MessageBox.tsx b/src/components/MessageBox.tsx
index 05e1b42..d89a05f 100644
--- a/src/components/MessageBox.tsx
+++ b/src/components/MessageBox.tsx
@@ -11,20 +11,32 @@ import {
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { SmilePlusIcon } from "lucide-react";
-import { useRef, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { format, isToday, isYesterday } from "date-fns";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
-import { StringMap } from "@/types/typeUtils";
+import { NumberMap, StringMap } from "@/types/typeUtils";
import { User } from "@/hooks/get-user";
+import axios from "axios";
function MessageContainer({
message,
userList,
+ setUserList,
}: {
message: Message;
- userList: StringMap;
+ userList: NumberMap;
+ setUserList: React.Dispatch>>;
}) {
+ useEffect(() => {
+ axios
+ .get("/api/user/", { params: { id: message.from } })
+ .then((res) =>
+ setUserList((prev) => ({ ...prev, [message.from]: res.data as User }))
+ )
+ .catch(console.error);
+ }, [message.from, setUserList]);
+
return (
@@ -117,19 +129,33 @@ function MessageContainer({
export default function MessageBox({
messages,
userList,
+ setUserList,
sendMessage,
+ channelName,
}: {
messages: Message[];
- userList: StringMap
;
+ userList: NumberMap;
+ setUserList: React.Dispatch>>;
sendMessage: (m: string) => void;
+ channelName?: string;
}) {
const [text, setText] = useState("");
return (
-
+
+ {channelName && (
+
+ )}
{messages.map((msg) => (
-
+
))}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 4c4a870..f1bdcde 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -1,21 +1,12 @@
"use client";
import {
- Sidebar,
- SidebarContent,
- SidebarHeader,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- SidebarProvider,
-} from "./ui/sidebar";
-import { ChevronDown, HashIcon, MicIcon } from "lucide-react";
-import {
- DropdownMenuItem,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuTrigger,
-} from "./ui/dropdown-menu";
+ HashIcon,
+ MessageCircle,
+ Plus,
+ Search,
+ Volume2Icon,
+} from "lucide-react";
import Server from "@/types/server";
import { useEffect, useState } from "react";
import Cookies from "js-cookie";
@@ -32,7 +23,11 @@ import {
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Button } from "./ui/button";
-import Link from "next/link";
+import { Card } from "./ui/card";
+import { Avatar, AvatarFallback } from "./ui/avatar";
+import { ScrollArea } from "./ui/scroll-area";
+import { useRouter } from "next/navigation";
+import Channel from "@/types/channel";
export default function AppSidebar({
server,
@@ -43,6 +38,7 @@ export default function AppSidebar({
}>) {
const [servers, setServers] = useState<[string, string][]>([]);
const [newServer, setNewServer] = useState({ name: "", ip: "" });
+ const router = useRouter();
useEffect(() => {
const s = Cookies.get("servers")?.split(",");
@@ -63,97 +59,162 @@ export default function AppSidebar({
setNewServer({ name: "", ip: "" });
};
- return server ? (
-