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
6 changes: 6 additions & 0 deletions apps/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ FRONTEND_URL="http://localhost:5173"
# Server
PORT="3000"
NODE_ENV="development"

# Cloudflare
R2_ACCOUNT_ID="your_account_id_here"
R2_ACCESS_KEY_ID="your_access_key_id_here"
R2_SECRET_ACCESS_KEY="your_secret_access_key_here"
R2_BUCKET_NAME="iiitbuzz-uploads"
71 changes: 70 additions & 1 deletion apps/server/src/routers/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { eq } from "drizzle-orm";
import { desc, eq, sql } from "drizzle-orm";
import type { FastifyInstance } from "fastify";
import { users } from "@/db/schema/user.schema";
import { threads } from "@/db/schema/thread.schema";
import { posts } from "@/db/schema/post.schema";
import { votes } from "@/db/schema/vote.schema";
import { topics } from "@/db/schema/topic.schema";
import { DrizzleClient } from "../db/index";
import {
type User,
Expand Down Expand Up @@ -238,4 +242,69 @@ export async function userRoutes(fastify: FastifyInstance) {
}
},
);

fastify.get(
"/:userId/activity",
{ preHandler: optionalAuth },
async (request, reply) => {
try {
const { userId } = request.params as { userId: string };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The userId parameter is extracted from request.params without validation. To maintain consistency with other routes in this file (e.g., line 22) and ensure data integrity, consider using a validation schema (e.g., Zod) to parse and validate the userId format before using it in database queries.


const userThreads = await DrizzleClient.select({
id: threads.id,
type: sql<string>`'thread'`,
title: threads.threadTitle,
createdAt: threads.createdAt,
})
.from(threads)
.where(eq(threads.createdBy, userId))
.orderBy(desc(threads.createdAt))
.limit(5);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The limit for individual activity types (threads, posts, likes) is set to 5. When these are merged and sliced to 15 (line 299), the resulting feed might be incomplete if a user's activity is heavily weighted toward one type. For example, a user with many posts but no threads would only see 5 items. Consider increasing the limit for each query (lines 262, 276, and 290) to 15 to ensure the feed is always populated with the most recent items across all categories.

Suggested change
.limit(5);
.limit(15);


const userPosts = await DrizzleClient.select({
id: posts.id,
type: sql<string>`'post'`,
title: threads.threadTitle,
content: posts.content,
threadId: posts.threadId,
createdAt: posts.createdAt,
})
.from(posts)
.innerJoin(threads, eq(posts.threadId, threads.id))
.where(eq(posts.createdBy, userId))
.orderBy(desc(posts.createdAt))
.limit(5);

const userLikes = await DrizzleClient.select({
id: votes.id,
type: sql<string>`'like'`,
title: threads.threadTitle,
threadId: threads.id,
createdAt: votes.createdAt,
})
.from(votes)
.innerJoin(posts, eq(votes.postId, posts.id))
.innerJoin(threads, eq(posts.threadId, threads.id))
.where(eq(votes.userId, userId))
.orderBy(desc(votes.createdAt))
.limit(5);

const activity = [...userThreads, ...userPosts, ...userLikes].sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);

return reply.send({
success: true,
activity: activity.slice(0, 15),
});
} catch (err) {
fastify.log.error("Error fetching user activity:", err);
return reply.status(500).send({
error: "Failed to fetch user activity",
success: false,
});
}
},
);
}
9 changes: 4 additions & 5 deletions apps/web/src/components/mode-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export function ModeToggle() {

if (!mounted) {
return (
<div className="border-3 border-border bg-card px-4 py-2 font-bold shadow-[4px_4px_0px_0px_var(--shadow-color)]">
<Droplet className="h-5 w-5" />
<div className="border-2 border-border bg-card h-9 w-9 flex items-center justify-center font-bold shadow-none">
<Droplet className="h-4 w-4" />
</div>
);
}
Expand All @@ -44,10 +44,9 @@ export function ModeToggle() {
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="border-3 flex items-center gap-2 border-border bg-card px-4 py-2 font-bold text-card-foreground shadow-[4px_4px_0px_0px_var(--shadow-color)] transition-all hover:shadow-[2px_2px_0px_0px_var(--shadow-color)] hover:translate-x-[2px] hover:translate-y-[2px]"
className="border-2 flex h-9 w-9 items-center justify-center border-border bg-card font-bold text-card-foreground shadow-none transition-all hover:bg-secondary hover:text-black group"
>
<Icon className="h-5 w-5" style={{ color: currentTheme.color }} />
<span className="hidden sm:inline">{currentTheme.label}</span>
<Icon className="h-4 w-4 transition-transform group-hover:scale-110" style={{ color: currentTheme.color }} />
</button>

{isOpen && (
Expand Down
9 changes: 5 additions & 4 deletions apps/web/src/components/profile-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ export default function ProfileDropdown() {
<DropdownMenuTrigger asChild>
<Button
variant="neutral"
className="border-3 flex items-center gap-2 neo-brutal-button border-primary text-primary bg-secondary hover:bg-secondary hover:text-black"
size="sm"
className="h-9 border-2 border-border flex items-center gap-2 px-3 text-sm font-bold shadow-none hover:bg-secondary hover:text-black transition-all group"
>

<span className="text-sm tracking-tight">
<span className="tracking-tight">
{user.firstName || user.username}
</span>
<ChevronDown size={16} className="text-primary opacity-70" />
<ChevronDown size={14} className="opacity-70 transition-transform group-hover:translate-y-0.5" />
</Button>
</DropdownMenuTrigger>

Expand Down Expand Up @@ -82,7 +83,7 @@ export default function ProfileDropdown() {

<DropdownMenuItem asChild>
<Link
to="/my-threads"
to={user.username ? `/profile/${user.username}` : "/my/profile"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The "My Threads" link points to the same destination as "My Profile" (line 66). This redundancy is also present in the mobile header drawer. If the intention is to show the user's threads, consider linking to the profile page with a query parameter or hash to activate the threads tab (e.g., /profile/${user.username}#threads), or remove the duplicate entry to simplify the menu.

className="flex cursor-pointer items-center gap-2 p-3 font-bold transition-all hover:translate-x-1 hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground"
>
<MessageSquare size={18} />
Expand Down
225 changes: 190 additions & 35 deletions apps/web/src/components/ui/header.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
import { useState } from "react";
import { Link } from "react-router";
import { ModeToggle } from "@/components/mode-toggle";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext";
import { NotificationBell } from "@/components/ui/notification-bell";
import ProfileDropdown from "@/components/profile-dropdown";
import { Home, Plus, Shield } from "lucide-react";
import {
Github,
Home,
LogOut,
Menu,
MessageSquare,
Plus,
Settings,
Shield,
User,
} from "lucide-react";
import { RxDiscordLogo } from "react-icons/rx";
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";

interface HeaderProps {
hideThemeToggle?: boolean;
}

const Header = ({ hideThemeToggle = false }: HeaderProps) => {
const { user, isLoading, isAuthenticated, isAdmin, login } = useAuth();
const { user, isLoading, isAuthenticated, isAdmin, login, logout } = useAuth();
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);

return (
<header className="border-b border-border bg-background/95 backdrop-blur-sm sticky top-0 z-50">
Expand All @@ -24,46 +44,181 @@ const Header = ({ hideThemeToggle = false }: HeaderProps) => {
</Link>

<div className="flex items-center gap-2">
{!hideThemeToggle && <ModeToggle />}
{!hideThemeToggle && (
<div className="nav-desktop">
<ModeToggle />
</div>
)}

{isLoading ? (
<div className="animate-pulse mono-meta">...</div>
) : isAuthenticated ? (
<>
{!hideThemeToggle && (
<Link to="/home">
<Button
type="button"
variant="neutral"
className="flex items-center gap-1 px-2 py-1 text-xs font-bold"
<div className="nav-desktop items-center gap-3">
{!hideThemeToggle && (
<Link to="/home">
<Button
type="button"
variant="neutral"
size="sm"
className="flex h-9 items-center gap-1 px-3 text-xs font-bold border-2 border-border shadow-none hover:bg-secondary hover:text-black transition-all group"
>
<Home className="h-4 w-4 transition-transform group-hover:scale-110" />
<span>Home</span>
</Button>
</Link>
)}
{isAdmin && (
<Link to="/admin">
<Button
type="button"
variant="neutral"
size="sm"
className="flex h-9 items-center gap-1 px-3 text-xs font-bold border-2 border-border shadow-none hover:bg-secondary hover:text-black transition-all group"
>
<Shield className="h-4 w-4 transition-transform group-hover:scale-110" />
<span>Admin</span>
</Button>
</Link>
)}
<NotificationBell />
<ProfileDropdown />
{!hideThemeToggle && (
<Link
to="/new-thread"
className="bg-primary h-9 px-4 flex items-center gap-2 font-bold text-primary-foreground text-xs border-2 border-border hover:bg-primary/90 transition-all hover:-translate-y-0.5 active:translate-y-0 shadow-none group"
>
<Plus className="h-4 w-4 transition-transform group-hover:rotate-90" />
<span>New</span>
</Link>
)}
</div>

<div className="nav-mobile items-center gap-2">
<NotificationBell />
{!hideThemeToggle && (
<Link
to="/new-thread"
className="bg-primary p-2 flex items-center justify-center font-bold text-primary-foreground border-2 border-border"
>
<Home className="h-3 w-3" />
</Button>
</Link>
)}
{isAdmin && (
<Link to="/admin">
<Button
type="button"
variant="neutral"
className="flex items-center gap-1 px-2 py-1 text-xs font-bold"
<Plus className="h-4 w-4" />
</Link>
)}

{!isLoading && (
<Drawer
open={isMobileNavOpen}
onOpenChange={setIsMobileNavOpen}
direction="right"
>
<Shield className="h-3 w-3" />
<span className="hidden sm:inline">Admin</span>
</Button>
</Link>
)}
<NotificationBell />
<ProfileDropdown />
{!hideThemeToggle && (
<Link
to="/new-thread"
className="bg-primary px-2 py-1 flex items-center gap-1 font-bold text-primary-foreground text-xs border-[1.5px] border-border"
>
<Plus className="h-3 w-3" />
<span className="hidden sm:inline">New</span>
</Link>
)}
<DrawerTrigger asChild>
<Button variant="neutral" className="p-2 border-2">
<Menu className="h-5 w-5" />
</Button>
</DrawerTrigger>
<DrawerContent className="p-4">
<DrawerHeader className="px-0 pt-0 text-left">
<DrawerTitle className="text-xl font-black">Menu</DrawerTitle>
</DrawerHeader>

<div className="flex flex-col gap-4 mt-4">
{user && (
<div className="p-3 border-2 border-border bg-secondary/50 rounded-lg">
<p className="font-black text-sm">{user.firstName || user.username}</p>
<p className="text-xs text-muted-foreground">@{user.username}</p>
</div>
)}

<nav className="flex flex-col gap-2">
<Link
to="/home"
className="flex items-center gap-3 p-3 font-bold border-2 border-transparent hover:border-border hover:bg-secondary rounded-lg transition-all"
onClick={() => setIsMobileNavOpen(false)}
>
<Home className="h-5 w-5" />
<span>Home</span>
</Link>

{!hideThemeToggle && (
<div className="flex items-center justify-between p-3 font-bold border-2 border-transparent">
<div className="flex items-center gap-3">
<Settings className="h-5 w-5 opacity-50" />
<span>Theme</span>
</div>
<ModeToggle />
</div>
)}

{isAdmin && (
<Link
to="/admin"
className="flex items-center gap-3 p-3 font-bold border-2 border-transparent hover:border-border hover:bg-secondary rounded-lg transition-all"
onClick={() => setIsMobileNavOpen(false)}
>
<Shield className="h-5 w-5" />
<span>Admin Dashboard</span>
</Link>
)}

<div className="h-px bg-border my-1" />

<Link
to={user?.username ? `/profile/${user.username}` : "/my/profile"}
className="flex items-center gap-3 p-3 font-bold border-2 border-transparent hover:border-border hover:bg-secondary rounded-lg transition-all"
onClick={() => setIsMobileNavOpen(false)}
>
<User className="h-5 w-5" />
<span>My Profile</span>
</Link>

<Link
to="/my/profile"
className="flex items-center gap-3 p-3 font-bold border-2 border-transparent hover:border-border hover:bg-secondary rounded-lg transition-all"
onClick={() => setIsMobileNavOpen(false)}
>
<Settings className="h-5 w-5" />
<span>Settings</span>
</Link>

<Link
to={user?.username ? `/profile/${user.username}` : "/my/profile"}
className="flex items-center gap-3 p-3 font-bold border-2 border-transparent hover:border-border hover:bg-secondary rounded-lg transition-all"
onClick={() => setIsMobileNavOpen(false)}
>
<MessageSquare className="h-5 w-5" />
<span>My Threads</span>
</Link>

<div className="h-px bg-border my-1" />

<a href="https://github.com/p-society/" target="_blank" rel="noreferrer" className="flex items-center gap-3 p-3 font-bold border-2 border-transparent hover:border-border hover:bg-secondary rounded-lg transition-all">
<Github className="h-5 w-5" />
<span>GitHub</span>
</a>
<a href="https://discord.gg/q74qC2exY4" target="_blank" rel="noreferrer" className="flex items-center gap-3 p-3 font-bold border-2 border-transparent hover:border-border hover:bg-secondary rounded-lg transition-all">
<RxDiscordLogo className="h-5 w-5" />
<span>Discord</span>
</a>

<div className="mt-4">
<Button
variant="neutral"
className="w-full justify-start gap-3 border-2 border-destructive text-destructive hover:bg-destructive hover:text-white"
onClick={() => {
logout();
setIsMobileNavOpen(false);
}}
>
<LogOut className="h-5 w-5" />
<span>Logout</span>
</Button>
</div>
</nav>
</div>
</DrawerContent>
</Drawer>
)}
</div>
</>
) : (
<>
Expand Down
Loading
Loading