Skip to content

Commit

Permalink
Merge pull request #34 from kscalelabs/second_milestone_serhii
Browse files Browse the repository at this point in the history
feature_protected_router_and_subscription
  • Loading branch information
Serhii Ofii authored Sep 20, 2024
2 parents 3c87365 + 1a3f7ae commit 771ea9d
Show file tree
Hide file tree
Showing 30 changed files with 602 additions and 67 deletions.
3 changes: 2 additions & 1 deletion frontend/.env.development
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
REACT_APP_BACKEND_URL=http://localhost:8080
REACT_APP_BACKEND_URL=http://localhost:8080
REACT_APP_STRIPE_API_KEY=pk_test_51Nv2EyKeTo38dsfeVEz53ZE9wFsAwwfQKEW5SROEb1ZbPg0rkX9xbZRi0ah7bjVQ7cuoy4dGH85rTag1TcLrhAi50043215z40
3 changes: 2 additions & 1 deletion frontend/.env.production
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
REACT_APP_BACKEND_URL=https://api.linguaphoto.com
REACT_APP_BACKEND_URL=https://api.linguaphoto.com
REACT_APP_STRIPE_API_KEY=pk_live_51Nv2EyKeTo38dsfexsIGE1kPyPq8O7GpmsS6lFfmlOdfD5m70Mgq3c8Uq7CL0LvroMLybjxy0j28ErOKHwFQtFMf00p46WUYdr
9 changes: 8 additions & 1 deletion frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ export default [
...pluginReactConfig,
rules: {
...pluginReactConfig.rules,
"react/react-in-jsx-scope": "off",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off", // Disable prop-types validation
},
}),
{
files: ["*.ts", "*.tsx"], // Apply this rule to TypeScript files
rules: {
"react/prop-types": "off", // Ensure prop-types are off for TypeScript files
},
},
];
23 changes: 23 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@stripe/react-stripe-js": "^2.8.0",
"@stripe/stripe-js": "^4.5.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
Expand Down
50 changes: 46 additions & 4 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import Collections from "pages/Collections";
import Home from "pages/Home";
import LoginPage from "pages/Login";
import NotFound from "pages/NotFound";
import SubscriptionTypePage from "pages/SubscriptioinType";
import SubscriptionCancelPage from "pages/Subscription";
import Test from "pages/Test";
import PrivateRoute from "ProtectedRoute";
import { Container } from "react-bootstrap";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import "./App.css";
Expand All @@ -29,17 +32,56 @@ const App = () => {
<Route path="/" element={<Home />} />
<Route path="/test" element={<Test />} />
<Route path="/404" element={<NotFound />} />
<Route path="/collections" element={<Collections />} />
<Route path="/collection" element={<CollectionPage />} />
<Route
path="/collections"
element={
<PrivateRoute
element={<Collections />}
requiredSubscription={true} // Set true if subscription is required for this route
/>
}
/>
<Route
path="/collection"
element={
<PrivateRoute
element={<CollectionPage />}
requiredSubscription={true} // Set true if subscription is required for this route
/>
}
/>
<Route
path="/collection/new"
element={<CollectionPage />}
element={
<PrivateRoute
element={<CollectionPage />}
requiredSubscription={true} // Set true if subscription is required for this route
/>
}
/>
<Route
path="/collection/:id"
element={<CollectionPage />}
element={
<PrivateRoute
element={<CollectionPage />}
requiredSubscription={true} // Set true if subscription is required for this route
/>
}
/>
<Route
path="/subscription"
element={
<PrivateRoute
element={<SubscriptionCancelPage />}
requiredSubscription={true} // Set true if subscription is required for this route
/>
}
/>
<Route path="/login" element={<LoginPage />} />
<Route
path="/subscription_type"
element={<SubscriptionTypePage />}
/>
<Route path="*" element={<NotFoundRedirect />} />
</Routes>
</Container>
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useAuth } from "contexts/AuthContext";
import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";

interface PrivateRouteProps {
element: JSX.Element;
requiredSubscription?: boolean; // Optional flag for subscription requirement
}

const PrivateRoute: React.FC<PrivateRouteProps> = ({
element,
requiredSubscription = false,
}) => {
const { auth } = useAuth();
const location = useLocation();
const navigate = useNavigate();

useEffect(() => {
if (auth)
if (!auth?.is_auth) {
// Redirect to login if not authenticated
navigate("/login", { replace: true, state: { from: location } });
} else if (requiredSubscription && !auth?.is_subscription) {
// Redirect to subscription page if subscription is required and not active
navigate("/subscription_type", { replace: true });
}
}, [auth, requiredSubscription, navigate, location]);

if (!auth?.is_auth || (requiredSubscription && !auth?.is_subscription)) {
// Render nothing while redirecting
return null;
}

return element;
};

export default PrivateRoute;
21 changes: 21 additions & 0 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export interface CollectionCreateFragment {
title: string;
description: string;
}
// Define the types for the API response and request payload
export interface SubscriptionResponse {
success: boolean;
error?: string;
requires_action?: boolean;
payment_intent_client_secret?: string;
}
export class Api {
public api: AxiosInstance;

Expand Down Expand Up @@ -75,4 +82,18 @@ export class Api {
);
return response.data;
}

