Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ build/
out/
*.local
.agent/
apps/backend/uploads

# Prisma generated client
apps/backend/generated/*
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ export class AuthController {
email: true,
emailVerified: true,
avatar: true,
bio: true,
createdAt: true,
},
})
Expand Down
95 changes: 92 additions & 3 deletions apps/backend/src/routes/user.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,17 @@ router.get("/search", authMiddleware, async (req, res) => {
})


router.get("/:username", async (req, res) => {
router.get("/:username", authMiddleware, async (req, res) => {
const parsed = usernameParamsSchema.safeParse(req.params)
if (!parsed.success) {
return respondWithZodError(res, parsed.error)
}

const currentUserId = req.user?.id
if (!currentUserId) {
return res.status(401).json({ message: "Not authenticated" })
}

const { username } = parsed.data;

const user = await prisma.user.findUnique({
Expand All @@ -56,13 +61,97 @@ router.get("/:username", async (req, res) => {
username: true,
name: true,
avatar: true,
publicNumericId: true
bio: true,
publicNumericId: true,
isOnline: true,
lastLogin: true,
createdAt: true,
}
});

if (!user) return res.status(404).json({ message: "User not found" });

res.json({ user });
const targetUserId = user.id

const [friendship, pendingRequest, currentFriends, targetFriends, currentZones, targetZones] =
await Promise.all([
prisma.friend.findFirst({
where: {
OR: [
{ user1Id: currentUserId, user2Id: targetUserId },
{ user1Id: targetUserId, user2Id: currentUserId },
],
},
select: { id: true },
}),
prisma.friendRequest.findFirst({
where: {
status: "PENDING",
OR: [
{ senderId: currentUserId, receiverId: targetUserId },
{ senderId: targetUserId, receiverId: currentUserId },
],
},
select: { id: true },
}),
prisma.friend.findMany({
where: { OR: [{ user1Id: currentUserId }, { user2Id: currentUserId }] },
select: { user1Id: true, user2Id: true },
}),
prisma.friend.findMany({
where: { OR: [{ user1Id: targetUserId }, { user2Id: targetUserId }] },
select: { user1Id: true, user2Id: true },
}),
prisma.chatParticipant.findMany({
where: { userId: currentUserId, chat: { type: "ZONE" } },
select: { chat: { select: { id: true, publicId: true, name: true } } },
}),
prisma.chatParticipant.findMany({
where: { userId: targetUserId, chat: { type: "ZONE" } },
select: { chat: { select: { id: true, publicId: true, name: true } } },
}),
])

const friendStatus = friendship ? "accepted" : pendingRequest ? "pending" : "none"

const currentFriendIds = new Set(
currentFriends.map((f) => (f.user1Id === currentUserId ? f.user2Id : f.user1Id)),
)
const targetFriendIds = new Set(
targetFriends.map((f) => (f.user1Id === targetUserId ? f.user2Id : f.user1Id)),
)
const mutualFriendIds = [...currentFriendIds].filter((id) => targetFriendIds.has(id))

const mutualFriends = mutualFriendIds.length
? await prisma.user.findMany({
where: { id: { in: mutualFriendIds } },
select: { id: true, name: true, username: true },
})
: []

const currentZoneMap = new Map(currentZones.map((entry) => [entry.chat.id, entry.chat]))
const mutualZones = targetZones
.map((entry) => currentZoneMap.get(entry.chat.id))
.filter((zone): zone is { id: number; publicId: string; name: string } => Boolean(zone))
.map((zone) => ({ id: zone.publicId, name: zone.name }))

res.json({
user: {
id: String(user.id),
name: user.name ?? user.username,
avatar: user.avatar,
bio: user.bio,
friendStatus,
mutualFriends: mutualFriends.map((friend) => ({
id: String(friend.id),
name: friend.name ?? friend.username,
})),
mutualZones,
isOnline: user.isOnline,
lastLogin: user.lastLogin,
createdAt: user.createdAt,
},
});
});

router.patch(
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"lint": "eslint ."
},
"dependencies": {
"@emoji-mart/data": "^1.1.0",
"@emoji-mart/react": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/app/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Eye, EyeOff, Mail, Lock, User, CheckCircle2, AlertCircle, Loader2, Spar
import { useRouter } from "next/navigation";
import { api } from '@openchat/lib';
import { Checkbox, Label } from "@openchat/ui"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger, DialogFooter } from "@openchat/ui"
import { signupSchema } from "@openchat/lib/validations/auth";
import { useGoogleLogin } from "@react-oauth/google";
import Link from 'next/link';
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default async function DashboardLayout({
}

return (
<div className="min-h-screen bg-[#0b1220] text-foreground">
<div className="min-h-screen bg-sidebar text-foreground">
{children}
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default async function RootLayout({

return (
<html lang="en" suppressHydrationWarning>
<body className="min-h-screen bg-[#0b1220]">
<body className="min-h-screen bg-main">
< ClientProviders initialUser={user} >
{children}
</ClientProviders >
Expand Down
53 changes: 39 additions & 14 deletions apps/frontend/src/app/settings/_components/SettingsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import { useState } from 'react'
import { api, cn } from '@openchat/lib'
import { User, Shield, Lock, Bell, Trash2, LogOut, ArrowLeft, Keyboard } from 'lucide-react'
import { User, Shield, Lock, Bell, Trash2, LogOut, ArrowLeft, Keyboard, Menu, X } from 'lucide-react'
import { useUserStore } from '@/app/stores/user-store'
import { useChatsStore } from '@/app/stores/chat-store'
import { useFriendsStore } from '@/app/stores/friends-store'
import { Button } from 'packages/ui'
import {
Sheet,
SheetContent,
SheetTrigger,
} from 'packages/ui'

const tabs = [
{ name: 'Profile', href: '/settings/profile', icon: User },
Expand All @@ -18,7 +24,7 @@ const tabs = [
{ name: 'Account', href: '/settings/account', icon: Trash2 },
]

export default function SettingsSidebar() {
function SettingsNav({ onLinkClick }: { onLinkClick?: () => void }) {
const pathname = usePathname()
const router = useRouter()

Expand All @@ -36,23 +42,21 @@ export default function SettingsSidebar() {
useChatsStore.getState().reset()
useFriendsStore.getState().reset()

// Use window.location.href to ensure a clean state and clear server-side caches
window.location.href = '/auth'
}

return (
<div className="fixed min-h-[100vh] w-64 bg-[#0a101c] border-r border-white/5 p-6 flex flex-col">
<div className="flex flex-col h-full p-6">
<div className="mb-6">
<Button
variant="ghost"
onClick={() => router.push('/zone')}
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-white transition"
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition"
>
<ArrowLeft size={16} />
Back
</Button>
</div>
{/* Tabs */}
<div className="space-y-2 flex-1">
{tabs.map((tab) => {
const Icon = tab.icon
Expand All @@ -62,11 +66,12 @@ export default function SettingsSidebar() {
<Link
key={tab.name}
href={tab.href}
onClick={onLinkClick}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-all duration-200',
active
? 'bg-primary/15 text-primary border-l-2 border-primary'
: 'text-muted-foreground hover:bg-white/5 hover:text-white'
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'
)}
>
<Icon size={16} />
Expand All @@ -75,20 +80,40 @@ export default function SettingsSidebar() {
)
})}
</div>

