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
4 changes: 2 additions & 2 deletions src/app/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NavigationBar } from "../../components/navigation-bar/NavigationBar";

export const Layout = () => {
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-gray-50 flex flex-col">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: 🟠 Major

Add a small regression test for the footer-bottom flex layout so future class refactors don’t reintroduce the floating footer.

(For example: render the layout with a minimal router + outlet, then assert the root has flex flex-col and <main> has flex-1.)

<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
Expand All @@ -14,7 +14,7 @@ export const Layout = () => {
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<main className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 w-full">
<Outlet />
</main>
<Footer />
Expand Down
2 changes: 1 addition & 1 deletion src/components/footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const Footer = () => {
return (
<footer className="bg-white border-t border-gray-200 mt-16">
<footer className="bg-white border-t border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<p className="text-center text-gray-600 text-sm">
React Resource Hub - Your curated collection of React learning
Expand Down
43 changes: 43 additions & 0 deletions src/components/googlelogo/GoogleLogo.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { GoogleLogo } from "./GoogleLogo";

interface RenderOptions {
className?: string;
}

const defaultProps: RenderOptions = {
className: "w-5 h-5",
};

const renderGoogleLogo = (additionalProps: RenderOptions = {}) => {
const props = { ...defaultProps, ...additionalProps };

return render(<GoogleLogo className={props.className} />);
};

describe("GoogleLogo", () => {
it("should render an svg element", () => {
const { container } = renderGoogleLogo();

const svg = container.querySelector("svg");

expect(svg).toBeInTheDocument();
});

it("should apply provided className when passed", () => {
const { container } = renderGoogleLogo({ className: "w-8 h-8" });

const svg = container.querySelector("svg");

expect(svg).toHaveClass("w-8");
});

it("should be hidden from the accessibility tree", () => {
const { container } = renderGoogleLogo();

const svg = container.querySelector("svg");

expect(svg).toHaveAttribute("aria-hidden", "true");
});
});
32 changes: 32 additions & 0 deletions src/components/googlelogo/GoogleLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
type GoogleLogoProps = {
className?: string;
};

export const GoogleLogo = ({ className }: GoogleLogoProps) => {
return (
<svg
className={className ?? "w-5 h-5"}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
>
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
);
};
69 changes: 69 additions & 0 deletions src/features/auth/SignInPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it } from "vitest";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: 🟠 Major

Import expect (or ensure Vitest globals are enabled) so the test file works consistently with the rest of the suite.

import { describe, it, expect } from "vitest";

import { BrowserRouter } from "react-router-dom";
import { SignInPage } from "./SignInPage";

const renderSignInPage = () => {
return render(
<BrowserRouter>
<SignInPage />
</BrowserRouter>,
);
};

describe("SignInPage", () => {
it("should render the email input field", () => {
renderSignInPage();

expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
});

it("should render the password input field", () => {
renderSignInPage();

expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
});

it("should allow typing in the email input", async () => {
renderSignInPage();
const user = userEvent.setup();

const emailInput = screen.getByLabelText(/email/i);
await user.type(emailInput, "test@example.com");

expect(emailInput).toHaveValue("test@example.com");
});

it("should allow typing in the password input", async () => {
renderSignInPage();
const user = userEvent.setup();

const passwordInput = screen.getByLabelText(/password/i);
await user.type(passwordInput, "password123");

expect(passwordInput).toHaveValue("password123");
});

it("should render the sign in button", () => {
renderSignInPage();

expect(
screen.getByRole("button", { name: /sign in/i }),
).toBeInTheDocument();
});

it("should render the continue with google button", () => {
renderSignInPage();

expect(
screen.getByRole("button", { name: /continue with google/i }),
).toBeInTheDocument();
});

it("should render the sign up link", () => {
renderSignInPage();

expect(screen.getByRole("link", { name: /sign up/i })).toBeInTheDocument();
});
});
92 changes: 92 additions & 0 deletions src/features/auth/SignInPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { Button } from "../../components/button/Button";
import { Input } from "../../components/input/Input";
import { Card, CardBody } from "../../components/card/Card";
import { GoogleLogo } from "../../components/googlelogo/GoogleLogo";

export const SignInPage = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

const handleSubmit = (event: React.FormEvent) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: 🔴 Critical

Avoid relying on a global React namespace for event types—import FormEvent (or type React) so TS builds don’t break.

import { useState, type FormEvent } from "react";
...
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {

event.preventDefault();
};

return (
<div className="min-h-screen bg-linear-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="w-full max-w-md">
<Card>
<CardBody className="space-y-6">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-2">
Welcome Back
</h1>
<p className="text-gray-600">
Sign in to access your saved resources
</p>
</div>

<form onSubmit={handleSubmit} className="space-y-4">
<Input
label="Email"
type="email"
placeholder="Enter your email"
value={email}
onChange={(event) => setEmail(event.target.value)}
className="w-full"
required
autoComplete="email"
/>

<Input
label="Password"
type="password"
placeholder="Enter your password"
value={password}
onChange={(event) => setPassword(event.target.value)}
className="w-full"
required
autoComplete="current password"
/>

<Button type="submit" className="w-full">
Sign In
</Button>
</form>

<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">
Or continue with
</span>
</div>
</div>

<Button
variant="outline"
type="button"
className="w-full flex gap-2"
>
<GoogleLogo />
Continue with Google
</Button>

<p className="text-center text-gray-600 text-sm">
Don't have an account?{" "}
<Link
to="/signup"
className="text-blue-600 hover:text-blue-700 font-medium"
>
Sign up
</Link>
</p>
</CardBody>
</Card>
</div>
</div>
);
};