public async createSubscription(
payment_method_id: string,
email: string,
name: string,
): Promise<SubscriptionResponse> {
// Send payment method to the backend for subscription creation
const { data } = await this.api.post("/create_subscription", {
payment_method_id,
email,
name,
});
return data;
}
}
10 changes: 5 additions & 5 deletions frontend/src/components/nav/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ interface SidebarProps {

const Sidebar = ({ show, onClose }: SidebarProps) => {
const navigate = useNavigate();
const { is_auth, signout } = useAuth();
const { auth, signout } = useAuth();
return (
<div>
{show ? (
Expand Down Expand Up @@ -106,7 +106,7 @@ const Sidebar = ({ show, onClose }: SidebarProps) => {
}}
size="md"
/>
{is_auth ? (
{auth?.is_auth ? (
<SidebarItem
title="Collections"
icon={<FaThList />}
Expand All @@ -120,10 +120,10 @@ const Sidebar = ({ show, onClose }: SidebarProps) => {
<></>
)}
<SidebarItem
title="Privacy"
title="Subscription"
icon={<FaLock />}
onClick={() => {
navigate("/privacy");
navigate("/subscription");
onClose();
}}
size="md"
Expand All @@ -134,7 +134,7 @@ const Sidebar = ({ show, onClose }: SidebarProps) => {
<hr className="my-4 border-gray-300 dark:border-gray-600" />

<ul className="space-y-1">
{is_auth ? (
{auth?.is_auth ? (
<SidebarItem
title="Logout"
icon={<FaSignOutAlt />}
Expand Down
113 changes: 98 additions & 15 deletions frontend/src/components/nav/TopNavbar.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,120 @@
import { useAuth } from "contexts/AuthContext";
import { useTheme } from "hooks/theme";
import { useState } from "react";
import { Container, Nav, Navbar } from "react-bootstrap";
import { GearFill, MoonFill, SunFill } from "react-bootstrap-icons";
import { Link } from "react-router-dom";
import { MoonFill, SunFill } from "react-bootstrap-icons";
import {
FaBars,
FaLock,
FaSignInAlt,
FaSignOutAlt,
FaThList,
} from "react-icons/fa";
import { Link, useLocation } from "react-router-dom";
import Sidebar from "./Sidebar";

const TopNavbar = () => {
const [showSidebar, setShowSidebar] = useState<boolean>(false);
const { theme, setTheme } = useTheme();

const location = useLocation(); // To determine the active link
const { auth, signout } = useAuth();
return (
<>
<Navbar
className="fixed w-full top-0 z-50 bg-gray-100 dark:bg-gray-800 justify-content-between"
className="fixed w-full top-0 z-50 bg-gray-100 dark:bg-gray-800 justify-content-between shadow-lg"
expand="lg"
style={{ height: "60px" }} // Set navbar height
>
<Container>
<Navbar.Brand as={Link} to="/">
<Container className="flex justify-between items-center h-full">
<Navbar.Brand
as={Link}
to="/"
className="text-xl text-gray-900 dark:text-white" // Reduced font size
>
LinguaPhoto
</Navbar.Brand>

<div className="d-flex gap-3">
<Nav.Link
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
{theme === "dark" ? <MoonFill /> : <SunFill />}
</Nav.Link>
<Nav.Link onClick={() => setShowSidebar(true)}>
<GearFill />
</Nav.Link>
<div className="flex gap-6 items-center">
{/* Main Navigation Links */}
<div className="hidden sm:flex gap-6">
{" "}
{/* Hidden on small screens */}
{auth?.is_auth ? (
<>
<Nav.Link
as={Link}
to="/collections"
className={`flex items-center gap-2 px-3 py-2 text-sm transition-colors rounded-md ${
location.pathname === "/collections"
? "text-blue-600 dark:text-blue-400 font-semibold"
: "text-gray-800 dark:text-gray-300"
} hover:text-blue-500 dark:hover:text-blue-300`}
>
<FaThList /> <span>Collections</span>
</Nav.Link>

<Nav.Link
as={Link}
to="/subscription"
className={`flex items-center gap-2 px-3 py-2 text-sm transition-colors rounded-md ${
location.pathname === "/subscription"
? "text-blue-600 dark:text-blue-400 font-semibold"
: "text-gray-800 dark:text-gray-300"
} hover:text-blue-500 dark:hover:text-blue-300`}
>
<FaLock /> <span>Subscription</span>
</Nav.Link>
<Nav.Link
as={Link}
to="/login"
className="flex items-center gap-2 px-3 py-2 text-sm transition-colors rounded-md hover:text-blue-500 dark:hover:text-blue-300"
onClick={() => {
// Handle logout logic here
signout();
}}
>
<FaSignOutAlt /> <span>Logout</span>
</Nav.Link>
</>
) : (
<Nav.Link
as={Link}
to="/login"
className={`flex items-center gap-2 px-3 py-2 text-sm transition-colors rounded-md ${
location.pathname === "/login"
? "text-blue-600 dark:text-blue-400 font-semibold"
: "text-gray-800 dark:text-gray-300"
} hover:text-blue-500 dark:hover:text-blue-300`}
onClick={() => {
// Handle logout logic here
signout();
}}
>
<FaSignInAlt /> <span>Login / Sign Up</span>
</Nav.Link>
)}
</div>

{/* Theme Toggle and Sidebar */}
<div className="flex items-center gap-3">
<Nav.Link
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="flex items-center text-lg text-gray-800 dark:text-gray-300"
>
{theme === "dark" ? <SunFill /> : <MoonFill />}
</Nav.Link>

<Nav.Link
onClick={() => setShowSidebar(true)}
className="flex items-center text-lg text-gray-800 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-300 lg:hidden" // Show only on small screens
>
<FaBars /> {/* Hamburger Button */}
</Nav.Link>
</div>
</div>
</Container>
</Navbar>

<Sidebar show={showSidebar} onClose={() => setShowSidebar(false)} />
</>
);
Expand Down
Loading

0 comments on commit 771ea9d

Please sign in to comment.