Browse Source

Introduce Login page

Tatiana Inama 4 years atrás
parent
commit
b1b7f18e9c
14 changed files with 296 additions and 91 deletions
  1. 19 0
      components/Layout.tsx
  2. 37 0
      components/Nav.tsx
  3. 1 0
      package.json
  4. 4 4
      pages/_app.tsx
  5. 0 13
      pages/api/hello.ts
  6. 21 0
      pages/api/login.ts
  7. 12 69
      pages/index.tsx
  8. 79 0
      pages/login.tsx
  9. 1 2
      styles/globals.css
  10. 37 2
      tailwind.config.js
  11. 2 1
      tsconfig.json
  12. 39 0
      utils/api.ts
  13. 32 0
      utils/apiErrorHandler.ts
  14. 12 0
      yarn.lock

+ 19 - 0
components/Layout.tsx

@@ -0,0 +1,19 @@
+import { FC } from "react";
+import Head from "next/head";
+import Nav from "./Nav";
+
+const Layout: FC = ({ children }) => (
+  <>
+    <Head>
+      <title>Prisma Home Challenge</title>
+      <meta name="description" content="My take on Prisma's Home Challenge" />
+      <link rel="icon" href="/favicon.ico" />
+    </Head>
+    <Nav />
+    <main className="mx-auto w-full max-w-screen-lg px-4 mt-24">
+      {children}
+    </main>
+  </>
+);
+
+export default Layout;

+ 37 - 0
components/Nav.tsx

@@ -0,0 +1,37 @@
+import { FC } from "react";
+
+type NavTypes = {
+  SecondaryNavigation?: FC;
+};
+
+const Nav: FC<NavTypes> = ({ SecondaryNavigation }) => (
+  <header className="bg-prisma-50 w-full min-h-nav flex items-end">
+    <nav className="w-full max-w-screen-lg flex mx-auto justify-between">
+      <ul className="flex">
+        <li className="">
+          <a href="#" className="py-7 px-4 block font-bold">
+            Prisma
+          </a>
+        </li>
+      </ul>
+      {SecondaryNavigation ? (
+        <SecondaryNavigation />
+      ) : (
+        <ul className="flex">
+          <li className="">
+            <a href="#" className="py-7 px-4 block">
+              Sign up
+            </a>
+          </li>
+          <li className="">
+            <a href="#" className="py-7 px-4 block">
+              Log in
+            </a>
+          </li>
+        </ul>
+      )}
+    </nav>
+  </header>
+);
+
+export default Nav;

+ 1 - 0
package.json

