Skip to content
Open
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
439 changes: 437 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"start": "concurrently \"npm run dev\" \"npm run backend\"",
"backend": "cd backend && npm run dev",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@propelauth/react": "^2.0.29",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
Expand Down Expand Up @@ -39,6 +42,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@supabase/supabase-js": "^2.49.1",
"@tanstack/react-query": "^5.56.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down Expand Up @@ -69,6 +73,7 @@
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"concurrently": "^9.1.2",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
Expand Down
31 changes: 16 additions & 15 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,27 @@ import Login from "./pages/Login";
import Signup from "./pages/Signup";
import Dashboard from "./pages/Dashboard";
import NotFound from "./pages/NotFound";
import AuthProvider from "./utils/AuthProvider";

const queryClient = new QueryClient();

const App = () => (
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="light">
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/pricing" element={<PricingPage />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
<ThemeProvider>
<AuthProvider>
<TooltipProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/pricing" element={<PricingPage />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
</AuthProvider>
</ThemeProvider>
</QueryClientProvider>
);
Expand Down
94 changes: 94 additions & 0 deletions src/components/AuthComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useAuthInfo, useRedirectFunctions, WithLoggedInAuthInfoProps, withRequiredAuthInfo } from "@propelauth/react";
import { Navigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { useLogoutFunction } from "@propelauth/react"
import { LogOut, User } from "lucide-react";
import Loader from "./ui/loader";

// A component that redirects to the dashboard if logged in
export const RedirectIfLoggedIn = ({ children }: { children: React.ReactNode }) => {
const { isLoggedIn, loading } = useAuthInfo();



if (loading) {
return <Loader />;
}

if (isLoggedIn) {
return <Navigate to="/dashboard" replace />;
}

return <>{children}</>;
};

// A component that requires authentication
export const ProtectedRoute = withRequiredAuthInfo(
({ children, ...authProps }: { children: React.ReactNode } & WithLoggedInAuthInfoProps) => {
return <>{children}</>;
},
{
// Custom component to show while loading
displayWhileLoading: <Loader />,
// Custom component to show if not logged in
displayIfLoggedOut: <Navigate to="/login" replace />
}
);

// Login button component
export const LoginButton = () => {
const { redirectToLoginPage } = useRedirectFunctions();

return (
<Button
onClick={() => redirectToLoginPage({ postLoginRedirectUrl: window.location.href })}
variant="default"
>
Log In
</Button>
);
};

// Signup button component
export const SignupButton = () => {
const { redirectToSignupPage } = useRedirectFunctions();

return (
<Button
onClick={() => redirectToSignupPage({ postSignupRedirectUrl: window.location.href })}
variant="outline"
>
Sign Up
</Button>
);
};

// Logout button component
export const LogoutButton = () => {
const logout = useLogoutFunction();


return (
<Button variant="ghost" className="w-full justify-start my-2" asChild onClick={() => logout(false)}>
<a href="/login" className="flex items-center gap-2">
<LogOut className="h-5 w-5" />
<span>Log out</span>
</a>
</Button>
);
};

// User profile component
export const UserProfile = withRequiredAuthInfo(({ user }) => {
return (
<div className="flex items-center gap-2">
<div className="h-9 w-9 rounded-full bg-primary/10 flex items-center justify-center">
<User className="h-4 w-4 text-primary" />
</div>
<div>
<p className="text-sm font-medium">{user.email}</p>
<p className="text-xs text-muted-foreground">Premium Plan</p>
</div>
</div>
);
});
117 changes: 60 additions & 57 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { Button } from "@/components/ui/button";
import { MoonIcon, SunIcon, MenuIcon, XIcon } from "lucide-react";
import { useTheme } from "@/hooks/use-theme";
import { useIsMobile } from "@/hooks/use-mobile";
import { useAuthInfo } from "@propelauth/react";
import { LoginButton, SignupButton, LogoutButton, UserProfile } from "@/components/AuthComponents";

const Navbar = () => {
const { theme, setTheme } = useTheme();
const isMobile = useIsMobile();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const { isLoggedIn } = useAuthInfo();

useEffect(() => {
const handleScroll = () => {
Expand All @@ -34,30 +37,27 @@ const Navbar = () => {
setIsMenuOpen(!isMenuOpen);
};

const navbarClasses = `fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
scrolled
? "bg-background/80 backdrop-blur-md shadow-sm"
: "bg-transparent"
}`;
const navbarClasses = `fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${scrolled
? "bg-background/80 backdrop-blur-md shadow-sm"
: "bg-transparent"
}`;

return (
<nav className={navbarClasses}>
<div className="container mx-auto px-4 md:px-6 py-4">
<div className="flex items-center justify-between">
<Link
to="/"
className="text-xl font-semibold tracking-tight transition-colors hover:text-primary animate-fade-in"
>
Calculus Tutor
</Link>
<div className="container mx-auto px-4 md:px-6 py-3">
<div className="flex justify-between items-center">
<div className="flex items-center gap-6">
<Link to="/" className="flex items-center gap-2 font-semibold">
<span>Calculus Tutor</span>
</Link>
</div>

{isMobile ? (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
aria-label="Toggle theme"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="rounded-full"
>
{theme === "dark" ? (
Expand All @@ -69,8 +69,7 @@ const Navbar = () => {
<Button
variant="ghost"
size="icon"
onClick={toggleMenu}
aria-label="Toggle menu"
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="rounded-full"
>
{isMenuOpen ? (
Expand All @@ -83,35 +82,26 @@ const Navbar = () => {
) : (
<div className="flex items-center gap-4">
<div className="flex gap-6">
<Link
to="/dashboard"
className="text-sm font-medium transition-colors hover:text-primary"
>
Dashboard
</Link>
<Link
to="/pricing"
{isLoggedIn ? (
<Link
to="/dashboard"
className="text-sm font-medium transition-colors hover:text-primary"
>
Dashboard
</Link>
) : null}
<Link
to="/pricing"
className="text-sm font-medium transition-colors hover:text-primary"
>
Pricing
</Link>
</div>
<div className="flex items-center gap-2">
<Link to="/login">
<Button variant="outline" size="sm" className="rounded-full px-4">
Log in
</Button>
</Link>
<Link to="/signup">
<Button size="sm" className="rounded-full px-4">
Sign up
</Button>
</Link>
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
aria-label="Toggle theme"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="rounded-full"
>
{theme === "dark" ? (
Expand All @@ -120,6 +110,16 @@ const Navbar = () => {
<MoonIcon className="h-5 w-5" />
)}
</Button>
{isLoggedIn ? (
<div className="flex items-center gap-2">
<LogoutButton />
</div>
) : (
<div className="flex items-center gap-2">
<LoginButton />
<SignupButton />
</div>
)}
</div>
</div>
)}
Expand All @@ -129,31 +129,34 @@ const Navbar = () => {
{isMobile && isMenuOpen && (
<div className="py-4 animate-slide-in">
<div className="flex flex-col gap-4">
<Link
to="/dashboard"
className="text-sm font-medium transition-colors hover:text-primary px-2 py-1"
onClick={() => setIsMenuOpen(false)}
>
Dashboard
</Link>
<Link
to="/pricing"
{isLoggedIn ? (
<Link
to="/dashboard"
className="text-sm font-medium transition-colors hover:text-primary px-2 py-1"
onClick={() => setIsMenuOpen(false)}
>
Dashboard
</Link>
) : null}
<Link
to="/pricing"
className="text-sm font-medium transition-colors hover:text-primary px-2 py-1"
onClick={() => setIsMenuOpen(false)}
>
Pricing
</Link>
<div className="flex flex-col gap-2 pt-2">
<Link to="/login" onClick={() => setIsMenuOpen(false)}>
<Button variant="outline" className="w-full justify-start rounded-lg">
Log in
</Button>
</Link>
<Link to="/signup" onClick={() => setIsMenuOpen(false)}>
<Button className="w-full justify-start rounded-lg">
Sign up
</Button>
</Link>
<div className="pt-2 mt-2 border-t">
{isLoggedIn ? (
<div className="flex flex-col gap-2">
<UserProfile />
<LogoutButton />
</div>
) : (
<div className="flex flex-col gap-2">
<LoginButton />
<SignupButton />
</div>
)}
</div>
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions src/components/ui/loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

export default function Loader() {
return (
<div className="flex items-center justify-center h-screen">
<div className="loader"></div>
</div>
)
}
Loading