{/* Divider */}
<div className="h-px bg-white/5 my-4" />

{/* Logout */}
<div className="h-px bg-border my-4" />
<Button
variant="destructive"
onClick={handleLogout}
className="flex justify-start gap-3 px-3 py-2 rounded-lg text-sm text-red-400 transition-all duration-200 hover:bg-red-500/10 hover:text-red-500"
className="flex justify-start gap-3 px-3 py-2 rounded-lg text-sm text-red-500 transition-all duration-200 hover:bg-red-500/10"
>
<LogOut size={16} />
Logout
</Button>

</div>
)
}

export default function SettingsSidebar() {
const [open, setOpen] = useState(false)

return (
<>
<div className="hidden md:flex fixed min-h-[100vh] w-64 bg-sidebar border-r">
<SettingsNav />
</div>

<div className="md:hidden fixed top-4 left-4 z-50">
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-lg">
<Menu size={20} />
</Button>
</SheetTrigger>
<SheetContent side="left" className="p-0 w-[280px] bg-sidebar border-r">
<SettingsNav onLinkClick={() => setOpen(false)} />
</SheetContent>
</Sheet>
</div>
</>
)
}
4 changes: 2 additions & 2 deletions apps/frontend/src/app/settings/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function AccountPage() {
</div>

{/* Sessions */}
<Card className="bg-[#111a2b] border border-white/5">
<Card className="bg-surface border-border">
<CardHeader>
<CardTitle>Sessions</CardTitle>
<CardDescription>
Expand Down Expand Up @@ -75,7 +75,7 @@ export default function AccountPage() {
</Card>

{/* Data */}
<Card className="bg-[#111a2b] border border-white/5">
<Card className="bg-surface border-border">
<CardHeader>
<CardTitle>Your Data</CardTitle>
<CardDescription>
Expand Down
16 changes: 8 additions & 8 deletions apps/frontend/src/app/settings/keyboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function ShortcutRecorder({ action, value, onChange, otherShortcut }: ShortcutRe

return (
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-medium text-zinc-200">{label}</span>
<span className="text-sm font-medium">{label}</span>
<div className="flex items-center gap-2">
{recording ? (
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-purple-600/20 border border-purple-500/50 rounded-md min-w-[120px]">
Expand All @@ -113,11 +113,11 @@ function ShortcutRecorder({ action, value, onChange, otherShortcut }: ShortcutRe
</div>
) : value ? (
<div className="flex items-center gap-1 px-3 py-1.5 bg-white/5 border border-white/10 rounded-md min-w-[120px]">
<Keyboard className="w-3.5 h-3.5 text-zinc-400" />
<span className="text-sm text-zinc-200 font-mono">{value}</span>
<Keyboard className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-sm font-mono">{value}</span>
<button
onClick={clearShortcut}
className="ml-1 p-0.5 hover:bg-white/10 rounded text-zinc-500 hover:text-zinc-300 transition-colors"
className="ml-1 p-0.5 hover:bg-muted rounded text-muted-foreground hover:text-foreground transition-colors"
title="Clear shortcut"
>
<X className="w-3 h-3" />
Expand All @@ -126,7 +126,7 @@ function ShortcutRecorder({ action, value, onChange, otherShortcut }: ShortcutRe
) : (
<button
onClick={startRecording}
className="px-3 py-1.5 text-sm text-zinc-400 hover:text-white bg-white/5 hover:bg-white/10 border border-white/10 rounded-md transition-colors min-w-[120px]"
className="px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground bg-muted hover:bg-muted/50 border-border rounded-md transition-colors min-w-[120px]"
>
Not set
</button>
Expand Down Expand Up @@ -170,7 +170,7 @@ export default function KeyboardShortcutsPage() {
</p>
</div>

<Card className="bg-[#111a2b] border border-white/5">
<Card className="bg-surface border-border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Keyboard className="w-5 h-5" />
Expand All @@ -197,11 +197,11 @@ export default function KeyboardShortcutsPage() {
</CardContent>
</Card>

<Card className="bg-[#111a2b] border border-white/5">
<Card className="bg-surface border-border">
<CardHeader>
<CardTitle className="text-base">Tips</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-zinc-400">
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>• Shortcuts work only when the app window is focused</p>
<p>• Shortcuts are disabled when typing in input fields</p>
<p>• Press Escape to cancel recording</p>
Expand Down
6 changes: 3 additions & 3 deletions apps/frontend/src/app/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ export default async function SettingsLayout({

return (
<SettingsClientWrapper>
<div className="flex min-h-screen bg-[#0b1220]">
<div className="flex min-h-screen bg-main">
<SettingsSidebar />

<div className="flex-1 flex justify-center">
<div className="w-full max-w-4xl p-12">
<div className="flex-1 flex justify-center md:pl-64">
<div className="w-full max-w-4xl p-4 md:p-12">
{children}
</div>
</div>
Expand Down
Loading
Loading