@@ -10,6 +10,7 @@
     "prettier": "prettier --write ."
   },
   "dependencies": {
+    "axios": "^0.26.1",
     "next": "12.1.0",
     "react": "17.0.2",
     "react-dom": "17.0.2"

+ 4 - 4
pages/_app.tsx

@@ -1,8 +1,8 @@
-import '../styles/globals.css'
-import type { AppProps } from 'next/app'
+import "../styles/globals.css";
+import type { AppProps } from "next/app";
 
 function MyApp({ Component, pageProps }: AppProps) {
-  return <Component {...pageProps} />
+  return <Component {...pageProps} />;
 }
 
-export default MyApp
+export default MyApp;

+ 0 - 13
pages/api/hello.ts

@@ -1,13 +0,0 @@
-// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
-import type { NextApiRequest, NextApiResponse } from "next";
-
-type Data = {
-  name: string;
-};
-
-export default function handler(
-  req: NextApiRequest,
-  res: NextApiResponse<Data>
-) {
-  res.status(200).json({ name: "John Doe" });
-}

+ 21 - 0
pages/api/login.ts

@@ -0,0 +1,21 @@
+import axios from "axios";
+import type { NextApiHandler } from "next";
+import { errorHandler } from "../../utils/apiErrorHandler";
+
+const loginHandler: NextApiHandler = async (req, res) => {
+  if (req.body) {
+    try {
+      const response = await axios.post(
+        `${process.env.PRISMA_API}/login`,
+        req.body,
+        { headers: { "Content-Type": "application/json" } }
+      );
+      res.status(200).json(response.data);
+    } catch (error) {
+      const { status, message } = errorHandler(error);
+      res.status(status).json({ message });
+    }
+  }
+};
+
+export default loginHandler;

+ 12 - 69
pages/index.tsx

@@ -1,72 +1,15 @@
-import type { NextPage } from 'next'
-import Head from 'next/head'
-import Image from 'next/image'
-import styles from '../styles/Home.module.css'
+import type { NextPage } from "next";
+import Layout from "../components/Layout";
 
 const Home: NextPage = () => {
   return (
-    <div className={styles.container}>
-      <Head>
-        <title>Create Next App</title>
-        <meta name="description" content="Generated by create next app" />
-        <link rel="icon" href="/favicon.ico" />
-      </Head>
-
-      <main className={styles.main}>
-        <h1 className={styles.title}>
-          Welcome to <a href="https://nextjs.org">Next.js!</a>
-        </h1>
-
-        <p className={styles.description}>
-          Get started by editing{' '}
-          <code className={styles.code}>pages/index.tsx</code>
-        </p>
-
-        <div className={styles.grid}>
-          <a href="https://nextjs.org/docs" className={styles.card}>
-            <h2>Documentation &rarr;</h2>
-            <p>Find in-depth information about Next.js features and API.</p>
-          </a>
-
-          <a href="https://nextjs.org/learn" className={styles.card}>
-            <h2>Learn &rarr;</h2>
-            <p>Learn about Next.js in an interactive course with quizzes!</p>
-          </a>
-
-          <a
-            href="https://github.com/vercel/next.js/tree/canary/examples"
-            className={styles.card}
-          >
-            <h2>Examples &rarr;</h2>
-            <p>Discover and deploy boilerplate example Next.js projects.</p>
-          </a>
-
-          <a
-            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
-            className={styles.card}
-          >
-            <h2>Deploy &rarr;</h2>
-            <p>
-              Instantly deploy your Next.js site to a public URL with Vercel.
-            </p>
-          </a>
-        </div>
-      </main>
-
-      <footer className={styles.footer}>
-        <a
-          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Powered by{' '}
-          <span className={styles.logo}>
-            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
-          </span>
-        </a>
-      </footer>
-    </div>
-  )
-}
-
-export default Home
+    <Layout>
+      <div className="">
+        <h1 className="">Welcome!</h1>
+        <p className="">Log in to get started</p>
+      </div>
+    </Layout>
+  );
+};
+
+export default Home;

+ 79 - 0
pages/login.tsx

@@ -0,0 +1,79 @@
+import { NextPage } from "next";
+import { FormEventHandler, useState } from "react";
+import Layout from "../components/Layout";
+import fetchApi, { ApiMethods, ApiPaths } from "../utils/api";
+
+type FormError = {
+  message: string;
+};
+
+const Login: NextPage = () => {
+  const [error, setError] = useState<FormError | null>();
+
+  const submitHandler: FormEventHandler<HTMLFormElement> = async (event) => {
+    event.preventDefault();
+    const formData = new FormData(event.currentTarget);
+    const { error, message } = await fetchApi({
+      method: ApiMethods.POST,
+      path: ApiPaths.Login,
+      body: Object.fromEntries(formData.entries()),
+    });
+    if (error) {
+      setError({ message: error });
+    } else {
+      console.log(message);
+    }
+  };
+
+  return (
+    <Layout>
+      <form
+        className="w-full max-w-screen-xs mx-auto flex flex-col xs:grid xs:grid-cols-form xs:items-center gap-4"
+        onSubmit={submitHandler}
+      >
+        <div className="xs:contents">
+          <label htmlFor="email" className="font-bold flex-1 xs:text-right">
+            Email
+          </label>
+          <input
+            type="email"
+            name="email"
+            id="email"
+            className="bg-prisma-50 w-full rounded py-2 px-3 text-sm"
+            placeholder="monkey@gmail.com"
+            required
+          />
+        </div>
+        <div className="xs:contents">
+          <label htmlFor="password" className="font-bold flex-1 xs:text-right">
+            Password
+          </label>
+          <input
+            type="password"
+            name="password"
+            id="password"
+            className="bg-prisma-50 w-full rounded py-2 px-3 text-sm"
+            placeholder="••••••"
+            required
+          />
+        </div>
+        <button
+          type="submit"
+          className="col-start-2 w-max py-2 px-4 bg-prisma-900 text-white font-bold rounded text-sm hover:bg-prisma-800 active:bg-prisma-700"
+        >
+          Log in
+        </button>
+        {error && (
+          <div
+            role="alert"
+            className="col-span-2 bg-red-200 p-4 text-center rounded font-bold text-red-900"
+          >
+            {error.message}
+          </div>
+        )}
+      </form>
+    </Layout>
+  );
+};
+
+export default Login;

