This is a Discord-like chat interface with a very clean, minimalist dark design. I'll need to recreate this using Next.js and shadcn/ui components with a black and white theme.
For this project, I'll need to create:
I'll use a Code Project to implement this as it requires multiple React components and real-time functionality.
</Thinking>
I'll create a real-time chat application that matches the design in your image, using Next.js and shadcn/ui with a black and white theme.
<tsx file="app/globals.css">
@tailwind base;
@tailwind components;
@tailwind utilities;@layer base { :root { --background: 0 0% 100%; --foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;}
.dark { --background: 0 0% 8%; --foreground: 0 0% 98%;
--card: 0 0% 12%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 12%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 18%;
--input: 0 0% 18%;
--ring: 0 0% 83.9%;} }
@layer base {
{ @apply border-border; } body { @apply bg-background text-foreground; } }
</tsx> <tsx file="app/layout.tsx">
import type { Metadata } from "next";
import { Inter } from 'next/font/google';
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = { title: "hamm.er | Real-time Chat App", description: "A real-time chat application built with Next.js and shadcn/ui", };
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={${inter.className} dark}>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
forcedTheme="dark"
>
{children}
</ThemeProvider>
</body>
</html>
);
}
</tsx>
<tsx file="app/page.tsx">
import ChatLayout from "@/components/chat-layout";export default function Home() {
return <ChatLayout />;
}
</tsx>
<tsx file="components/chat-layout.tsx">
"use client";import { useState } from "react"; import Sidebar from "./sidebar"; import ChatArea from "./chat-area"; import { Channel } from "@/lib/types";
export default function ChatLayout() {
const [activeChannel, setActiveChannel] = useState<Channel>({
id: "general",
name: "general",
type: "text"
});
const channels: Channel[] = [ { id: "general", name: "general", type: "text" }, { id: "call-room", name: "Call Room", type: "voice" }, ];
return (
<div className="flex h-screen bg-black text-white">
<Sidebar
channels={channels}
activeChannel={activeChannel}
setActiveChannel={setActiveChannel}
/>
<ChatArea channel={activeChannel} />
</div>
);
}
</tsx>
<tsx file="components/sidebar.tsx">
import { Channel } from "@/lib/types";
import { Hash, Volume2, ChevronRight, Users, Calendar } from 'lucide-react';
import { Input } from "@/components/ui/input";interface SidebarProps { channels: Channel[]; activeChannel: Channel; setActiveChannel: (channel: Channel) => void; }
export default function Sidebar({ channels, activeChannel, setActiveChannel }: SidebarProps) {
return (
<div className="w-64 bg-[#111111] flex flex-col h-full border-r border-[#222222]">
<div className="p-4 flex items-center justify-between border-b border-[#222222]">
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold">hamm.er</h1>
<ChevronRight className="h-5 w-5 text-gray-400" />
</div>
</div>
<div className="p-2">
<div className="relative">
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<Input
type="search"
placeholder="Search"
className="pl-10 bg-[#1E1E1E] border-none rounded-full text-gray-300 focus-visible:ring-0 focus-visible:ring-offset-0"
/>
</div>
</div>
<div className="p-2 flex justify-end gap-2">
<button className="p-2 bg-[#1E1E1E] rounded-full">
<Users className="h-5 w-5 text-gray-300" />
</button>
<button className="p-2 bg-[#1E1E1E] rounded-full">
<Calendar className="h-5 w-5 text-gray-300" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1">
{channels.map((channel) => (
<button
key={channel.id}
onClick={() => setActiveChannel(channel)}
className={`w-full text-left p-2 rounded flex items-center gap-2 ${
activeChannel.id === channel.id ? "bg-[#1E1E1E]" : "hover:bg-[#1A1A1A]"
}`}
>
{channel.type === "text" ? (
<Hash className="h-5 w-5 text-gray-400" />
) : (
<Volume2 className="h-5 w-5 text-gray-400" />
)}
<span className="text-gray-300">{channel.name}</span>
</button>
))}
</div>
</div>);
}
</tsx>
<tsx file="components/chat-area.tsx">
import { useState, useEffect, useRef } from "react";
import { Channel, Message } from "@/lib/types";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Send } from 'lucide-react';
import MessageItem from "./message-item";
import { useMessages } from "@/hooks/use-messages";interface ChatAreaProps { channel: Channel; }
export default function ChatArea({ channel }: ChatAreaProps) {
const [inputValue, setInputValue] = useState("");
const { messages, sendMessage } = useMessages(channel.id);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]);
const handleSendMessage = (e: React.FormEvent) => { e.preventDefault(); if (inputValue.trim()) { sendMessage(inputValue); setInputValue(""); } };
return (
<div className="flex-1 flex flex-col h-full bg-[#121212]">
<div className="p-4 border-b border-[#222222] flex items-center">
<h2 className="text-xl font-semibold">#{channel.name}</h2>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-400">
<p>No messages in #{channel.name} yet</p>
<p className="text-sm">Be the first to send a message!</p>
</div>
) : (
messages.map((message) => (
<MessageItem key={message.id} message={message} />
))
)}
<div ref={messagesEndRef} />
</div>
<div className="p-4 border-t border-[#222222]">
<form onSubmit={handleSendMessage} className="flex gap-2">
<Input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={`Message #${channel.name}`}
className="bg-[#1E1E1E] border-none focus-visible:ring-0 focus-visible:ring-offset-0"
/>
<Button type="submit" size="icon" variant="ghost">
<Send className="h-5 w-5" />
</Button>
</form>
</div>
</div>);
}
</tsx>
<tsx file="components/message-item.tsx">
import { Message } from "@/lib/types";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { formatDistanceToNow } from "date-fns";interface MessageItemProps { message: Message; }
export default function MessageItem({ message }: MessageItemProps) {
return (
<div className="flex gap-3">
<Avatar>
<AvatarImage src={message.user.avatar} />
<AvatarFallback>{message.user.name.charAt(0)}</AvatarFallback>
</Avatar>
<div>
<div className="flex items-center gap-2">
<span className="font-semibold">{message.user.name}</span>
<span className="text-xs text-gray-400">
{formatDistanceToNow(new Date(message.timestamp), { addSuffix: true })}
</span>
</div>
<p className="text-gray-200">{message.content}</p>
</div>
</div>
);
}
</tsx>
<tsx file="lib/types.ts">
export interface User {
id: string;
name: string;
avatar: string;
}export interface Message { id: string; content: string; timestamp: number; user: User; channelId: string; }
export interface Channel {
id: string;
name: string;
type: "text" | "voice";
}
</tsx>
<tsx file="hooks/use-messages.ts">
"use client";import { useState, useEffect } from "react"; import { Message } from "@/lib/types"; import { v4 as uuidv4 } from "uuid";
// Mock user data const currentUser = { id: "user1", name: "Current User", avatar: "/placeholder.svg?height=40&width=40", };
// Initial messages for demo purposes const initialMessages: Record<string, Message[]> = { general: [ { id: "msg1", content: "Welcome to the general channel!", timestamp: Date.now() - 3600000, user: { id: "system", name: "System", avatar: "/placeholder.svg?height=40&width=40", }, channelId: "general", }, ], "call-room": [], };
export function useMessages(channelId: string) { const [messages, setMessages] = useState<Message[]>(initialMessages[channelId] || []);
// Simulate real-time updates by listening to channel changes useEffect(() => { setMessages(initialMessages[channelId] || []);
// Simulate receiving a message after 2 seconds if the channel is empty
if ((initialMessages[channelId] || []).length === 0) {
const timer = setTimeout(() => {
const newMessage: Message = {
id: uuidv4(),
content: `Welcome to the ${channelId} channel!`,
timestamp: Date.now(),
user: {
id: "system",
name: "System",
avatar: "/placeholder.svg?height=40&width=40",
},
channelId,
};
setMessages([newMessage]);
initialMessages[channelId] = [newMessage];
}, 2000);
return () => clearTimeout(timer);
}}, [channelId]);
const sendMessage = (content: string) => { const newMessage: Message = { id: uuidv4(), content, timestamp: Date.now(), user: currentUser, channelId, };
// Update local state
setMessages((prev) => [...prev, newMessage]);
// Update our mock database
initialMessages[channelId] = [...(initialMessages[channelId] || []), newMessage];
// In a real app, you would send this to your backend/API
// socket.emit('send_message', newMessage);};
return { messages, sendMessage };
}
</tsx>
<tsx file="tailwind.config.ts">
import type { Config } from "tailwindcss";const config = { darkMode: ["class"], content: [ './pages//*.{ts,tsx}', './components//.{ts,tsx}', './app/**/.{ts,tsx}', './src/**/*.{ts,tsx}', ], prefix: "", theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, keyframes: { "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, }, }, plugins: [require("tailwindcss-animate")], } satisfies Config
export default config
</tsx>
useMessages hook simulates real-time functionality (in a production app, you would integrate with a real-time service like Socket.io or Pusher)For a production-ready app, you might want to:
You can install this project by clicking the "Add to codebase" button in the Block view.
I'll update the chat app to match the new channel view design when a user clicks on the "general" option.
I've updated the chat interface to match the design in the new image:
Header Design:
Welcome Message:
Message Display:
Message Input:
Now when a user clicks on the "general" channel, they'll see the exact interface shown in your image, with the welcome message, server setup button, and the "Hi" message from Sreeman.