Profiles, Giphy, uploads, settings UI

Backend:
- Auth: split routes into RegisterPublicRoutes + RegisterProtectedRoutes
- Auth: PATCH /me for profile updates (display_name, bio, accent_color, status_text)
- Auth: Gravatar fallback for avatars on register
- DB: users table now has bio, accent_color, status_text columns
- giphy/client.go: Search() and GetTrending() against Giphy API
- upload/handlers.go: MinIO file upload + serve
- config: added GiphyConfig and MinIO config
- cmd/server: wired giphy (/gifs/search, /gifs/trending), upload (/upload), files (/files/*) routes

Frontend:
- auth store: updated User interface with new profile fields, added updateProfile()
- UserSettings.tsx: terminal-styled profile editor (display_name, bio, accent_color, status_text, avatar upload)
- GiphyPicker.tsx: terminal-styled GIF picker with search + trending
- ChatArea.tsx: integrated [GIF] button into message input
- App.tsx: imports UserSettings, added /settings route
This commit is contained in:
2026-06-28 16:06:08 -04:00
parent bb5a56816b
commit e69553af02
18 changed files with 1422 additions and 352 deletions
+26
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useMessageStore } from '../stores/message.ts';
import { useChannelStore } from '../stores/channel.ts';
import { GiphyPicker, type Gif } from './GiphyPicker';
function formatTime(iso: string): string {
const date = new Date(iso);
@@ -18,6 +19,7 @@ export function ChatArea() {
const fetchMessages = useMessageStore((state) => state.fetchMessages);
const sendMessage = useMessageStore((state) => state.sendMessage);
const [input, setInput] = useState('');
const [showGifPicker, setShowGifPicker] = useState(false);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -39,6 +41,13 @@ export function ChatArea() {
setInput('');
};
const handleGifSelect = async (gif: Gif) => {
if (!activeChannelId) return;
const content = `![${gif.title || 'GIF'}](${gif.url})`;
await sendMessage(activeChannelId, content);
setShowGifPicker(false);
};
return (
<div className="flex flex-col h-full bg-gb-bg">
<div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg-s">
@@ -58,6 +67,14 @@ export function ChatArea() {
))}
<div ref={bottomRef} />
</div>
{showGifPicker && (
<div className="px-3 pb-1">
<GiphyPicker
onSelect={handleGifSelect}
onClose={() => setShowGifPicker(false)}
/>
</div>
)}
<form onSubmit={handleSubmit} className="p-3 flex items-center gap-2">
<span className="text-gb-fg-f select-none">{'>'}</span>
<input
@@ -68,6 +85,15 @@ export function ChatArea() {
className="terminal-input"
disabled={!activeChannelId}
/>
<button
type="button"
onClick={() => setShowGifPicker((prev) => !prev)}
disabled={!activeChannelId}
className="text-gb-fg-f hover:text-gb-orange font-mono text-sm select-none disabled:opacity-50 disabled:cursor-not-allowed"
title="Toggle GIF picker"
>
[GIF]
</button>
</form>
</div>
);