+ 1 - 2
styles/globals.css

@@ -5,8 +5,7 @@
 @layer base {
   html,
   body {
-    padding: 0;
-    margin: 0;
+    @apply text-prisma-900 p-0 m-0;
     font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
       Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
   }

+ 37 - 2
tailwind.config.js

@@ -1,7 +1,42 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+
+const defaultTheme = require("tailwindcss/defaultTheme");
+const colors = require("tailwindcss/colors");
+
 module.exports = {
-  content: [],
+  content: [
+    "./pages/**/*.{js,ts,jsx,tsx}",
+    "./components/**/*.{js,ts,jsx,tsx}",
+  ],
   theme: {
+    colors: {
+      white: colors.white,
+      black: colors.black,
+      red: colors.red,
+      prisma: {
+        50: "#ebebeb",
+        100: "#d2d2d2",
+        200: "#bcbcbc",
+        300: "#a5a5a5",
+        400: "#8f8f8f",
+        500: "#797979",
+        600: "#626262",
+        700: "#4c4c4c",
+        800: "#353535",
+        900: "#1f1f1f",
+      },
+    },
+    minHeight: {
+      nav: "7rem",
+    },
+    gridTemplateColumns: {
+      form: "1fr minmax(15rem, 2fr)",
+    },
+    screens: {
+      xs: "475px",
+      ...defaultTheme.screens,
+    },
     extend: {},
   },
   plugins: [],
-}
+};

+ 2 - 1
tsconfig.json

@@ -1,6 +1,7 @@
 {
   "compilerOptions": {
     "target": "es5",
+    "downlevelIteration": true,
     "lib": ["dom", "dom.iterable", "esnext"],
     "allowJs": true,
     "skipLibCheck": true,
@@ -16,5 +17,5 @@
     "incremental": true
   },
   "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
-  "exclude": ["node_modules"]
+  "exclude": ["node_modules", "*.js"]
 }

+ 39 - 0
utils/api.ts

@@ -0,0 +1,39 @@
+export enum ApiMethods {
+  GET = "GET",
+  POST = "POST",
+}
+
+export enum ApiPaths {
+  Login = "login",
+}
+
+type FetchApiProps<T> = {
+  method: ApiMethods;
+  path: ApiPaths;
+  body: T;
+};
+
+type FetchApiResult = { error?: string; message?: string };
+
+const fetchApi = async <T>({
+  method,
+  path,
+  body,
+}: FetchApiProps<T>): Promise<FetchApiResult> => {
+  const response = await fetch(`/api/${path}`, {
+    method: method,
+    body: JSON.stringify(body),
+  });
+
+  const { message } = await response.json();
+
+  if (response.ok) {
+    return { message };
+  } else {
+    return {
+      error: message,
+    };
+  }
+};
+
+export default fetchApi;

+ 32 - 0
utils/apiErrorHandler.ts

@@ -0,0 +1,32 @@
+import axios from "axios";
+
+enum HttpError {
+  Unauthorized = 401,
+  ServerError = 500,
+}
+
+type ErrorHandlerResponse = {
+  status: HttpError;
+  message: string;
+};
+
+export const errorHandler = (error: unknown): ErrorHandlerResponse => {
+  if (axios.isAxiosError(error) && error.response) {
+    const { status, data } = error.response;
+    console.log(status, data);
+    if (
+      status === HttpError.ServerError &&
+      data?.message.includes("Incorect")
+    ) {
+      return {
+        status: HttpError.Unauthorized,
+        message: "The email or password was incorrect",
+      };
+    }
+    return { status, message: data };
+  }
+  return {
+    status: HttpError.ServerError,
+    message: "There was an error, please try again",
+  };
+};

+ 12 - 0
yarn.lock

@@ -419,6 +419,13 @@ axe-core@^4.3.5:
   resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
   integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==
 
+axios@^0.26.1:
+  version "0.26.1"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
+  integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
+  dependencies:
+    follow-redirects "^1.14.8"
+
 axobject-query@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -1017,6 +1024,11 @@ flatted@^3.1.0:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
   integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
 
+follow-redirects@^1.14.8:
+  version "1.14.9"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
+  integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
+
 fraction.js@^4.1.2:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"