Skip to content
Draft
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
2 changes: 2 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import RemoveBackground from "./pages/RemoveBackground"
import ReviewResume from "./pages/ReviewResume"
import Community from "./pages/Community"
import REmoveObject from "./pages/RemoveObject"
import ManagePlan from "./pages/ManagePlan"
import { ThemeProvider } from "./context/ThemeContext"

import {Toaster} from 'react-hot-toast'
Expand All @@ -32,6 +33,7 @@ const App = () => {
<Route path='remove-object' element={<REmoveObject/>} />
<Route path='review-resume' element={<ReviewResume/>} />
<Route path='community' element={<Community/>} />
<Route path='manage-plan' element={<ManagePlan/>} />
</Route>

</Routes>
Expand Down
11 changes: 11 additions & 0 deletions client/src/App.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jest.mock("./pages/RemoveBackground", () => () => <div>Remove Background Page</d
jest.mock("./pages/RemoveObject", () => () => <div>Remove Object Page</div>);
jest.mock("./pages/ReviewResume", () => () => <div>Review Resume Page</div>);
jest.mock("./pages/Community", () => () => <div>Community Page</div>);
jest.mock("./pages/ManagePlan", () => () => <div>Manage Plan Page</div>);

// ---- MOCK CLERK ----
jest.mock("@clerk/clerk-react", () => ({
Expand Down Expand Up @@ -95,4 +96,14 @@ describe("App Routing", () => {

expect(screen.getByText("Community Page")).toBeInTheDocument();
});

test("renders Manage Plan page", () => {
render(
<MemoryRouter initialEntries={["/ai/manage-plan"]}>
<App />
</MemoryRouter>
);

expect(screen.getByText("Manage Plan Page")).toBeInTheDocument();
});
});
14 changes: 10 additions & 4 deletions client/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const navItems = [
{ to: "/ai/community", label: "Community", Icon: Users },
];

const Navbar = ({ theme: propTheme, setTheme: propSetTheme }) => {
const Navbar = ({ theme: propTheme, setTheme: propSetTheme, setSidebar }) => {
const navigate = useNavigate();
const location = useLocation();
const { user } = useUser();
Expand Down Expand Up @@ -107,7 +107,13 @@ const Navbar = ({ theme: propTheme, setTheme: propSetTheme }) => {
<div className="flex items-center gap-2">
{/* Mobile menu toggle */}
<button
onClick={() => setMobileOpen(!mobileOpen)}
onClick={() => {
if (setSidebar) {
setSidebar(prev => !prev);
} else {
setMobileOpen(prev => !prev);
}
}}
className={`lg:hidden p-2 rounded-md transition ${
theme === "dark"
? "text-zinc-300 hover:bg-zinc-800"
Expand Down Expand Up @@ -169,8 +175,8 @@ const Navbar = ({ theme: propTheme, setTheme: propSetTheme }) => {
</div>
</div>

{/* MOBILE MENU */}
{mobileOpen && (
{/* MOBILE MENU - only shown when no sidebar is managed externally */}
{mobileOpen && !setSidebar && (
<div
className={`lg:hidden border-t ${
theme === "dark"
Expand Down
50 changes: 39 additions & 11 deletions client/src/components/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// Sidebar.jsx
import { NavLink } from "react-router-dom";
import { Protect, useClerk, useUser } from "@clerk/clerk-react";
import { Eraser, FileText, Hash, House, ImageIcon, LogOut, Scissors, SquarePen, Users } from "lucide-react";
import { Eraser, FileText, Gem, Hash, House, ImageIcon, LogOut, Scissors, SquarePen, Users } from "lucide-react";
import { Button } from "@/components/ui/button";

const navItems = [
{ to: "/ai", label: "Dashboard", Icon: House },
{ to: "/ai/write-article", label: "Write Article", Icon: SquarePen },
{ to: "/ai/blog-titles", label: "Blog Titles", Icon: Hash },
{ to: "/ai/generate-images", label: "Generate Images", Icon: ImageIcon },
{ to: "/ai/remove-background", label: "Remove Background", Icon: Eraser },
{ to: "/ai/remove-object", label: "Remove Object", Icon: Scissors },
{ to: "/ai/review-resume", label: "Review Resume", Icon: FileText },
{ to: "/ai/community", label: "Community", Icon: Users },
{ to: "/ai", label: "Dashboard", Icon: House, premium: false },
{ to: "/ai/write-article", label: "Write Article", Icon: SquarePen, premium: false },
{ to: "/ai/blog-titles", label: "Blog Titles", Icon: Hash, premium: false },
{ to: "/ai/generate-images", label: "Generate Images", Icon: ImageIcon, premium: true },
{ to: "/ai/remove-background", label: "Remove Background", Icon: Eraser, premium: true },
{ to: "/ai/remove-object", label: "Remove Object", Icon: Scissors, premium: true },
{ to: "/ai/review-resume", label: "Review Resume", Icon: FileText, premium: true },
{ to: "/ai/community", label: "Community", Icon: Users, premium: false },
];

const Sidebar = ({ sidebar, setSidebar }) => {
Expand All @@ -31,7 +31,7 @@ const Sidebar = ({ sidebar, setSidebar }) => {
<h1 className="mt-2 text-center text-base font-semibold text-zinc-100">{user?.fullName}</h1>
</div>
<nav className="mt-6 space-y-2 flex-1 overflow-y-auto">
{navItems.map(({ to, label, Icon }) => (
{navItems.map(({ to, label, Icon, premium }) => (
<NavLink
key={to}
to={to}
Expand All @@ -46,11 +46,39 @@ const Sidebar = ({ sidebar, setSidebar }) => {
{({ isActive }) => (
<>
<Icon className={`w-5 h-5 ${isActive ? "text-zinc-100" : "text-zinc-400"}`} />
{label}
<span className="flex-1">{label}</span>
{premium && (
<Protect plan="premium" fallback={
<span className="text-[10px] font-semibold px-1.5 py-0.5 rounded bg-zinc-700 text-zinc-300 leading-none">
PRO
</span>
}>
{/* Premium users see no badge */}
{null}
</Protect>
)}
</>
)}
</NavLink>
))}

{/* Manage Plan link */}
<NavLink
to="/ai/manage-plan"
onClick={() => setSidebar(false)}
className={({ isActive }) =>
`flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors touch-manipulation ${
isActive ? "bg-zinc-800 text-zinc-100" : "text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100"
}`
}
>
{({ isActive }) => (
<>
<Gem className={`w-5 h-5 ${isActive ? "text-zinc-100" : "text-zinc-400"}`} />
Manage Plan
</>
)}
</NavLink>
</nav>
</div>
<div className="flex-shrink-0 border-t border-zinc-800 p-4 flex items-center justify-between">
Expand Down
25 changes: 19 additions & 6 deletions client/src/pages/Layout.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useState } from "react";
import { Outlet } from "react-router-dom";
import { SignIn, useUser } from "@clerk/clerk-react";
import Navbar from "@/components/Navbar";
import Sidebar from "@/components/Sidebar";
import { useTheme } from "@/context/ThemeContext";

const Layout = () => {
const { user, isLoaded } = useUser();
const { theme } = useTheme();
const [sidebar, setSidebar] = useState(false);

if (!isLoaded) {
return (
Expand All @@ -18,13 +21,23 @@ const Layout = () => {
}

return user ? (
<div className={`min-h-screen selection:bg-zinc-400 ${theme === "dark" ? "bg-zinc-950 text-zinc-100" : "bg-gray-50 text-zinc-900"}`}>
<Navbar />
<div className={`min-h-screen selection:bg-zinc-400 flex flex-col ${theme === "dark" ? "bg-zinc-950 text-zinc-100" : "bg-gray-50 text-zinc-900"}`}>
<Navbar setSidebar={setSidebar} />

{/* offset for fixed navbar */}
<main className="pt-16">
<Outlet />
</main>
<div className="flex flex-1 pt-14 overflow-hidden">
<Sidebar sidebar={sidebar} setSidebar={setSidebar} />
<main className="flex-1 overflow-y-auto">
<Outlet />
</main>
</div>

{/* Overlay to close sidebar on mobile */}
{sidebar && (
<div
className="fixed inset-0 z-40 bg-black/50 sm:hidden"
onClick={() => setSidebar(false)}
/>
)}
</div>
) : (
<div className={`flex items-center justify-center h-screen ${
Expand Down
45 changes: 45 additions & 0 deletions client/src/pages/ManagePlan.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { PricingTable, Protect } from "@clerk/clerk-react";
import { Gem } from "lucide-react";
import { useTheme } from "@/context/ThemeContext";

const ManagePlan = () => {
const { theme } = useTheme();
const isDark = theme === "dark";

return (
<section
className={`min-h-[calc(100vh-64px)] px-4 sm:px-6 lg:px-10 py-8 transition-colors ${
isDark ? "bg-zinc-950 text-zinc-100" : "bg-gray-50 text-zinc-900"
}`}
>
<div className="max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-zinc-200 dark:bg-zinc-800 flex items-center justify-center">
<Gem className="w-5 h-5 text-zinc-700 dark:text-zinc-300" />
</div>
<div>
<h1 className="text-2xl font-semibold tracking-tight">Manage Plan</h1>
<p className="text-sm text-zinc-600 dark:text-zinc-400">
Current plan:{" "}
<span className="font-medium">
<Protect plan="premium" fallback="Free">Premium</Protect>
</span>
</p>
</div>
</div>

{/* Pricing Table */}
<div>
<PricingTable
className={`shadow-xl rounded-2xl border ${
isDark ? "border-zinc-800" : "border-gray-200"
}`}
/>
</div>
</div>
</section>
);
};

export default ManagePlan;
12 changes: 2 additions & 10 deletions server/middlewares/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,15 @@ export const auth = async (req, res, next) => {

const user = await clerkClient.users.getUser(userId);

if (!hasPremiumPlan && user.privateMetadata.free_usage) {
req.free_usage = user.privateMetadata.free_usage
if (!hasPremiumPlan) {
req.free_usage = user.privateMetadata.free_usage ?? 0;
} else {
await clerkClient.users.updateUserMetadata(userId, {
privateMetadata: {
free_usage: 0
}
})
req.free_usage = 0;

}

req.plan = hasPremiumPlan ? 'premium' : 'free';
next()
} catch (error) {
res.json({ success: false, message: error.message })
}


}